你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發綜合 >> objc.io 4.3 運用 sqlite 替代 CoreData (轉)

objc.io 4.3 運用 sqlite 替代 CoreData (轉)

編輯:IOS開發綜合

憑良知講,我不能通知你不去運用 Core Data。它不錯,而且也在變得更好,並且它被很多其他 Cocoa 開發者所了解,當有新人參加你的團隊或許需求他人接手你的 app 的時分,這點很重要。

更重要的是,不值得花時間和精神去寫自己的零碎去替代它。運用 Core Data 吧。真的。

為什麼我不運用Core Data

Mike Ash 寫到:

就團體而言,我不是個狂熱粉絲。我發現 (Core Data 的) API 是蠢笨的,並且框架自身關於超越一定數量級的數據的處置是極端遲緩的。

一個實踐的例子:10,000 個條目

想象一個 XmlRss/ target=_blank class=infotextkey>XmlRss/ target=_blank class=infotextkey>Rss 閱讀器,一個用戶可以在一個 feed 上點擊右鍵,並且選擇標志一切為已讀。

實踐完成上,我們有一個帶有 read 屬性的 Article 實體。把一切條目的記為已讀,app 需求加載這個 feed 的一切文章 (能夠經過一對多的關系),然後設置 read 屬性為 YES。

大局部時分這樣是沒問題的。但是想象那個 feed 有 200 篇文章,為了防止阻塞主線程,你能夠思索在後台線程裡做這個任務 (尤其是假如這個 app 是一個 iPhone app)。一旦你開端運用 Core Data 多線程的時分,事情就開端變得不益處理了。

這能夠還沒這麼蹩腳,至多不值得丟棄運用 Core Data。

但是,再添加同步。

我用過兩個不同的 XmlRss/ target=_blank class=infotextkey>XmlRss/ target=_blank class=infotextkey>Rss 同步 API,它們前往已讀文章的 uniqueID 數組。其中一個前往近 10,000 個 ID。

你不會計劃在主線程中加載 10,000 篇文章,然後設置 read 為 NO。你大約也不會想在後台線程裡加載 10,000 篇文章,即便很小心腸管理內存。這裡有太多的任務(假如你頻繁的這麼做,想一下對電池壽命的影響)。

概念下去說,你真正想要做的是,讓數據庫將 uniqueID 列表裡的每一篇文章的 read 設置為 YES。

SQLite 可以做到這個,只用一次調用。假如 uniqueID 上有索引,這會很快。而且你可以在後台線程執行,這和在主線程執行一樣容易。

另一個例子:疾速啟動

我的另一個 app,我想增加啟動時間 — 不只是 app 的啟動時間,還無數據顯示之前所需求的時間。

這是個相似 Twitter 的 app (雖然它不是):它顯示音訊的時間軸。顯示時間軸意味著獲取音訊,並加載相關用戶。它很快,但是在啟動的時分,會填充 UI,然後填充數據。

關於 iPhone app(或許一切使用),我的實際是,啟動時間比其他大局部開發者想的都要重要。啟動時間很慢的 app 是不太能夠被啟動的,由於人們潛認識裡會記住,並且在啟動那個使用這件事情上構成一種抵抗心思。增加啟動時間可以增加這種阻力,用戶也會更情願運用你的使用,並且把它引薦給其別人。這是你讓你的 app 成功的一局部。

由於我不運用 Core Data,我手頭有一個復雜的,保守的處理方案。我把時間軸(音訊和人物對象)經過 NSCoding 保管到一個 plist 文件中。啟動的時分它讀取這個文件,創立音訊和人物對象,UI 一呈現就顯示時間軸。

這分明的增加了延遲。

把音訊和人物對象作為 NSManagedObject 的實例對象,這是不能夠的。(假定我曾經編碼並且存儲對象的 IDs,但是那意味著讀取 plist 文件,之後再觸及數據庫。這種方式我完全防止了數據庫)。

(在更新更快的機器出來後, 我去掉了那些代碼。回憶過來,我希望我可以把它留上去。)

我怎樣思索這個問題

當思索能否運用 Core Data,我思索上面這些事情:

會有難以相信數量的數據嗎?

關於一個 XmlRss/ target=_blank class=infotextkey>Rss 閱讀器或許 Twitter app,答案不言而喻:是的。有些人關注上百團體。一團體能夠訂閱了上千個 feed。

即便你的使用不從網絡獲取數據,用戶依然有能夠自動添加數據。假如你用一個支持 AppleScript 的 Mac,有人會寫腳本去加載十分多的數據。假如經過 web API 去添加數據也是一樣的。

會有一個 Web API 包括相似於數據庫的後果嗎(比照於相似對象的後果)?

一個 RSS 同步 API 可以前往一個已讀文章的 uniqueID 列表。一個筆記的使用的一個同步 API 能夠前往已存檔的和已刪除的筆記的 uniqueID 列表。

用戶能夠經過操作處置少量對象嗎?

在底層,需求思索和之前一樣的問題。當有人刪除一切曾經下載的 5,000 個面食食譜,你的食譜 app 功能如何?(在 iPhone 上?)

假如我決議運用 Core Data(我曾經發布過運用 Core Data 的使用),我會特別留意我如何運用它。後果為了失掉好的功能,我發現我把它當做了一個奇異接口的 SQL 數據庫在運用,然後我就知道了,我應該捨棄 Core Data,而去直接運用 SQLite。

我如何運用 SQLite

我經過 FMDB Wrapper 來運用 SQLite,FMDB 來自 Flying Meat Software,由 Gus Mueller 開發。

根本操作

在運用 iPhone 和 Core Data 之前,我就運用過 SQLite。這裡有關於它如何任務的要點:

一切數據庫訪問 - 讀和寫 - 發作在一個後台線程的延續的隊列裡。在主線程中觸及數據庫是歷來不被允許的。運用一個延續隊列來保證每一件事是按順序發作的。 我少量運用 blocks 使得異步編程容易些。 模型對象只存在在主線程(但有兩個重要的例外),改動會觸發一個後台保管。 模型對象列出來它們在數據庫中存儲的屬性。這能夠在代碼裡或許在 plist 文件裡。 有些模型對象是獨一的,有些不是。取決於 app 的需求(大局部狀況是獨一的)。 對關系型數據,我盡能夠防止創立查詢表。 一些對象類型在啟動的時分就完全讀入內存,另一些對象類型我能夠創立和維護的只要它們 uniqueID 的一個 NSMutableSet,所以我可以在不去碰數據庫的狀況下就知道什麼存在、什麼不存在。 Web API 的調用發作在後台線程,它們運用“別離“的模型對象。

我會運用我目前的 app 的代碼來描繪。

數據庫更新

在我最近的 app 中,有一個單一的數據庫控制器 - VSDatabaseController,它經過 FMDB 來與 SQLite 對話。

FMDB 區分更新和查詢。更新數據庫,app 調用:

-[VSDatabaseController runDatabaseBlockInTransaction:(VSDatabaseUpdateBlock)databaseBlock]

VSDatabaseUpdateBlock很復雜:

typedef void (^VSDatabaseUpdateBlock)(FMDatabase *database);

runDatabaseBlockInTransaction也很復雜:

- (void)runDatabaseBlockInTransaction:(VSDatabaseUpdateBlock)databaseBlock {
    dispatch_async(self.serialDispatchQueue, ^{
        @autoreleasepool {
            [self beginTransaction];
            databaseBlock(self.database);
            [self endTransaction];
        }
    });
}

(留意我用的自己的延續 dispatch 隊列。Gus 建議看一下 FMDatabaseQueue,這也是一個延續調度隊列。由於它比 FMDB 剩下的其他東西都要新,所以我自己還沒有去看過。)

beginTransactionendTransaction 的調用是可嵌套的(在我的數據庫控制器裡)。在適宜的時分他們會調用 -[FMDatabase beginTransaction]-[FMDatabase commit]。(運用 transactions 是讓 SQLite 變快的一大關鍵。)提示:我在 -[NSThread threadDictionary] 中存儲以後的 transaction 的計數。這關於針對每個線程的數據來說是很方便的,我也簡直從不必它做其他的事情。

這兒有個調用更新數據庫的復雜例子:

- (void)emptyTagsLookupTableForNote:(VSNote *)note {
    NSString *uniqueID = note.uniqueID;
    [self runDatabaseBlockInTransaction:^(FMDatabase *database) {
        [database executeUpdate:
            @"delete from tagsNotesLookup where noteUniqueID = ?;", uniqueID];
    }];
}

這闡明了不少事情。首先, SQL 並不可怕。即便你從沒見過它,你也知道這行代碼做了什麼。

VSDatabaseController 的一切其他公共接口一樣,emptyTagsLookupTableForNote 也應該在主線程中被調用。模型對象只能在主線程中被援用,所以在 block 中運用 uniqueID ,而不是 VSNote 對象。

留意在這種狀況下,我更新了一個查詢表。Notes 和 tags 有一個多對多關系,一種表現方式是用一個數據庫表映射 note uniqueIDs 和 tag uniqueIDs。這些表不會很難維護,但是假如能夠,我盡量防止運用它們。

留意在更新字符串中的 ?-[FMDatabase executeUpdate:] 是一個可變參數函數。SQLite 支持運用占位符 - ? 字符 - 所以你不需求把實踐的值放入字符串中去。這是一個平安上的考量:它可以守護順序防止 SQL 注入。它也可以協助你增加必需 escape 值這樣的不用要的費事。

最後,留意在 tagsNotesLookup 表中,有一個 noteUniqueID 的索引(索引是 SQLite 功能的又一個關鍵)。這行代碼在每次啟動時都調用:

[self.database executeUpdate:
    @"CREATE INDEX if not exists noteUniqueIDIndex on tagsNotesLookup (noteUniqueID);"];
數據庫獲取

要獲取對象,app 調用:

-[VSDatabaseController runFetchForClass:(Class)databaSEObjectClass 
                             fetchBlock:(VSDatabaseFetchBlock)fetchBlock 
                      fetchResultsBlock:(VSDatabaseFetchResultsBlock)fetchResultsBlock];

這兩行代碼做了大局部任務:

FMResultSet *resultSet = fetchBlock(self.database);
NSArray *fetchedObjects = [self databaSEObjectsWithResultSet:resultSet 
                                                       class:databaSEObjectClass];

用 FMDB 查找數據庫前往一個 FMResultSet. 經過 resultSet 你可以逐句循環,創立模型對象。

我建議寫通用的代碼去將數據庫中的行轉換為對象。一種我曾經運用的辦法是在 app 中用一個 plist 文件,將列的名字映射到模型對象的屬性上去。它也包括類型,所以你知道是調用 -[FMResultSet dateForColumn:]還是 -[FMResultSet stringForColumn:]或是其他辦法。

在我的最新 app 裡我做的事情更復雜。數據庫行剛好對應模型對象屬性的名字。除了那些名字以 “Date” 開頭的屬性以外,一切屬性都是字符串。復雜,但是你可以看到所需求分明明晰的對應關系。

獨一對象

創立模型對象的操作和從數據庫獲取數據操作在異樣的後台線程停止。一但獲取到,app 會把它們轉到主線程。

通常我會運用獨一對象。數據庫裡的同一行,一直對應著異樣的一個對象。

為了做到獨一,我運用 NSMapTable 創立了一個對象緩存,在 init 函數裡:_objectCache = [NSMapTable weakToWeakObjectsMapTable]。我來解釋一下:

例如,當你停止一個數據庫獲取操作並且把對象轉交給一個視圖控制器時,你希望在這個視圖控制器運用完這些對象後,或許在一個不一樣的視圖控制器被顯示後,這些對象可以消逝。

假如你的對象緩存是一個 NSMutableDictionary,那你將需求做一些額定的任務來清空緩存中的對象。保證它只援用了那些其他中央有援用的對象是一件十分讓人蛋疼的事情。而運用配合弱援用的NSMapTable,這個問題就被自動處置掉了。

所以:我們在主線程中讓對象獨一。假如一個對象曾經在對象緩存中存在,我們就用那個存在的對象。(由於主線程中對象能夠有改動,因而在抵觸時我們運用主線程的對象。)假如對象緩存中沒有,它會被加上。

堅持對象在內存中

有很屢次,把整個對象類型保存在內存中是有道理的。我最新的 app 有一個 VSTag 對象。雖然能夠有成百上千篇筆記,但 tags 的數量很小,根本少於十個。一個 tag 只要 6 個屬性:三個 BOOL,兩個很小的 NSstring,還有一個 NSDate。

啟動的時分,app 獲取一切 tags 並且把它們保管在兩個字典裡,其中一個的鍵是 tag 的 uniqueID,另一個的鍵是 tag 名字的小寫。

這簡化了很多事,比方 tag 自動補全零碎,就可以完全在內存中操作,而不需求從數據庫獲取了。

但是很屢次,把一切數據保存在內存中是不實踐的。比方我們不會在內存中保存一切筆記。

但是也有很屢次,把一切對象保管在內存中是不可行的。當不能在內存中保存一個對象類型時,你能夠會希望在內存中保存一切 uniqueID,你可以停止這樣一個獲取操作:

FMResultSet *resultSet = [self.database executeQuery:@"select uniqueID from some_table"];

resultSet 只包括了 uniqueIDs, 你可以存儲到一個 NSMutableSet 裡。

我發現有時這個對 web APIs 很有用。想象一個 API 前往從某個確定的時間當前所創立筆記的 uniqueIDs 列表。假如我本地曾經有了一個包括一切筆記 uniqueIDs 的 NSMutableSet,我可以 (經過 -[NSMutableSet minusSet]) 疾速反省能否有漏掉的筆記,然後去調用另一個 API 下載那些漏掉的筆記。這些完全不需求觸及數據庫。

但是,像這樣的事情應該小心處置。app 可以提供足夠的內存嗎?它真的簡化編程並且進步功能了嗎?

運用 SQLite 和 FMDB 來替代 Core Data,會給你帶來少量的靈敏性和運用更聰明的方法來處理問題的空間。記住有的時分聰明是好的,也有的時分聰明是一個大錯誤。

Web APIs

我的 API 調用都跑在後台進程裡(通常是用一個 NSOperationQueue,這樣我可以取消操作)。模型對象只在主線程,然後將模型對象傳遞給我的 API 調用。

詳細這麼做:一個數據庫對象有一個 detachedCopy 辦法,可以復制數據庫對象。這個復制的對象不會被我用來做獨一化的對象緩存所援用。獨一援用這個對象的中央是 API 調用,當 API 調用完畢時,這個復制的對象也就消逝了。

這是一個好的零碎,由於它意味著我可以在 API 調用裡運用模型對象。辦法看起來像這樣:

- (void)uploadNote:(VSNote *)note {
    VSNoteAPICall *apiCall = [[VSNoteAPICall alloc] initWithNote:[note detachedCopy]];
    [self enqueueAPICall:apiCall];
}

VSNoteAPICall 從別離出來的 VSNote 中獲取值,並且創立 HTTP 懇求,而不是將 note 包裝成一個字典或其他表現方式。

處置 Web API 的前往值

我對 web 的前往值做了一些相似的處置。我會對前往的 JSON 或許 XML 創立一個模型對象,這個模型對象也是別離的。它沒有存儲在獨一化模型緩存裡。

這裡有些事情是不確定的。有時我們需求用那個模型對象在在內存緩存以及數據庫兩個中央做本地修正。

數據庫通常是容易的局部。比方:我的 app 曾經有一個辦法來保管筆記對象。它運用 SQL 的 insert or replace 命令。我只需用從 web API 前往值所生成的筆記對象來停止調用,數據庫就會更新。

但是能夠異樣的對象在內存中還有一個版本,僥幸的是我們很容易找到它:

VSNote *cachedNote = [self.mapTable objectForKey:downloadedNote.uniqueID];

假如 cachedNote 存在,我會讓它從 downloadedNote中 獲取值(這局部可以共享 detachedCopy 辦法的代碼。),而不是直接交換它(這樣能夠違背獨一性)。

一旦 cachedNote 更新了,察看者會經過 KVO 發覺到變化,或許我會發送一個 NSNotification,或許兩者都做。

Web API 調用也會前往一些其他值。我提到過 RSS 閱讀器能夠取得一個已讀條目的大列表。這種狀況下,我選擇經過那個列表創立一個 NSSet,在內存的緩存中更新每一個緩存文章的 read 屬性,然後調用 -[FMDatabase executeUpdate:]

完成這個任務的關鍵是 NSMapTable 的查找是疾速的。假如你找的對象在一個 NSArray 裡,我們就得重新思索思索了。

數據庫遷移

當正常任務的時分,Core Data 的數據庫遷移功用還是蠻酷的。

但是不可防止的,它在代碼和數據庫中參加了一層。假如你更直接一點,去運用 SQLite,那麼更新數據庫也就變得越直接。

你可以平安容易的做到這點。

比方加一個表:

[self.database executeUpdate:@"CREATE TABLE if not exists tags "
    "(uniqueID TEXT UNIQUE, name TEXT, deleted INTEGER, deletedModificationDate DATE);"];

或添加一個索引

[self.database executeUpdate:@"CREATE INDEX if not exists "
    "archivedSortDateIndex on notes (archived, sortDate);"];

或添加一列:

[self.database executeUpdate:@"ALTER TABLE tags ADD deletedDate DATE"];

app 應該用相似下面這樣的代碼來首先對數據庫停止設置。當前的改動就是添加對 executeUpdate 的調用 — 我讓他們按順序執行。由於我的數據庫是我設計的,所以這不會有什麼問題(我從沒碰到功能問題,它很快)。

當然大的改動需求更多代碼。假如你的數據經過 web 獲取,有時你可以從一個新數據庫模型開端,重新下載你需求的數據。

功能技巧

SQLite 可以十分十分快,但是也可以十分慢。完全取決於你怎樣運用它。

事務

把更新包裝在事務裡。在更新前調用 -[FMDatabase beginTransaction],更新後調用 -[FMDatabase commit]

假如你不得不反標准化( Denormalize)

反標准化讓人很不爽。這個辦法是,為了減速檢索而添加冗余數據,但是它意味著你需求維護冗余數據。

我總是盡力防止它,直到這樣能有嚴重的功能差別。然後我會盡能夠少得這麼做。

運用索引

我的 app 中 tags 表的創立語句像這樣:

CREATE TABLE if not exists tags 
  (uniqueID TEXT UNIQUE, name TEXT, deleted INTEGER, deletedModificationDate DATE);

uniqueID 列是自動索引的,由於它定義為 unique。但是假如我想用 name 來查詢表,我能夠會在name上創立一個索引,像這樣:

CREATE INDEX if not exists tagNameIndex on tags (name);

你可以一次性在多列上創立索引,像這樣:

CREATE INDEX if not exists archivedSortDateIndex on notes (archived, sortDate);

但是留意太多索引會降低你的拔出速度。你只需求足夠數量並且是正確的那些。

運用命令行使用

當我的 app 在模仿器裡運轉時,我會用 NSLog 輸入數據庫的途徑。我可以經過 sqlite3 的命令行來翻開數據庫。(經過 man sqlite3 命令來理解這個使用的更多信息)。

翻開數據庫的命令:sqlite3 path/to/database

翻開當前,你可以輸出 .schema 來檢查 schema。

你可以更新和查詢,這是在你的 app 運用 SQL 之前就將它們正確地預備妥當的很好的方式。

這外面最酷的一局部是,SQLite Explain Query Plan 命令,你會希望確保你的語句執行的盡能夠快。

真實的例子

我的 app 顯示一切沒有歸檔筆記的標簽列表。每當筆記或許標簽有變化,這個查詢就會重新執行一次,所以它需求很快。

我可以用 SQL join 來查詢,但是這會很慢(join 都很慢)。

所以我保持 sqlite3 並開端嘗試別的辦法。我又反省了一次我的 schema,認識到我可以反標准化。一個筆記的歸檔形態可以存儲在 notes 表裡,它也可以存儲在 tagsNotesLookup 表。

然後我可以執行一個查詢:

select distinct tagUniqueID from tagsNotesLookup where archived=0;

我曾經有了一個在 tagUniqueID 上的索引。所以我用 explain query plan 來通知我當我執行這個查詢的時分會發作什麼。

sqlite> explain query plan select distinct tagUniqueID from tagsNotesLookup where archived=0;
0|0|0|SCAN TABLE tagsNotesLookup USING INDEX tagUniqueIDIndex (~100000 rows)

它用了一個索引,這很不錯,但是 SCAN TABLE 聽起來不太好,最好是一個 SEARCH TABLE 加上掩蓋索引的方式。

我在 tagUniqueID 和 archive 上建了索引:

CREATE INDEX archivedTagUniqueID on tagsNotesLookup(archived, tagUniqueID);

再次執行 explain query plan:

sqlite> explain query plan select distinct tagUniqueID from tagsNotesLookup where archived=0;
0|0|0|SEARCH TABLE tagsNotesLookup USING COVERING INDEX archivedTagUniqueID (archived=?) (~10 rows)

如今好多了。

更多功能提示

FMDB 的某處加了緩存 statements 的才能,所以當創立或翻開一個數據庫的時分,我總是調用 [self.database setShouldCacheStatements:YES]。這意味著對每個調用你不需求再次編譯每個 statement。

我歷來沒有找到關於運用 vacuum 的好的指引。假如數據庫沒有活期緊縮,它會變得越來越慢。我的 app 會每周跑一次 vacuum。(在 NSUserDefaults 裡存儲上次 vacuum 的時間,然後在開端的時分反省能否過了一周)。

運用 auto_vacuum 能夠會更好,可以參看 pragma statements supported by SQLite 列表。

其他酷的東西

Gus Mueller 讓我講講自定義 SQLite 辦法的內容。我並沒有真的運用過這些東西,不過既然他指出了,我可以擔心的說我能找到它的用途。由於它很酷。

在 Gus 的這個 gist 裡,有一個查詢是這樣的:

select displayName, key from items where UTTypeConformsTo(uti, ?) order by 2;

SQLite 完全不知道 UTTypes 的事情。但是你可以經過代碼塊來添加中心辦法,感興味的話,可以看看 -[FMDatabase makeFunctionNamed:maximumArguments:withBlock:] 辦法。

你可以執行一個大的查詢來替代,然後評價每個對象 - 但是那需求更多任務。最好在 SQL 級就過濾,而不是在將表格行轉為對象當前再做這件事情。

最後

你真的應該運用 Core Data,我不是在開玩笑。

我用 SQLite 和 FMDB 一段時間了,我對多失掉的益處感到很興奮,也失掉非同普通的功能。

但是記住設備在不時變快。也請記住,其他看你代碼的人希冀看到 Core Data,這是他們曾經理解的 - 他們不計劃看你的數據庫代碼如何任務。

所以請把這整篇文章看做一個瘋子的叫喊,關於他為自己樹立了充溢細節又瘋狂的世界 - 並把自己鎖在了外面。

有點憂傷的搖頭,並且請享用這個話題下那些超贊的 Core Data 的文章吧。

而對我來說,接上去在研討完 Gus 指出的自定義 SQLite 辦法特性後,我會研討 SQLite 的 全文搜索擴展。 總有更多的內容需求不時去學習。

原文 On Using SQLite and FMDB Instead of Core Data

譯文 談談用SQLite和FMDB而不必Core Data

精密校正 sjpsega

【objc.io 4.3 運用 sqlite 替代 CoreData (轉)】的相關資料介紹到這裡,希望對您有所幫助! 提示:不會對讀者因本文所帶來的任何損失負責。如果您支持就請把本站添加至收藏夾哦!

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved