雖然從接觸iOS開發開始,做的每一個項目都在用Core Data,但是一些比較底層的東西都是boss寫的或者用的是Restkit這個開源項目。所以雖然之前看過兩遍Core Data Programming Guide,還有很多不理解或者不熟練或者做得不好的地方。所以第三遍看,想要把以前不理解的地方弄得清晰一點。
Core Data框架是獨立于Cocoa的,所以對于沒有界面的程序,也可以應用Core Data。為了方便,這里我自己試驗的代碼都是在一個Command Line tool工程里的0.0
關于Persistent Stack
對象和外部數據存儲,這兩者之間的媒介,被整體叫做persistence stack。其中,managed object context位于棧頂,persistent object store位于棧底,中間的是persistent store coordinator。
實際上,是persistent store coordinator決定著這個棧。它使用了facade模式,使得棧底的多個persistent store,在呈現給context的時候,就像一個整體一樣。
一個coordinator只能和一個managed object model相關聯。
關于Managed Object Model
一個managed object model是NSManagedObjectModel類的實例。它描述了第三方app中需要使用到的一系列entity,和多個entity之間的關系。
一個model中可能有很多NSEntityDescription對象來代表這個model的各個entity。對于每個entity來說,有兩個很重要的特性,一個是這個entity的名字,另一個是在運行時,表示這個entity的類的名字。
一個entity可能會有attribute、relationship,也可能有fetched property,這三者統稱為property。需要注意的是,property不能和NSObject或NSManagedObject已有的方法名重疊,比如,不能給某個property起名為“description”。
比較特殊的一種property叫做transient property,它是不會被保存到persistent store中去的。
多個entity之間可能會有繼承關系,也可能某個entity會被指定為抽象的。
大多數model中的元素(比如entity、attribute、relationship)都會有一個對應的user info。
創建一個model
使用Xcode創建model
在Xcode中,選擇File->New->File->Core Data->Data Model就可以創建一個擴展名為.xcdatamodeld的“源文件”了(實際上應該是一個目錄)。其中包含了一個擴展名為.xcdatamodel的“源文件”??梢允褂肵code的Core Data model editor,在xcdatamodel文件中編輯model的內容,比如其中包含什么樣的entity,每個entity中有什么樣的attribute,以及各個entity之間的關系,等等。
如果App更新時,需要對model進行改動,就需要創建一個新的model version。在Xcode中,選中xcdatamodeld,選擇Editor->Add Model Version,可以繼續創建其中的xcdatamodel“源文件”。
除了model中關于entity和property的各種信息,xcdatamodel還會包含一些其他信息,比如繪制的圖表的寬高排列之類的,但這些信息在運行時并沒有什么意義。所以,model文件的編譯工具momc會把運行時沒有意義的信息去掉,將xcdatamodel文件編譯成mom文件,將xcdatamodeld目錄編譯成momd目錄。
在Xcode中找到編譯好的.app文件,右鍵Show in Finder,打開里面的內容后,可以看到其中的.momd文件夾,和這個文件夾里面的.mom文件。
如果寫的是iOS上的app,則在需要程序員自己加載model文件。有這樣兩種方法:
- 使用NSManagedObjectModel的initWithContentOfURL:方法。
這是一種比較普遍使用的方法。
NSURL *modelURL = [[NSBundle mainBundle] URLForResource:modelName withExtension:@"momd"];
NSManagedObjectModel *model = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
- 使用
mergedModelFromBundles:
方法.
如果參數是nil,則會搜索main bundle,把其中的所有model給merge起來。
在代碼中創建\修改model
在model被一個managed object context或者一個persistent store coordinator使用之前,這個model是可以在代碼中被修改的。這允許程序員動態的創建或修改model。
試了一下在代碼中創建model:
NSManagedObjectModel *model = [[NSManagedObjectModel alloc] init];
NSEntityDescription *launchInfoEntity = [[NSEntityDescription alloc] init];
[launchInfoEntity setName:@"LaunchInfo"];
NSAttributeDescription *dateAttribute = [[NSAttributeDescription alloc] init];
[dateAttribute setName:@"date"];
[dateAttribute setAttributeType:NSDateAttributeType];
[dateAttribute setOptional:NO];
[launchInfoEntity setProperties:@[dateAttribute]];
[model setEntities:@[launchInfoEntity]];
如果model是在被一個managed object context或者一個persistent store coordinator使用之后,受到改動,則會拋出exception:
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Can't modify an immutable model.'
Fetch Request Template
程序員可以使用NSFetchRequest類來描述從持久化存儲中取得一些對象的請求。在實際的開發中,同樣或相似的請求往往會被執行多次,所以,程序員可以自定義一些fetch request template,并把它們存到model中??梢允褂肵code的Core Data model editor,也可以在代碼中定義。
使用Core Date model editor定義fetch request template
Editor->Add FetchRequest來新建一個fetch request。
填寫Predicate,可以使用變量。右邊欄還可以指定一些高級選項。
在需要使用時,只要在代碼中取出對應的fetch request template:
NSManagedObjectModel *managedObjectModel = [[context persistentStoreCoordinator] managedObjectModel];
NSFetchRequest *fetchRequest = [managedObjectModel fetchRequestFromTemplateWithName:@"fetchLaunchInfoBeforeSomeDate"
substitutionVariables:@{@"DATE" : [NSDate date]}];
NSArray *fetchResult = [context executeFetchRequest:fetchRequest error:&error];
就可以正常使用了。
直接在代碼中創建fetch request template
也可以完全動態的創建fetch request template:
NSManagedObjectModel *managedObjectModel = [[context persistentStoreCoordinator] managedObjectModel];
NSFetchRequest *fetchRequestTemplate = [[NSFetchRequest alloc] initWithEntityName:@"LaunchInfo"];
[fetchRequestTemplate setPredicate:[NSPredicate predicateWithFormat:@"date > $DATE"]];
[managedObjectModel setFetchRequestTemplate:fetchRequestTemplate forName:@"fetchLaunchInfoAfterSomeDate"];
關于Configuration
如果程序員想要把不同的entity存放到不同的persistent store中去,應該怎么做呢?一個coordinator只能對應一個managed object model,所以在默認情況下,每一個與這個coordinator相關聯的persistent store,都存放了同樣的entity。為了避免這樣的限制,可以使用Configuration來指定每個persistent store中應該存放哪些entity。
指定了Configuration之后,當程序員取這些對象的時候,它們會自動從不同的文件中被取出;保存時,它們也會被自動保存到不同的文件。
一個configuration由名字和若干entity組成??梢栽诖a中用
setEntities:forConfiguration:
方法動態的定義configuration;
也可以在Core Data editor tool中定義:
每當給coordinator增加persistent store的時候,只用在configuration參數中指定對應的configuration即可以使用:
if (![coordinator addPersistentStoreWithType:NSSQLiteStoreType
configuration:@"ExitInfoConfiguration"
URL:exitInfoStoreURL
options:nil
error:&error]) {
//Handle error
}
if (![coordinator addPersistentStoreWithType:NSSQLiteStoreType
configuration:@"LaunchInfoConfiguration"
URL:launchInfoStoreURL
options:nil
error:&error]) {
//Handle error
}
關于Managed Object
一個managed object代表的是一個entity的實例。
每個managed object與一個managed object context相關聯。在一個特定的context中,持久化存儲中的一個特定的記錄,只能有一個對應的managed object,這種技術叫做Uniquing。但是,也可能有多個context,每個context都持有一個表示同一條記錄的managed object。
關于accessor方法
可以使用Xcode根據xcdatamodel中的內容自動生成NSManagedObject的子類。在子類的實現中,我們能看到,property被@dynamic
修飾了。那是因為Core Data會在運行時動態生成accessor方法,這樣生成的accessor方法是比較高效的,也就是說,程序員一般不需要寫自定義的accessor方法。
也可以通過key-value的形式來獲取或設置attributes的值,但是在性能上KVC不如accessor方法,所以只應該在必要的情況下使用。
如果這個managed object有to-many relationship,很多時候,程序員可能會需要增添、刪除或改動這個to-many relationship中的某幾個元素,這個時候則應該使用mutableSetValueForKey:
方法或者動態生成的relationship mutator方法。
關于Managed Object的生命周期
一個managed object的生命周期和標準的Cocoa對象的生命周期不太一樣,因為那是由Core Data來管理的。一個managed object表示的數據的生命周期,和這個manged object的實例的生命周期是獨立的。
可以通過一個managed object得到它所在的context,也可以通過一個context得到其中的managed object。但是默認情況下,managed object和context之間的引用是弱引用。然而有一種例外情況,context會對“被改動過的”managed object持強引用,這里的改動包括插入、刪除和修改,直到context被save、reset或者rollback。同時,undo manager也會用強引用來維持被改動過的managed object。
可以用setRetainsRegisteredObjects:
方法改變這種默認情況,使得context對managed object持強引用。
當managed object有relationship的時候,它會對這個關聯的對象持強引用,這也意味著可能有強引用循環出現。所以,當使用完一個managed object的時候,應該用refreshObject:mergeChanges:
方法讓它成為一個fault。
在一個managed object被創建的時候,其中每個property的值是在對應的entity中的default value。如果需要做一些自定義的初始化,建議重寫:awakeFromInsert
或者awakeFromFetch
方法。
其中,awakeFromInsert
會在調用了initWithEntity:insertIntoManagedObjectContext:
或者insertNewObjectForEntityForName:inManagedObjectContext:
方法之后立刻被調用。所以,重寫這個方法,主要是可以為managed object中的property提供特殊的默認值,比如這個對象被創建的時間。
awakeFromFetch
方法會在managed object從一個持久化存儲中被取出來的時候調用。重寫這個方法,可以用于建立transient值和緩存。需要注意的是,如果在這個方法中,改變了managed object中某些property,context不會被認為是dirty的。這也就意味著不應該在這個方法中操縱relationship,因為目標對象不會為此做出應有的改變。
initWithEntity:insertIntoManagedObjectContext:
這個方法也可以重寫,但是并不鼓勵這樣做。因為在重寫的這個方法中改變的狀態,可能會不支持undo和redo。
在需要“析構”的時候,不應該重寫dealloc
方法,而是應該重寫didTurnInfoFault
方法。這個方法會在managed object變成fault的時候被調用,也就是說會比真正的析構早一些。
關于Relationship
大多數的relationship天生就是雙向的(一個主要的例外就是fetched property)。一般來說,在使用Core Data的時候,也應該為relationship指定反向關系,這樣可以確保object graph的一致性。
一個relationship是有delete rule的。這指定了當這個對象即將被刪除的時候應該發生的行為。有這樣幾種delete rule:
Deny
如果至少有一個relationship的目的對象存在,源對象是不能被刪除的;Nullify
在刪除當前對象的同時,將relationship的目的對象的反向關系設置為null;Cascade
在刪除當前對象的同時,也刪除relationship的目的對象;No Action
在刪除當前對象的同時,對relationship的目的對象不做任何操作。在使用這個delete rule的時候,程序員有責任自行維護object graph,所以應該將對應的反向關系設置成有意義的值。
關于Object ID
一個NSManagedObjectID對象是managed object的全局ID。Object ID有臨時和持久之分。當一個managed object剛剛被創建時,它將獲得一個臨時的object ID;只有當它被保存到持久化存儲中時,它才會被賦予一個持久的ID。
Object ID也可以被轉化成URI??梢允褂?managedObjectIDForURIRepresentation:
方法或objectWithID:
方法通過URI或ID獲取對應的managed object。
關于Validation
Validation機制用于檢驗managed object的property的值是否滿足一定條件。有兩種validation的類型,分別是:
- property層次的validation
- property之間的validation
Core Data允許程序員在managed object model中設定簡單的validation邏輯。比如,可以設置數字和日期的最大最小值,可以設置字符串的最大最小長度、需要匹配的正則表達式,還可以設置to-many relationship中數目的最大最小值。
除了可以對model設置這些validation邏輯,還可以在代碼中進行自定義。
如果想要自定義property層次的validation,程序員不應該重寫validateValue:forKey:error:
方法,而是應該實現validate<Key>:error:
方法。
然而,如果想要自行檢查某個property是否符合規定,應該調用的是validateValue:forKey:error:
方法,這個方法會將定義在managed object model中的validation邏輯也考慮進去。
也可以自定義property之間的validation。這可以通過重寫validateForUpdate:
、validateForInsert:
和validateForDelete:
方法來實現。在重寫的這三個方法中,應該首先調用父類的實現。
所有的validation限制都只有在保存操作的過程中會被應用。因為managed object context的本意就是一塊草稿板,所以應該允許其中的對象有臨時性的“不合理”。
關于Faulting
一個managed object通常會用于表示被持久化存儲的數據,但是在有些情況下,一個managed object可能是fault的,也就是說它的property還沒有從外部數據存儲中載入進來。這是Core Data用于減少內存占用的一種機制。
當訪問到一個managed object的某個持久化的property的時候,fault被觸發了,如果內存中的cache沒有被擊中的話,數據會被自動從持久化存儲中取過來,這里的開銷是比較昂貴的。
需要注意的是,description
方法是不會觸發fault的,所以打印剛剛取出來的managed object可以看到“<fault>”字樣。
比如這樣:
"<LaunchInfo: 0x10060b450> (entity: LaunchInfo; id: 0x40000b <x-coredata://4973AB39-0CD8-4480-AA07-7A3A877BE87D/LaunchInfo/p1> ; data: <fault>)"
如果重寫description
方法,并在其中訪問了某個持久化的property,則fault會被觸發。所以應該盡量避免這樣的做法。
可以使用refreshObject:mergeChanges:
并傳人參數no讓一個managed object變成fault。但是必須保證其中的relationship沒有被改變。
關于Fetching
取得指定的對象
如果app使用了多個context,那么程序員可能就需要測試一個對象是否已經從persistent store中被刪除了。這時,可以創建一個fetch request,其中這樣指定predicate:
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"self == %@", targetObject];
這樣就可以通過判斷fetch到的對象的數目是否為0來判斷目標對象是否已被刪除。其中的targetObject
可以是一個managed object,也可以是一個manged object ID。
如果一次需要測試多個目標對象是否被刪除,可以使用更高效的IN操作符:
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"self IN %@", arrayOfManagedObjectIDs];
獲取特定的值
有的時候,程序員可能不需要獲取整個managed object,而是只是需要其中的某個attribute。NSExpressionDescription可以幫助程序員取得需要的值。
這時,需要使用setResultType:
方法來指定這個fetch返回的結果類型是NSDictionaryResultType
;還需要創建NSExpressionDescription的實例,來指定哪些property是需要取得的。
官方文檔里有示例代碼,偷個懶。
還欠缺的部分
這篇博客真是拖著寫了好久。
但是還有好多內容沒有理解,因為偷懶+之前在工作中對這些部分接觸不多沒什么感受,所以先放在這里,等下一遍看的時候,再慢慢理解好了。
Localizing a Managed Object Model
Copying and Copy and Paste
Drag and Drop
Undo Management
Ensuring Data Is Up-to-Date
Change and Undo Management
Fetched Properties
Non-Standard Persistent Attributes
Associate Metadata With a Store to Provide Additional Information and Support Spotlight Indexing
Core Data and Cocoa Bindings
Change Management
Persistent Store Features
Core Data Performance
Troubleshooting Core Data
Efficiently Importing Data