Core Data基本操作

1 前言

CoreData不僅僅是數(shù)據(jù)庫(kù),而是蘋(píng)果封裝的一個(gè)更高級(jí)的數(shù)據(jù)持久化框架,SQLite只是其提供的一種數(shù)據(jù)存儲(chǔ)方法。CoreData對(duì)數(shù)據(jù)的查找做了很大的優(yōu)化,提供了大量的API。CoreData的遺憾之一是不能設(shè)置唯一主鍵,但是它可以設(shè)置實(shí)體的UserInfo(relatedByAttribute)來(lái)實(shí)現(xiàn)主鍵的功能,另外UserInfo字典也擴(kuò)展了CoreData的功能,可以通過(guò)代碼取得這個(gè)字典進(jìn)行自定義操作。但是通常并不會(huì)完全自己建立CoreData,常用的第三方框架是MagicRecord,在其GitHub主要上有設(shè)置唯一主鍵及其他詳盡的使用方法。

2 重要的類(lèi)

2.1 NSManagedObjectModel

NSManagedObjectModel可以認(rèn)為是對(duì)整個(gè)數(shù)據(jù)庫(kù)中各個(gè)表的各個(gè)字段和表之間聯(lián)系的描述,它不僅包含每個(gè)模型對(duì)象的屬性,還包含該模型對(duì)象和其他模型對(duì)象之間的關(guān)系即relationship。

2.2 NSPersistentStore

NSPersistentStore代表真正存儲(chǔ)的數(shù)據(jù),CoreData提供了SQLite等四種存儲(chǔ)模式。除SQLite外另外三種模式在對(duì)數(shù)據(jù)庫(kù)操作時(shí)需要將所有的數(shù)據(jù)全部讀入。同時(shí)SQLite是默認(rèn)的存儲(chǔ)方式。除了四種默認(rèn)的存儲(chǔ)模式外,CoreData還允許開(kāi)發(fā)者通過(guò)創(chuàng)建NSIncrementalStore來(lái)自定義存儲(chǔ)格式。

2.3 NSPersistentStoreCoordinator

NSPersistentStoreCoordinator是模型文件描述NSManagedObjectModel和數(shù)據(jù)存儲(chǔ)NSPersistentStore之間的橋梁。前者只關(guān)心整個(gè)數(shù)據(jù)庫(kù)各個(gè)表結(jié)構(gòu)及表之間的聯(lián)系,是一個(gè)抽象的概念。后者只關(guān)心數(shù)據(jù)的實(shí)際存儲(chǔ)而并不關(guān)心數(shù)據(jù)對(duì)應(yīng)的對(duì)象。Coordinator作為橋梁將數(shù)據(jù)庫(kù)文件轉(zhuǎn)化為具體的對(duì)象。

2.4 NSManagedObjectContext

NSManagedObjectContext是操作上下文,也是我們的工作區(qū),通常APP中會(huì)有一個(gè)版本的模型文件描述NSManagedObjectModel,一個(gè)對(duì)應(yīng)的數(shù)據(jù)存儲(chǔ)文件NSPersistentStore及一個(gè)它們之間的橋梁NSPersistentStoreCoordinator。和多個(gè)工作區(qū)NSManagedObjectContext。大多數(shù)時(shí)候只需要維護(hù)一個(gè)工作區(qū)即可,對(duì)數(shù)據(jù)庫(kù)的所有編輯操作都將被保存在這個(gè)工作區(qū)中,只有當(dāng)其執(zhí)行完save操作時(shí),數(shù)據(jù)庫(kù)才會(huì)被更改。多工作區(qū)的情況見(jiàn)CoreData多上下文操作。另外NSManagedObjectContext需要注意以下幾點(diǎn)。

  • 1)NSManagedObjectContext管理它創(chuàng)建或者抓取的對(duì)象的生命周期。
  • 2)一個(gè)對(duì)象必須依靠一個(gè)Context存在,每個(gè)對(duì)象都會(huì)引用管理它的Context可以通過(guò)object.managedObjectContext獲取,這是一個(gè)weak弱引用。
  • 3)一個(gè)對(duì)象在整個(gè)生命周期內(nèi)都只會(huì)對(duì)一個(gè)Context保持引用。
  • 4)一個(gè)應(yīng)用程序可以擁有多個(gè)Context。
  • 5)Context是線(xiàn)程不安全的,對(duì)每個(gè)的對(duì)象,創(chuàng)建、修改和刪除操作必須在同一個(gè)線(xiàn)程中完成。

2.5 NSPersistentStoreDescription

NSPersistentStoreDescription是在iOS10后新增的,其主要用于為NSPersistentContainer配置數(shù)據(jù)遷移和數(shù)據(jù)存儲(chǔ)的URL等信息。

2.6 NSPersistentContainer

在iOS10后,CoreDataStack概念被引入,NSPersistentContainer也是在iOS10過(guò)后新增的,它可以有效的將前四個(gè)類(lèi)的對(duì)象結(jié)合起來(lái),在程序中只用通過(guò)指定name創(chuàng)建一個(gè)NSPersistentContainer的實(shí)例,然后為它配置NSPersistentStore。CoreData會(huì)自動(dòng)創(chuàng)建其他相關(guān)的實(shí)例進(jìn)行數(shù)據(jù)庫(kù)初始化。初始化完成后的數(shù)據(jù)庫(kù)URL和主工作區(qū)都可以通過(guò)其persistentStoreDescription屬性中的URL方法拿到。另外也可以手動(dòng)指定其persistentStoreDescription屬性配置數(shù)據(jù)遷移和數(shù)據(jù)存儲(chǔ)URL等信息。

3 建立CoreData

3.1 初始化CoreData

CoreData的初始化工作需要在A(yíng)ppDelegate的applicationDidFinishLaunching中以同步方式在主線(xiàn)程中進(jìn)行,因?yàn)槿绻麛?shù)據(jù)庫(kù)無(wú)法正確初始化,整個(gè)程序的運(yùn)行都將無(wú)意義。

在iOS項(xiàng)目中使用CoreData持久化存儲(chǔ)數(shù)據(jù)可以在創(chuàng)建項(xiàng)目時(shí)勾選CoreData選項(xiàng),此時(shí)系統(tǒng)會(huì)在A(yíng)ppdelegate中自動(dòng)生成NSPersistentContainer及其相關(guān)代碼,此外工程中也將多一個(gè)以項(xiàng)目名稱(chēng)命名的.xcdatamodeld文件。但是通常不會(huì)使用系統(tǒng)自帶的這個(gè)功能而是手動(dòng)建立CoreData,并且通常使用MagicRecord第三方框架。

iOS10中蘋(píng)果引入了CoreDataStack堆棧的概念,因此下面分別介紹iOS10之前、iOS10之后、使用MagicRecord初始化CoreData的方法。

iOS10之前
lazy var managerContext: NSManagedObjectContext = {
  let context = NSManagedObjectContext(concurrencyType: .MainQueueConcurrencyType)
  let model = NSManagedObjectModel(contentsOfURL: NSBundle.mainBundle().URLForResource("Person", withExtension: "momd")!)!
  let coordinator = NSPersistentStoreCoordinator(managedObjectModel: model)
  let path = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true).last! +  "/person.db"
  let url = NSURL(fileURLWithPath: path)
  try! coordinator.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: url, options: nil)
  context.persistentStoreCoordinator = coordinator
  return context
}()

static let sharedCoreDataManager = HYFCoreDataManager()
iOS10之后
var storeURL : URL {
  let storePaths = NSSearchPathForDirectoriesInDomains(.applicationSupportDirectory, .userDomainMask, true)
  let storePath = storePaths[0] as NSString
  let fileManager = FileManager.default
  
  do {
    try fileManager.createDirectory(
      atPath: storePath as String,
      withIntermediateDirectories: true,
      attributes: nil)
  } catch {
    print("Error creating storePath \(storePath): \(error)")
  }
  
  let sqliteFilePath = storePath
    .appendingPathComponent(storeName + ".sqlite")
  return URL(fileURLWithPath: sqliteFilePath)
}

private lazy var storeContainer: NSPersistentContainer = {
  let container = NSPersistentContainer(name: self.modelName)
  //指定Descriptions可以指定數(shù)據(jù)庫(kù)的存儲(chǔ)位置,但是一般不設(shè)置,由CoreData設(shè)置默認(rèn)值,
  //默認(rèn)設(shè)置為支持?jǐn)?shù)據(jù)庫(kù)遷移,支持自動(dòng)推斷映射模型,默認(rèn)SQLite存儲(chǔ),其默認(rèn)的URL可以通過(guò)
  //Container的persistentStore屬性中的URL方法拿到。
  container.persistentStoreDescriptions = [self.storeDescription]
  container.loadPersistentStores { (storeDescription, error) in
    if let error = error as NSError? {
      print("Unersolved error \(error), \(error.userInfo)")
    }
  }
  return container;
}()

lazy var managedContext: NSManagedObjectContext = {
  return self.storeContainer.viewContext
}()

lazy var storeDescription: NSPersistentStoreDescription = {
  let description = NSPersistentStoreDescription(url: self.storeURL)
  description.shouldInferMappingModelAutomatically = true
  description.shouldMigrateStoreAutomatically = true
  //description.type = NSInMemoryStoreType
  return description
}()
MagicRecord中
//默認(rèn)為支持?jǐn)?shù)據(jù)遷移支持自動(dòng)推斷映射模型,其數(shù)據(jù)存儲(chǔ)URL可以通過(guò)MagicRecord類(lèi)方法獲得
[MagicalRecord setupCoreDataStackWithAutoMigratingSqliteStoreNamed:@"Database.sqlite"];

3.2 新建模型文件

在工程中新建一個(gè)CoreData分類(lèi)下的DataModel文件,現(xiàn)在只考慮單個(gè)版本的模型文件,多版本管理和數(shù)據(jù)遷移在后續(xù)文章中介紹。首先看到的是三個(gè)部分,左側(cè)列出了所有的實(shí)體,中間列出了某個(gè)實(shí)體的所有屬性以及和其他實(shí)體之間的關(guān)系。右側(cè)為通過(guò)工具面板,當(dāng)選中左側(cè)的某個(gè)實(shí)體或者中間的某個(gè)實(shí)體的某個(gè)屬性來(lái)進(jìn)行更詳盡的編輯。

3.2.1 新建實(shí)體

實(shí)體可以理解為對(duì)某個(gè)對(duì)象的描述,它在SQLite數(shù)據(jù)庫(kù)中具體體現(xiàn)為一張表。選中實(shí)體時(shí)右側(cè)通用工具欄中最后一個(gè)Data Model Inspector可以進(jìn)行給更多細(xì)節(jié)的編輯。

Entity描述中,這里的Abstract Entity表示抽象實(shí)體,意味著不會(huì)創(chuàng)建具體的實(shí)體,通常一個(gè)抽象實(shí)體是多個(gè)具體實(shí)體的父實(shí)體,如抽象實(shí)體Attachment可以對(duì)應(yīng)幾個(gè)具體的子實(shí)體ImageAttachment和VideoAttachment。

Class描述中,通常Codegen選中Manual/None,表示由開(kāi)發(fā)者手動(dòng)建立實(shí)體類(lèi)的OC文件,否則由CoreData自動(dòng)生成。手動(dòng)建立具體通過(guò)選中某個(gè)模型文件,在XCode菜單的Editor中選則創(chuàng)建Create NSManagedObject Subclass...。此時(shí)工程中會(huì)為某個(gè)實(shí)例生成兩個(gè)文件,分別為【實(shí)例名+CoreDataProperties.swift】和【實(shí)例名+CoreDataClass.swift】,如果需要生成OC文件需在選中模型文件后在右側(cè)的通用工具欄的第一項(xiàng)菜單下將Code Generation改為Objective-C。兩個(gè)文件作用在后續(xù)創(chuàng)建NSManagedObject類(lèi)中介紹。此時(shí)Class區(qū)域內(nèi)Name會(huì)被自動(dòng)填充為實(shí)體名,Swift中涉及到命名空間問(wèn)題,需要將其Module設(shè)置為當(dāng)前Module。

UserInfo描述,它可以自定用用戶(hù)信息,擴(kuò)展CoreData功能,在MagicRecord中通過(guò)relatedByAttribute設(shè)置唯一主鍵。

Versioning描述,這里應(yīng)該是關(guān)于實(shí)體版本控制的,但在數(shù)據(jù)遷移中主要判斷的是模型的版本而不是實(shí)體的版本,暫時(shí)未用到,具體用法還行參考官網(wǎng)描述。

3.2.2 新建屬性

Attribute Type
新建屬性時(shí),CoreData支持的類(lèi)型和在代碼中映射的類(lèi)型對(duì)應(yīng)關(guān)系為,【Integer16 - NSNumber】、【Integer32 - NSNumber】、【Integer64 - NSNumber】、【Decimal - NSDecimalNumber】、【Double - NSNumber】、【Float - NSNumber】、【String - String】、【Boolean - Bool】、【Date - NSDate】、【Binary Data - NSData】、【Transformable - NSObject】。

Interger:在指定整形的Attribute type時(shí)需根據(jù)樣本的實(shí)際情況選擇合適類(lèi)型。

  • Integer 16 有符號(hào)的占2字節(jié)整數(shù) -32768~32767
  • Integer 32 有符號(hào)的占4字節(jié)整數(shù) -2147483648 ~ 2147483647
  • Integer 64 有符號(hào)的占8字節(jié)整數(shù) ...

Decimal:它表示一種科學(xué)計(jì)數(shù)法,具體為10^exponent,exponent is an integer from –128 through 127。

NSDecimalNumber *number = [NSDecimalNumber decimalNumberWithMantissa:1234 exponent:-2 isNegative:NO];   //12.34
number = [NSDecimalNumber decimalNumberWithMantissa:1234 exponent:2 isNegative:YES];   //-123400

Binary Data:其映射類(lèi)型為NSData用于存儲(chǔ)類(lèi)似于圖片、PDF文件或者其他任何能被編碼為二進(jìn)制的資源。這里不需要擔(dān)心從內(nèi)存中加載大量二進(jìn)制數(shù)據(jù)問(wèn)題,因?yàn)镃ore Data已經(jīng)對(duì)這個(gè)問(wèn)題作出了優(yōu)化。在一個(gè)實(shí)體中,該類(lèi)型的屬性右方的Attribute設(shè)置面板中勾選Allows External Storage選項(xiàng),這時(shí)Core Data會(huì)自動(dòng)對(duì)每一個(gè)實(shí)體對(duì)象檢查判斷是否將改資源作為二進(jìn)制數(shù)據(jù)存儲(chǔ)到數(shù)據(jù)庫(kù)中,或者單獨(dú)將其存儲(chǔ)在主存中并為其創(chuàng)建一個(gè)指向該資源的通用標(biāo)識(shí)符URI,并在數(shù)據(jù)庫(kù)中存儲(chǔ)URI。至于什么時(shí)候會(huì)存儲(chǔ)URI,在官方文檔中暫時(shí)未找到合理解釋?zhuān)谡搲杏幸?jiàn)提到是根據(jù)要存儲(chǔ)的數(shù)據(jù)大小來(lái)決定的,但是在很多案例中,這依然是個(gè)可行而高效的方法。在存儲(chǔ)時(shí)需手動(dòng)轉(zhuǎn)換為NSData進(jìn)行存儲(chǔ),讀取時(shí)也應(yīng)手動(dòng)從NSData轉(zhuǎn)換為相應(yīng)的變量類(lèi)型。

這里需要注意的是,大多數(shù)開(kāi)發(fā)者都指出了當(dāng)使用Allows External Storage這種方式儲(chǔ)存資源在做數(shù)據(jù)庫(kù)遷移時(shí)有一個(gè)隱含bug,誘因是數(shù)據(jù)庫(kù)遷移后新的統(tǒng)一存儲(chǔ)該資源文件的文件夾會(huì)被刪除并重新創(chuàng)建,從而導(dǎo)致數(shù)據(jù)丟失,以下是一種解決方案。

- (NSPersistentStoreCoordinator*)persistentStoreCoordinator {
  if (_persistentStoreCoordinator !=nil) {
    return _persistentStoreCoordinator;
  }
  NSURL*storeURL =[[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"CoreDataBinaryBug.sqlite"];
  NSError*error =nil;
  NSDictionary*sourceMetadata =[NSPersistentStoreCoordinator metadataForPersistentStoreOfType:NSSQLiteStoreType
  URL:storeURL
  error:&error];
  //Check if the new model is compatible with any previously stored model
  BOOL isCompatibile = [self.managedObjectModel isConfiguration:nil compatibleWithStoreMetadata:sourceMetadata];
  BOOL needsMigration =!isCompatibile;
  NSFileManager*fileManager =[NSFileManager defaultManager];
  //Prepare a temporary path to move CoreData's external data storage folder to if automatic model migration is required
  NSString*documentsPath =[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
  NSString*tmpPathToExternalStorage =[documentsPath stringByAppendingPathComponent:@"tmpPathToReplacementData"];
  NSString*pathToExternalStorage =[documentsPath stringByAppendingPathComponent:@".CoreDataBinaryBug_SUPPORT/_EXTERNAL_DATA"];
  if (needsMigration) {
    if ([fileManager fileExistsAtPath:pathToExternalStorage]) {
    //Move Apple's CoreData external storage folder before it's nuked by the migration bug
    [fileManager moveItemAtPath:pathToExternalStorage toPath:tmpPathToExternalStorage error:nil];
  }
  }
  NSDictionary*options = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithBool:YES],NSMigratePersistentStoresAutomaticallyOption,[NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption, nil];
  _persistentStoreCoordinator =[[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
  if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:options error:&error]) {
     
    NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
    abort();
  } else {
    if (needsMigration) {
    //Apple's automatic migration is now complete. Replace the default external storage folder with the version pre upgrade
    [[NSFileManager defaultManager] removeItemAtPath:pathToExternalStorage error:nil];
    [[NSFileManager defaultManager] moveItemAtPath:tmpPathToExternalStorage toPath:pathToExternalStorage error:nil];
    }
  }
  return _persistentStoreCoordinator;
}

Transformable:只要遵守NSCoding協(xié)議的對(duì)象都能被以Transformable類(lèi)型的方式存儲(chǔ),默認(rèn)的映射類(lèi)型為NSObject,可以直接將從數(shù)據(jù)庫(kù)中獲得的對(duì)象進(jìn)行強(qiáng)制類(lèi)型轉(zhuǎn)變?yōu)楫?dāng)前類(lèi)。同時(shí)這類(lèi)型實(shí)體還允許在Data Model Inspector中通過(guò)Value Transfo...關(guān)聯(lián)一個(gè)類(lèi)并實(shí)現(xiàn)以下操作輔助將該對(duì)象轉(zhuǎn)化為另外一個(gè)符合NSCoding協(xié)議對(duì)象來(lái)存儲(chǔ)。當(dāng)再模型文件中直接引用工程中的文件時(shí),必須指定Module,通常是項(xiàng)目名稱(chēng)。NSCoding用法

class ImageTransformer: ValueTransformer {
  override class func transformedValueClass() -> AnyClass {
    return NSData.self
  }

  override class func allowsReverseTransformation() -> Bool {
    return true
  }
  
  override func reverseTransformedValue(_ value: Any?) -> Any? {
    guard let data = value as? Data else { return nil }
    return UIImage(data: data)
  }

  override func transformedValue(_ value: Any?) -> Any? {
    guard let image = value as? UIImage else { return nil }
    return UIImagePNGRepresentation(image)
  }
}

小結(jié):在選擇屬性類(lèi)型時(shí),通常字符串、數(shù)字、Bool類(lèi)型數(shù)據(jù)都有直接與之對(duì)應(yīng)的類(lèi)型,但是對(duì)于UIImage,UIcolor以及自定義類(lèi)等數(shù)據(jù)類(lèi)型并沒(méi)有直接與之匹配的屬性類(lèi)型,此時(shí)可以將某個(gè)類(lèi)型分開(kāi)存儲(chǔ),使用時(shí)再將其合成,如UIColor可以分解為RGBA四個(gè)部分整數(shù)分開(kāi)存儲(chǔ),但更為有效的是在Binary Data和Transformable類(lèi)型中選擇合適的類(lèi)型。

Attribute通用工具欄設(shè)置
選中一個(gè)屬性時(shí),右側(cè)也會(huì)出現(xiàn)三個(gè)可選界面,分別是File Inspector、Quick Help Inspector和Data Model Inspector,在Data Model Inspector中也可以對(duì)屬性進(jìn)行高級(jí)設(shè)置,不同類(lèi)型屬性的高級(jí)設(shè)置面板有部分變動(dòng),以下以Integer 32類(lèi)型為例。

Attribute描述:其中Properties中Optional表示該屬性是否為必有屬性,對(duì)應(yīng)Swift中的必選屬性,當(dāng)指定為必選屬性時(shí)需為其指定默認(rèn)值,兩位兩個(gè)屬性暫未用過(guò)。Validation表示對(duì)數(shù)據(jù)的校驗(yàn),這里可以設(shè)置數(shù)據(jù)校驗(yàn)規(guī)則,它負(fù)責(zé)對(duì)數(shù)據(jù)進(jìn)行校驗(yàn),不在此范圍內(nèi)的數(shù)據(jù)不會(huì)被存儲(chǔ)到Core Data中,這個(gè)錯(cuò)誤將在調(diào)用Context的Save方法時(shí)候拋出。如果后期版本迭代時(shí)此處發(fā)生改變,需要進(jìn)行輕量級(jí)數(shù)據(jù)遷移。同時(shí)這里可以設(shè)置最大最小和默認(rèn)值。Advanced中兩個(gè)勾選框暫未用過(guò)。

User Info描述:這里添加的字點(diǎn)再代碼中都可以拿到,用于擴(kuò)展CoreData的功能,在MagicRecord中,可以添加mappedKeyName-Value來(lái)將后來(lái)返回的Value字段轉(zhuǎn)換為Attribute本身。

Versioning描述,同樣這里應(yīng)該是關(guān)于屬性版本控制的,但在數(shù)據(jù)遷移中主要判斷的是模型的版本而不是屬性的版本,暫時(shí)未用到,具體用法還行參考官網(wǎng)描述。

3.2.3 新建關(guān)系

在CoreData中,表之間的聯(lián)系需要設(shè)置為關(guān)系,比如一個(gè)公司實(shí)體Company擁有很多雇員實(shí)體Employee。這里Company和Employee在數(shù)據(jù)庫(kù)中分別為兩張表,而實(shí)現(xiàn)上述需求需要在Company實(shí)體中添加一個(gè)Destination為Employee的employees關(guān)系。

在CoreData中關(guān)系分為三類(lèi),一對(duì)一,一對(duì)多和多對(duì)多。需要特別注意的是無(wú)論哪種類(lèi)型的關(guān)系,關(guān)系都是成對(duì)出現(xiàn)的,并且必須設(shè)置Inverse。并且關(guān)系可以指向?qū)嶓w自身。

一對(duì)一的關(guān)系只需要在實(shí)體A中添加指向?qū)嶓wB的to-one類(lèi)型關(guān)系,并且在食堂B中同樣添加指向A的to-one類(lèi)型關(guān)系,同時(shí)將兩個(gè)關(guān)系互相設(shè)置為inverse。一對(duì)多的關(guān)系需要在實(shí)體A中添加指向?qū)嶓wB的to-many類(lèi)型關(guān)系,并且在食堂B中同樣添加指向A的to-one類(lèi)型關(guān)系,同時(shí)將兩個(gè)關(guān)系互相設(shè)置為inverse。多對(duì)多的關(guān)系需要在實(shí)體A中添加指向?qū)嶓wB的to-many類(lèi)型關(guān)系,并且在食堂B中同樣添加指向A的to-many類(lèi)型關(guān)系,同時(shí)將兩個(gè)關(guān)系互相設(shè)置為inverse。

關(guān)系高級(jí)設(shè)置選項(xiàng)中,properties通常保留默認(rèn)值為optional,type根據(jù)需要選擇,Delete Rule通常選擇為Nullify,其余選項(xiàng)下面介紹。Count可以設(shè)置最大最小的數(shù)量,Advanced暫未用過(guò),保留默認(rèn)值即可。User InfoVersioning描述同前文類(lèi)似。Arrangement表示是否排序,當(dāng)選擇to-Many類(lèi)型關(guān)系時(shí),關(guān)系中的元素將以集合Set形式組織數(shù)據(jù)而非數(shù)組。勾選ordered會(huì)使用有序集合NSInorderedSet,此時(shí)生成的【實(shí)例名+CoreDataProperties.swift】文件中CoreData會(huì)自動(dòng)生成集合的操作方法。在MagicRecord中可以向UserInfo中添加relatedByAttribute字段指定排序?qū)傩浴?/p>

幾種關(guān)系的刪除規(guī)則Delete Rule:

  • Nullify(作廢):當(dāng)A對(duì)象的關(guān)系指向的B對(duì)象被刪除后,A對(duì)象的關(guān)系將被設(shè)為nil。對(duì)于To Many關(guān)系類(lèi)型,B對(duì)象只會(huì)從A對(duì)象的關(guān)系的容器中被移除。
  • Cascade(級(jí)聯(lián)):當(dāng)B對(duì)象的關(guān)系指向的C對(duì)象被刪除后,B對(duì)象也會(huì)被刪除。B對(duì)象關(guān)聯(lián)(以Cascade刪除規(guī)則)的二級(jí)對(duì)象A也會(huì)被刪除。以此類(lèi)推。
  • Deny(拒絕):如果刪除A對(duì)象時(shí),A對(duì)象的關(guān)系指向的B對(duì)象仍存在,則刪除操作會(huì)被拒絕。
  • NO Action:當(dāng)A對(duì)象的關(guān)系指向的B對(duì)象被刪除后,A對(duì)象保持不變,這意味著A對(duì)象的關(guān)系會(huì)指向一個(gè)不存在的對(duì)象。如果沒(méi)有充分的理由,最好不要使用。

3.3 創(chuàng)建NSManagedObject實(shí)體類(lèi)

選中某個(gè)模型文件NSManagedObjectModle后,在XCode的菜單欄中選擇Editor可以為模型創(chuàng)建NSManagedObject實(shí)體類(lèi),當(dāng)創(chuàng)建一個(gè)實(shí)例的類(lèi)時(shí),系統(tǒng)自行生成兩個(gè)不同文件【實(shí)例名 +CoreDataProperties.swift 】和【實(shí)例名+CoreDataClass.swift】。其中第一個(gè)只包含所有屬性,第二個(gè)包含所有操作。這樣設(shè)計(jì)的目的是,當(dāng)后期為實(shí)例增加屬性時(shí)再?gòu)腅ditor選項(xiàng)中創(chuàng)建對(duì)應(yīng)類(lèi)時(shí)只會(huì)重新生成CoreDataProperties.swift文件,避免對(duì)CoreDataClass.swift文件的修改。

4 操作數(shù)據(jù)庫(kù)

4.1 簡(jiǎn)單的操作

插入數(shù)據(jù)

let walk = Walk(context: managedContex)
walk.date = NSDate()
currentDog?.addToWalks(walk)

do {
  try managedContex.save()
} catch let error as NSError {
  print("Save error: \(error), description: \(error.userInfo)")
}

刪除數(shù)據(jù),只有執(zhí)行save后才會(huì)被真正刪除,注意刪除數(shù)據(jù)必須是一件謹(jǐn)慎的事情,在iOS9之前必須將程序中所有對(duì)被刪除數(shù)據(jù)的引用置為nill,否則會(huì)引發(fā)CoreData異常導(dǎo)致程序崩潰,幸運(yùn)的是在iOS9過(guò)后,NSMnagedObjectContex對(duì)象有一個(gè)默認(rèn)為T(mén)rue的屬性shouldDeleteInaccessibleFaults,當(dāng)其為T(mén)rue時(shí),對(duì)被刪除的數(shù)據(jù)操作將返回nil。

guard let walkToRemove = currentDog?.walks?[indexPath.row] as? Walk, editingStyle == .delete else {
  return
}

managedContex.delete(walkToRemove)

do {
  try managedContex.save()
} catch let error as NSError {
  print("Saving error: \(error), description: \(error.userInfo)")
}

查詢(xún)數(shù)據(jù)

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    
    guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
        return;
    }
    
    let managedContext = appDelegate.persistentContainer.viewContext
    let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "Person")
    
    do {
        people = try managedContext.fetch(fetchRequest)
    } catch let error as NSError {
        print("Could not fetch. \(error), \(error.userInfo)")
    }
}

錯(cuò)誤處理
對(duì)于一個(gè)NSError的正確處理應(yīng)該是檢查錯(cuò)誤的Domain和Error code,標(biāo)準(zhǔn)的處理流程見(jiàn)官網(wǎng)。

4.2 查詢(xún)操作

4.2.1 NSFetchRequest基本使用

創(chuàng)建一個(gè)FetchRequest的方式有5種

let fetchRequest1 = NSFetchRequest<Dog>()
let entity = NSEntityDescription.entity(forEntityName: "Dog", in: managedContex)
fetchRequest1.entity = entity

let fetchRequest2 = NSFetchRequest<Dog>(entityName: "Dog")

let fetchRequest3: NSFetchRequest<Dog> = Dog.fetchRequest()

let fetchRequest4 = managedObjectModel.fetchRequestTemplate(forName: "venueFR")

let fetchRequest5 = managedObjectModel.fetchRequestTemplate(forName: "venueFR", substitutionVariables: ["NAME" : "Vivi bubble Tea"])

其中4、5方法都需要在.xcdatamodeld文件中的添加實(shí)體按鈕下拉選項(xiàng)中選擇添加可視化的fetchRequest。并通過(guò)Coordinator的managedObjectModel生成fetchRequest對(duì)象。注意其中的name參數(shù)必須嚴(yán)格與CoreData Editor中的fetchRequest名字一致,否則程序?qū)?huì)崩潰。
當(dāng)程序中對(duì)于某個(gè)對(duì)象存在大量復(fù)雜查找時(shí),并不注重排序時(shí)可以通過(guò)以上兩種方法,其優(yōu)點(diǎn)是少些代碼,缺點(diǎn)是不能排序。

guard let model = coreDataStack.managedContext.persistentStoreCoordinator?.managedObjectModel, 
      let fetchRequest = model.fetchRequestTemplate(forName: "FetchRequest") as? NSFetchRequest<Venue> else {
  return
}

NSFetchRequest的resultType有四個(gè)值,其中.managedObjectResultType為默認(rèn)值,返回的是滿(mǎn)足條件的對(duì)象,.countResultType返回的是滿(mǎn)足條件對(duì)象的個(gè)數(shù),.dictionaryResultType返回了一個(gè)字典,其中包含平均值、最大最小值等統(tǒng)計(jì)信息,.managedObjectIDResultType返回滿(mǎn)足條件的唯一標(biāo)識(shí)符。仔細(xì)選擇返回類(lèi)型,在某些時(shí)候會(huì)極大提升程序運(yùn)行效率。需要注意的是,當(dāng)設(shè)置了某個(gè)具體resultType時(shí),NSFetchRequest的范形需要與之對(duì)應(yīng)。.managedObjectIDResultType返回的是一個(gè)NSManagedObjectID對(duì)象數(shù)組,因?yàn)檫@個(gè)屬性是線(xiàn)程安全的,在iOS5以前經(jīng)常使用,但是在之后很少使用,因?yàn)镃oreData提供了更好的處理方式。

當(dāng)查找符合某個(gè)條件的對(duì)象數(shù)量時(shí),方法1示例如下:

let fetchRequest = NSFetchRequest<NSNumber>(entityName: "Venue")
fetchRequest.resultType = .countResultType

另外也可以不設(shè)置請(qǐng)求類(lèi)型,直接調(diào)用context的方法,方法2如下:

let count = try coreDataStack.managedContext.count(for: fetchRequest)

dictionaryResultType:查找一個(gè)類(lèi)所有數(shù)據(jù)某個(gè)屬性的統(tǒng)計(jì)結(jié)果用法很多,關(guān)于統(tǒng)計(jì)的可選函數(shù)列表見(jiàn)NSExpression文檔,下面只展示兩個(gè)實(shí)例。

案例一:求和

let fetchRequest = NSFetchRequest<NSDictionary>(entityName: "Venue")
fetchRequest.resultType = .dictionaryResultType

let sumExpressionDesc = NSExpressionDescription()
sumExpressionDesc.name = "sumDeals"

let specialCountExp = NSExpression(forKeyPath: #keyPath(Venue.specialCount))
sumExpressionDesc.expression = NSExpression(forFunction: "sum:", arguments: [specialCountExp])
sumExpressionDesc.expressionResultType = .integer32AttributeType

fetchRequest.propertiesToFetch = [sumExpressionDesc]

do {
  let results = try coreDataStack.managedContext.fetch(fetchRequest)
  let resultDict = results.first!
  let numDeals = resultDict["sumDeals"]!
  numDealsLabel.text = "\(numDeals) total deals"
} catch let error as NSError {
  print("Count not fetch \(error), \(error.userInfo)")
}

案例二:計(jì)數(shù)

func totalEmployeesPerDepartmentFast() -> [[String: String]] {
  //1 創(chuàng)建NSExpressionDescription命名為“headCount”
  let expressionDescreption = NSExpressionDescription()
  expressionDescreption.name = "headCount"
  
  //2 創(chuàng)建函數(shù)統(tǒng)計(jì)每個(gè)"department"的成員數(shù)量,更多的函數(shù)關(guān)鍵字如average,sum,count,min等見(jiàn)NSExpression文檔
  expressionDescreption.expression =
    NSExpression(forFunction: "count:",
                 arguments: [NSExpression(forKeyPath: "department")])
  
  //3 通過(guò)設(shè)置propertiesToFetch初始化fetch的內(nèi)容,這樣CoreData就不會(huì)查尋每條記錄的所有數(shù)據(jù),這里只查詢(xún)"department"屬性,并通過(guò)expressionDescreption函數(shù)記錄不同"department"的數(shù)量。
  let fetchRequest: NSFetchRequest<NSDictionary> = NSFetchRequest(entityName: "Employee")
  // 這兩個(gè)參數(shù)都是必須的,第一個(gè)"department"只會(huì)關(guān)注對(duì)應(yīng)的屬性并不會(huì)關(guān)注統(tǒng)計(jì),其對(duì)應(yīng)結(jié)果是【"department":name】的字典,第二個(gè)參數(shù)expressionDescreption只關(guān)注統(tǒng)計(jì)結(jié)果并不關(guān)注具體是哪一個(gè)department,其結(jié)果是【"headCount":value】的字典
  fetchRequest.propertiesToFetch = ["department", expressionDescreption]
  //查詢(xún)結(jié)果以"department"分組,這樣將返回一個(gè)數(shù)組
  fetchRequest.propertiesToGroupBy = ["department"]
  fetchRequest.resultType = .dictionaryResultType
  
  //4 執(zhí)行查詢(xún)操作
  var fetchResults: [NSDictionary] = []
  do {
    fetchResults = try coreDataStack.mainContext.fetch(fetchRequest)
  } catch let error as NSError {
    print("ERROR: \(error.localizedDescription)")
    return [[String: String]]()
  }
  //5 查詢(xún)的結(jié)果是一個(gè)[NSDictionary],其中元素個(gè)數(shù)取決于fetchRequest.propertiesToGroupBy的分組個(gè)數(shù),每個(gè)字典的元素個(gè)數(shù)取決于fetchRequest.propertiesToFetch中的個(gè)數(shù)。在上述兩個(gè)屬性都未設(shè)置時(shí),其結(jié)果為[NSManagedObject]。
  return fetchResults as! [[String: String]]
}
4.2.2 NSPredicate限制NSFetchRequest

在數(shù)據(jù)庫(kù)中抓取數(shù)據(jù)的時(shí)候,CoreData會(huì)順著每一個(gè)實(shí)體的relationships去查詢(xún)相關(guān)實(shí)體,當(dāng)這種關(guān)系非常復(fù)雜,或者查詢(xún)的實(shí)體自身數(shù)量龐大的時(shí)候,這會(huì)十分消耗性能。幸運(yùn)的是,CoreData可以通過(guò)以下三種方式來(lái)優(yōu)化效率,1)CoreData支持分批查找,可以設(shè)置NSFetchRequest的fetchBatchSize、fetchLimit和fetchOffset屬性進(jìn)行控制。2)CoreData使用faulting來(lái)優(yōu)化內(nèi)存效率,一個(gè)fault是一個(gè)占位對(duì)象,表示還沒(méi)有完全加載入內(nèi)存的一個(gè)類(lèi)型。3)使用NSPredicate限制查詢(xún)范圍。

NSPredicate條件可以通過(guò)AND,OR,NOT等各種條件限制,這個(gè)類(lèi)是Foundation的內(nèi)容,具體使用可以查詢(xún)官網(wǎng)。NSDescriptor也是屬于Foundation的內(nèi)容。根據(jù)文檔中介紹,這兩個(gè)屬性是在SQLite level這一層生效。NSDescriptor有很多API可以得到一個(gè)comparator,NSPredicate也有很多實(shí)例化方法,但是CoreData并不會(huì)全部支持,因?yàn)樵撜Z(yǔ)法在SQLite生效,部分高效的方法無(wú)法轉(zhuǎn)化為SQLite的語(yǔ)法。

lazy var nameSortDescriptor: NSSortDescriptor = {
  let compareSelector = #selector(NSString.localizedStandardCompare(_:))
  return NSSortDescriptor(key: #keyPath(Venue.name), ascending: true, selector: compareSelector)
}()

lazy var dsitanceSortDescriptor: NSSortDescriptor = {
  return NSSortDescriptor(key: #keyPath(Venue.location.distance), ascending: true)
}()
4.2.3 異步抓取

和NSFetchRequest是NSPersistentStoreRequest的子類(lèi)一樣,NSAsynchronousFetchRequest也是NSPersistentStoreRequest的子類(lèi),它可以在子線(xiàn)程對(duì)大量數(shù)據(jù)抓取。它需要一個(gè)NSFetchResult的對(duì)象進(jìn)行初始化,包含一個(gè)完成回調(diào),在managedContext中調(diào)用execute執(zhí)行查詢(xún)操作。另外異步抓取請(qǐng)求可以通過(guò)NSAsynchronousFetchRequest對(duì)象的cancel()方法撤銷(xiāo)。異步抓取也可以使用Context執(zhí)行perform方法來(lái)實(shí)現(xiàn)。兩種方案任選其一即可,暫未發(fā)現(xiàn)它們之間的本質(zhì)區(qū)別。

fetchRequest = Venue.fetchRequest()

asyncFetchRequest = NSAsynchronousFetchRequest<Venue>(fetchRequest: fetchRequest, completionBlock: { [unowned self] (result: NSAsynchronousFetchResult) in
  guard let venues = result.finalResult else {
    return
  }
  self.venues = venues
  self.tableView.reloadData()
})

do {
  try coreDataStack.managedContext.execute(asyncFetchRequest)
} catch let error as NSError {
  print("Could not fetch \(error), \(error.userInfo)")
}

4.3 批量操作

4.3.1 批量更新

有時(shí)可能需要批量改變數(shù)據(jù)庫(kù)中某一個(gè)實(shí)體所有對(duì)象的單個(gè)屬性,首先傳統(tǒng)的將所有符合條件的對(duì)象從數(shù)據(jù)庫(kù)中加載到內(nèi)存中能夠?qū)崿F(xiàn)。但是當(dāng)需要處理成千上萬(wàn)條記錄的時(shí)候,這樣將會(huì)極大的浪費(fèi)內(nèi)存,降低效率。在iOS8以后,CoreData提供了更有效的操作,NSBatchUpdateRequest可以繞過(guò)加載到內(nèi)存的操作,直接對(duì)數(shù)據(jù)庫(kù)中的數(shù)據(jù)進(jìn)行批量更新。如郵件app中標(biāo)記所有為已讀。

let batchUpdate = NSBatchUpdateRequest(entityName: "Venue")
batchUpdate.propertiesToUpdate = [#keyPath(Venue.favorite) : true]
batchUpdate.affectedStores = coreDataStack.managedContext.persistentStoreCoordinator?.persistentStores
batchUpdate.resultType = .updatedObjectsCountResultType

do {
  let batchResult = try coreDataStack.managedContext.execute(batchUpdate) as! NSBatchUpdateResult
  print("Records updated \(batchResult.result!)")
} catch let error as NSError {
  print("Could not update \(error), \(error.userInfo)")
}
4.3.1 批量刪除

同樣的NSBatchDeleteRequest也是NSPersistentStoreRequest的一個(gè)子類(lèi),它和NSBatchUpdateRequest一樣直接對(duì)數(shù)據(jù)庫(kù)進(jìn)行操作,注意這兩個(gè)類(lèi)的操作將不會(huì)把對(duì)象和Context進(jìn)行關(guān)聯(lián),因此也不會(huì)對(duì)數(shù)據(jù)進(jìn)行校驗(yàn),因此當(dāng)執(zhí)行這兩個(gè)操作時(shí)需要手動(dòng)進(jìn)行數(shù)據(jù)校驗(yàn)。

4.4 NSFetchedResultsController

NSFetchedResultsController是蘋(píng)果特地為支持UITableView從數(shù)據(jù)庫(kù)讀取數(shù)據(jù)設(shè)計(jì)的一個(gè)類(lèi)。NSFetchedResultsController可以通過(guò)fetchRequest、managedObjectContext、sectionNameKeyPath和cacheName四個(gè)參數(shù)實(shí)例化一個(gè)對(duì)象。

其中第三個(gè)參數(shù)sectionNameKeyPath為分組的屬性字段,注意它不僅可以取一級(jí)屬性,還可以取多級(jí)屬性,如Team.qualifyZone.lon...。但是需要注意的是真正想讓數(shù)據(jù)分組展示必須使用相應(yīng)的NSSortDescriptor賦值給NSFetchedResultsController進(jìn)行數(shù)據(jù)排序。只要fetchRequest的NSSortDescriptor的sortDescriptors屬性數(shù)組中的首個(gè)元素和NSFetchedResultsController初始化時(shí)的分組使用同一鍵值,那么這里的升序或者降序不會(huì)對(duì)分組結(jié)果造成影響,只會(huì)影響排序。

第四個(gè)參數(shù)cacheName用于緩存分組相關(guān)信息,注意盡管這里并不是在數(shù)據(jù)庫(kù)中分類(lèi)存儲(chǔ),但是當(dāng)重啟程序后分組信息依然有效,這是因?yàn)槠渚唧w緩存位置在主存Disk中。另外,當(dāng)某個(gè)Request的條件改變時(shí)或者查詢(xún)另外一個(gè)實(shí)體時(shí),如果使用了同一個(gè)name進(jìn)行緩存,需要調(diào)用deleteCatch(withName:)或者使用另外一個(gè)不同的name,因此name盡量取得更有意義。

override func viewDidLoad() {
  super.viewDidLoad()

  let fetchRequest: NSFetchRequest<Team> = Team.fetchRequest()

  let zoneSort = NSSortDescriptor(key: #keyPath(Team.qualifyingZone), ascending: true)
  let scoreSort = NSSortDescriptor(key: #keyPath(Team.wins), ascending: false)
  let nameSort = NSSortDescriptor(key: #keyPath(Team.teamName), ascending: true)

  fetchRequest.sortDescriptors = [zoneSort, scoreSort, nameSort]

  fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest,
                                                        managedObjectContext: coreDataStack.managedContext,
                                                        sectionNameKeyPath: #keyPath(Team.qualifyingZone),
                                                        cacheName: "worldCup")

  fetchedResultsController.delegate = self

  do {
    try fetchedResultsController.performFetch()
  } catch let error as NSError {
    print("Fetching error: \(error), \(error.userInfo)")
  }
}

獲取抓取到的數(shù)據(jù)

func numberOfSections(in tableView: UITableView) -> Int {
  guard let sections = fetchedResultsController.sections else {
    return 0
  }
  return sections.count
}

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  guard let sectionInfo = fetchedResultsController.sections?[section] else {
    return 0
  }
  return sectionInfo.numberOfObjects
}

let team = fetchedResultsController.object(at: indexPath)

監(jiān)聽(tīng)數(shù)據(jù)改變
當(dāng)某個(gè)fetchedResultsController的context對(duì)數(shù)據(jù)進(jìn)行改變時(shí),這個(gè)信息會(huì)發(fā)到它的delegate中。這里需要注意的是,當(dāng)你點(diǎn)擊了tableview的某一行,可能導(dǎo)致數(shù)據(jù)更新Update同時(shí)還導(dǎo)致了數(shù)據(jù)排序Move,CoreData會(huì)將這兩個(gè)操作合并為一個(gè)操作,并只調(diào)用一次didChange 方法并且NSFetchedResultsChangeType = Move。

extension ViewController: NSFetchedResultsControllerDelegate {
  func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
    tableView.endUpdates()
  }
  
  func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
    switch type {
    case .insert:
      tableView.insertRows(at: [newIndexPath!], with: .automatic)
    case .delete:
      tableView.deleteRows(at: [indexPath!], with: .automatic)
    case .update:
      let cell = tableView.cellForRow(at: indexPath!) as! TeamCell
      configure(cell: cell, for: indexPath!)
    case .move:
      tableView.deleteRows(at: [indexPath!], with: .automatic)
      tableView.insertRows(at: [newIndexPath!], with: .automatic)
    }
  }
  
  func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
    tableView.beginUpdates()
  }

  func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType) {
    let indexSet = IndexSet(integer: sectionIndex)
    switch type {
    case .insert:
      tableView.insertSections(indexSet, with: .automatic)
    case .delete:
      tableView.deleteSections(indexSet, with: .automatic)
    default:
      break
    }
  }
}

需要注意的是每次內(nèi)容改變第二和第四個(gè)方法只會(huì)調(diào)用一個(gè),當(dāng)不會(huì)新增或者刪除分區(qū)Section時(shí),CoreData調(diào)用第一、第二和第三個(gè)方法,當(dāng)發(fā)生新增或者刪除分區(qū)Section時(shí),CoreData調(diào)用第一、第三和第四個(gè)方法。當(dāng)某個(gè)分區(qū)只有一個(gè)對(duì)象時(shí),這個(gè)對(duì)象被刪除后就會(huì)調(diào)用類(lèi)型為.delete的方法四?;蛘咛砑恿艘粋€(gè)包含新的分區(qū)的對(duì)象,就會(huì)調(diào)用類(lèi)型為.insert的方法四。

同樣的,NSFetchedResultsController同樣對(duì)UICollectionview有很好的支持,不同的是,CollectionView并沒(méi)有beginUpdate和EndUpdate方法,因此需要在NSFetchedResultsController代理中didchanged中進(jìn)行UI更新。
在使用NSFetchedResultsController的代理時(shí),應(yīng)注意,只要是它管理的實(shí)體數(shù)據(jù)庫(kù)發(fā)生一點(diǎn)改變,其代理都會(huì)被調(diào)用。

5 小結(jié)

通常并不會(huì)手動(dòng)從0開(kāi)始初始化CoreData,更常用的是使用MagicRecordRecord進(jìn)行數(shù)據(jù)庫(kù)初始化。但是其并不能處理復(fù)雜的數(shù)據(jù)遷移,因此我們需要在調(diào)用它的初始化方法之前先手動(dòng)進(jìn)行數(shù)據(jù)庫(kù)遷移。關(guān)于MagicRecord詳細(xì)用法見(jiàn)其Github主頁(yè).

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 國(guó)家電網(wǎng)公司企業(yè)標(biāo)準(zhǔn)(Q/GDW)- 面向?qū)ο蟮挠秒娦畔?shù)據(jù)交換協(xié)議 - 報(bào)批稿:20170802 前言: 排版 ...
    庭說(shuō)閱讀 12,309評(píng)論 6 13
  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,506評(píng)論 19 139
  • 當(dāng)茫茫黑夜來(lái)臨 將一切淹沒(méi)在昏暗里 看不清 朦朧 朦朧 看不清 心不再飄向遠(yuǎn)方 而是 回到最初的地方 夢(mèng)開(kāi)始的地方...
    我是凈葉不沉閱讀 340評(píng)論 2 2
  • 1 周一照例是公司開(kāi)周會(huì)的日子,會(huì)上老總嘚吧嘚吧一通培訓(xùn),那他自己的話(huà)說(shuō),這是在給員工們洗腦。作為一個(gè)幾經(jīng)波折的創(chuàng)...
    奶油溜吖溜閱讀 622評(píng)論 4 2
  • 近日,季熏遙身邊悲慘世紀(jì)連連不斷,季熏遙感覺(jué)整個(gè)人都不好了。小霉運(yùn)都是不足為矣的,可這些就像是暴風(fēng)雨前夕,真正讓人...
    渡獄閱讀 392評(píng)論 0 3

友情鏈接更多精彩內(nèi)容