使用SQLite缓存数据分析与实现

/ 0

前言:如果你准备看这篇文章,并且在此之前并没有使用过SQLite,建议先看 SQLite数据库框架之FMDB ,其中有用到SQL语句,如果不会的可以先可以看 iOS数据库操作中的常见SQL语句 。

封装思想

在讲解SQLite之前,先聊聊封装思想。。写代码的都知道,面向对象三大思想(有些说四大,这个不是重点)之一的封装性,这里就举一个栗子来说明一下什么是封装。

比如控制器需要数据,控制器就跟模型说,我要数据,请给我数据。对于数据是怎么来的,控制器并不关心,控制器只关心模型有没有给他数据。

然后模型呢?控制器找模型要数据,模型也没有啊,所以模型去找数据访问层要数据,数据访问层的数据是怎么来的,模型也不关心,他关心的也只是有没有给他数据。

然后数据访问层呢?数据访问层自己也是没有数据的,他也得去找他的上一级要数据啊。

找谁要呢?这里就要分两种情况,第一种是本地有数据,他将本地数据直接返回给模型,模型再返回给我。第二种情况是本地没有数据,他就去网络请求类要,最终网络请求类才和后端交互,去后端请求数据,并将数据请求回来后保存到本地缓存,并返回给数据访问层,数据访问层再返回给模型,模型返回给控制器。

所以,这样就形成了一个数据请求数据响应的链条:

QQ20160614-1

上面栗子中提到了数据访问层(Data Access Layer),可能有些朋友对这个词比较陌生,你就当做是模型和数据之间的桥梁就行了。

需求分析

这里我以 六阿哥客户端 的(首页列表数据)为例,来一步步实现数据缓存。

QQ20160614-2

需求:列表数据如果本地有缓存,就直接加载本地数据,如果没有才去请求网络。并且在下拉刷新的时候清除列表的缓存数据,并把新请求回来的数据缓存到本地。

这里涉及到数据处理的类有,网络工具类、数据访问层类、列表模型类、控制器。当然我们这里并不是直接使用SQLite类库来缓存数据,而是通过FMDB(对SQLite进行封装的第三方类库),你也可以通过系统的CoreData来缓存数据。不过,我个人更喜欢FMDB,直接通过SQL语言管理数据。

代码实现

在进行数据缓存前,我们最好先对FMDB进行一次封装,让我们用起来更方便。这里我创建一个工具类JFSQLiteManager,直接贴代码,如果不熟悉FMDB的,建议先看 SQLite数据库框架之FMDB 

import UIKit
import FMDB

let NEWS_LIST_HOME_LIST = "jf_newslist_homelist"   // 首页 列表页 的 列表 数据表

class JFSQLiteManager: NSObject {
    
    /// FMDB单例
    static let shareManager = JFSQLiteManager()
    
    /// sqlite数据库名
    private let newsDBName = "news.db"
    
    let dbQueue: FMDatabaseQueue
    
    override init() {
        let documentPath = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentDirectory, NSSearchPathDomainMask.UserDomainMask, true).last!
        let dbPath = "\(documentPath)/\(newsDBName)"
        print(dbPath)
        
        // 根据路径创建并打开数据库,开启一个串行队列
        dbQueue = FMDatabaseQueue(path: dbPath)
        super.init()
        
        // 创建数据表
        createNewsListTable(NEWS_LIST_HOME_LIST)
    }
    
    private func createNewsListTable(tbname: String) {
        
        let sql = "CREATE TABLE IF NOT EXISTS \(tbname) ( \n" +
            "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, \n" +
            "classid INTEGER, \n" +
            "news TEXT, \n" +
            "createTime VARCHAR(30) DEFAULT (datetime('now', 'localtime')) \n" +
        ");"
        
        dbQueue.inDatabase { (db) in
            if db.executeStatements(sql) {
                print("创建 \(tbname) 表成功")
            } else {
                print("创建 \(tbname) 表失败")
            }
        }
    }
    
}

控制器向模型请求数据:

private func loadNews(classid: Int, pageIndex: Int, method: Int) {
    
    JFArticleListModel.loadNewsList(classid, pageIndex: pageIndex, type: 1) { (articleListModels, error) in
        
        self.tableView.mj_header.endRefreshing()
        self.tableView.mj_footer.endRefreshing()
        
        guard let list = articleListModels where error != true else {
            return
        }
        
        // id越大,文章越新
        let maxId = self.articleList.first?.id ?? "0"
        let minId = self.articleList.last?.id ?? "0"
        
        if method == 0 {
            // 0下拉加载最新 - 会直接覆盖数据,用最新的10条数据
            if Int(maxId) < Int(list[0].id!) {
                self.articleList = list
            }
        } else {
            // 1上拉加载更多 - 拼接数据
            if Int(minId) > Int(list[0].id!) {
                self.articleList = self.articleList + list
            } else {
                self.tableView.mj_footer.endRefreshingWithNoMoreData()
            }
        }
        
        self.tableView.reloadData()
    }
    
}

模型向数据访问层请求数据:

class func loadNewsList(classid: Int, pageIndex: Int, type: Int, finished: (articleListModels: [JFArticleListModel]?, error: NSError?) -> ()) {
    
    // 模型找数据访问层请求数据 - 然后处理数据回调给调用者直接使用
    JFNewsDALManager.shareManager.loadNewsList(classid, pageIndex: pageIndex, type: type) { (result, error) in
        
        // 请求失败
        if error != nil || result == nil {
            finished(articleListModels: nil, error: error)
            return
        }
        
        // 没有数据了
        if result?.count == 0 {
            finished(articleListModels: [JFArticleListModel](), error: nil)
            return
        }
        
        let data = result!.arrayValue
        var articleListModels = [JFArticleListModel]()
        
        // 遍历转模型添加数据
        for article in data {
            let postModel = JFArticleListModel(dict: article.dictionaryObject!)
            articleListModels.append(postModel)
        }
        
        finished(articleListModels: articleListModels, error: nil)
    }
    
}

数据访问层判断是去本地还是网络请求数据:

func loadNewsList(classid: Int, pageIndex: Int, type: Int, finished: (result: JSON?, error: NSError?) -> ()) {
    
    // 先从本地加载数据
    loadNewsListFromLocation(classid, pageIndex: pageIndex, type: type) { (success, result, error) in
        
        // 本地有数据直接返回
        if success == true {
            finished(result: result, error: nil)
            print("加载了本地数据 \(result)")
            return
        }
        
        // 本地没有数据才从网络中加载
        JFNetworkTool.shareNetworkTool.loadNewsListFromNetwork(classid, pageIndex: pageIndex, type: type) { (success, result, error) in
            
            if success == false || error != nil || result == nil {
                finished(result: nil, error: error)
                return
            }
            
            // 缓存数据到本地
            self.saveNewsListData(classid, data: result!, type: type)
            finished(result: result, error: nil)
            print("加载了远程数据 \(result)")
        }
    }
    
}

本地请求数据方法:

private func loadNewsListFromLocation(classid: Int, pageIndex: Int, type: Int, finished: NetworkFinished) {
    
    var sql = ""
    // 计算分页
    let pre_count = (pageIndex - 1) * 20
    let oneCount = 20
        
    if classid == 0 {
        sql = "SELECT * FROM \(NEWS_LIST_HOME_LIST) ORDER BY id ASC LIMIT \(pre_count), \(oneCount)"
    }
    
    JFSQLiteManager.shareManager.dbQueue.inDatabase { (db) in
        
        var array = [JSON]()
        
        let result = try! db.executeQuery(sql, values: nil)
        while result.next() {
            let newsJson = result.stringForColumn("news")
            let json = JSON.parse(newsJson)
            array.append(json)
        }
        
        if array.count > 0 {
            finished(success: true, result: JSON(array), error: nil)
        } else {
            finished(success: false, result: nil, error: nil)
        }
        
    }
    
}

远程请求数据的方法:

func loadNewsListFromNetwork(classid: Int, pageIndex: Int, type: Int, finished: NetworkFinished) {
    
    var parameters = [String : AnyObject]()
    
    if type == 1 {
        parameters = [
        "classid" : classid,
        "pageIndex" : pageIndex, // 页码
        "pageSize" : 20          // 单页数量
        ]
    } else {
        parameters = [
        "classid" : classid,
        "query" : "isgood",
        "pageSize" : 3
        ]
    }
    
    JFNetworkTool.shareNetworkTool.get(ARTICLE_LIST, parameters: parameters) { (success, result, error) -> () in
        
        guard let successResult = result where success == true else {
            finished(success: false, result: nil, error: error)
            return
        }
        finished(success: true, result: successResult["data"], error: nil)
    }
}

本地缓存数据的方法:

private func saveNewsListData(saveClassid: Int, data: JSON, type: Int) {
    
    var sql = ""
    if saveClassid == 0 {
        sql = "INSERT INTO \(NEWS_LIST_HOME_LIST) (classid, news) VALUES (?, ?)"
    }
    
    JFSQLiteManager.shareManager.dbQueue.inTransaction { (db, rollback) in
        
        guard let array = data.arrayObject as! [[String : AnyObject]]? else {
            return
        }
        
        // 每一个字典是一条资讯
        for dict in array {
            
            // 资讯分类id
            let classid = Int(dict["classid"] as! String)!
            
            // 单条资讯json数据
            let newsData = try! NSJSONSerialization.dataWithJSONObject(dict, options: NSJSONWritingOptions(rawValue: 0))
            let newsJson = String(data: newsData, encoding: NSUTF8StringEncoding)!
            
            if db.executeUpdate(sql, withArgumentsInArray: [classid, newsJson]) {
                print("缓存数据成功 - \(classid)")
            } else {
                print("缓存数据失败 - \(classid)")
                rollback.memory = true
                break
            }
        }
        
    }
    
}

下拉刷新的时候,需要清理当前分类的数据,这里也是调用模型的清理方法:

@objc private func updateNewData() {
    
    // 有网络的时候下拉会自动清除缓存
    if true {
        JFArticleListModel.cleanCache(classid!)
    }
    
    loadNews(classid!, pageIndex: 1, method: 0)
}

然后模型调用数据访问层的清理方法:

class func cleanCache(classid: Int) {
    JFNewsDALManager.shareManager.cleanCache(classid)
}

最终在数据访问层完成清理操作:

func cleanCache(classid: Int) {
    var sql = ""
    if classid == 0 {
        sql = "DELETE FROM \(NEWS_LIST_HOME_TOP); DELETE FROM \(NEWS_LIST_HOME_LIST);"
    }
    
    JFSQLiteManager.shareManager.dbQueue.inDatabase { (db) in
        
        if db.executeStatements(sql) {
            // print("清空表成功 classid = \(classid)")
        } else {
            // print("清空表失败 classid = \(classid)")
        }
    }
}

代码有些多,请把 六阿哥客户端 下载到本地,根据本文思路去理清比较好。只要理解了上面那张数据请求数据响应的流程,剩下的都是一些基本代码了。而关于缓存策略,缓存清除和缓存保存,则是按照自己的项目需求来针对性实现。