10 základních zásad pro používání CoreData, aniž byste si ustřelili hlavu

Během své vývojářské kariéry na platformě Apple jsem používal CoreData v průběhu několika projektů (poprvé to bylo v systému MacOS 10).5…) a v průběhu let jsem se naučil věci tvrdým způsobem (aka: „mlátit hlavou do zdi příliš dlouho“). Tak jsem si řekl, že se s vámi o své poznatky podělím. Snad vám ušetřím pár modřin na čele.

Upozorňuji, že toto není návod, jak nastavit CoreData. Má to být článek s několika dobrými postupy, které můžete převzít pro své projekty.

Tady je stručný přehled společnosti Apple: „CoreData slouží k ukládání trvalých dat aplikace pro offline použití, k ukládání dočasných dat do mezipaměti a k přidání funkce zrušení do aplikace na jednom zařízení.“

Pro upřesnění: CoreData je technologie společnosti Apple pro lokální ukládání strukturovaných dat. Pomocí editoru modelu definujete strukturu dat podobně jako v editoru databázového schématu a pak můžete tyto objekty používat ve svém kódu k ukládání dat.

CoreData je rámec pro správu objektových grafů

To znamená, že při vytváření modelu definujete své objekty a jejich vztahy. Například pokud máte ve svém datovém modelu entitu Společnost a entitu Zaměstnanec, budou tyto dvě entity propojeny vztahem, který tyto dvě entity spojuje. Pravděpodobně se bude jmenovat employees u entity Company a employer u entity Employee. Jednoduše přidáte vlastnost vztahu na entity a je to. Nemusíte vytvářet spojovací tabulku jako v databázi.

Krása CoreData ve srovnání s tradiční databází spočívá v tom, že když se zeptáte na firmu X, budete moci získat přímo zaměstnance jednoduše přístupem k company.employees. Není třeba sestavovat spojovací dotaz ani se znovu dotazovat.

CoreData umí ukládat data v různých formátech

Ano, slyšíte správně, CoreData umí ukládat data v různých formátech/místech. Každý z nich má své výhody a nevýhody.

  1. Binární úložiště
  2. SQLitové úložiště
  3. Uložiště v paměti

Můžete si přečíst tento článek, kde najdete výhody a nevýhody každého z nich. Já vždy používám SQLite, ale dokázal bych si představit, že úložiště v paměti by bylo superrychlé.

Pár definic pojmů

NSManagedObject: jedná se o základní třídu reprezentující objekty, které máte uložené v CoreData.

Požadavek na načtení: jedná se o ekvivalent dotazu ve světě CoreData. Reprezentuje ho třída NSFetchedObject.

Používejte NSPersistentContainer

Do systému iOS 10.0 zahrnovalo nastavení zásobníku CoreData poměrně dost kotlíkového kódu. Museli jste si sami poradit s definováním strategie, která se má použít k řešení vícevláknového použití CoreData.

S NSPersistentContainer společnost Apple zapouzdřila většinu kódu boilerplate. Ušetří vám tak debaty o tom, jak nejlépe nastavit zásobník pro optimální výkon.

Tento článek obsahuje všechny informace, které potřebujete ke správnému použití.

Vztahy použití

Vždy se o tom zmiňuji, protože ve dvou případech u projektů, které jsem zdědil, byl model CoreData nastaven jako databáze. Nebyly definovány žádné vztahy. Místo toho bylo uloženo ID souvisejícího objektu, a proto byl pro získání souvisejícího objektu vyžadován dodatečný požadavek na načtení. Myslím, že mnoho vývojářů přistupuje k modelu CoreData jako k databázi a dopouští se této chyby. Ve výsledku je pak celý projekt mnohem složitější a pomalejší.

Podle tohoto článku můžete začít.

Vyhněte se co nejvíce řetězcovému kódu

Řetězcovým kódem se rozumí kód, který k přístupu k datům používá řetězce. V systému iOS máte k dispozici několik stringly API:

Pokud se vrátíme k příkladu s firmou, níže jsou uvedeny dvě možnosti vytvoření firmy a nastavení jejího názvu:

//stringly version to create and edit a company
let company = NSEntityDescription.insertNewObject(forEntityName: "Company", in: managedObjectContext)
company.set("my less than great corporation", forKey: "name")//non stringly version
let company = Company(in: managedObjectContext)
company.name = "My super corporation"

Na první pohled obě tyto možnosti fungují, zkompilují se, není na nich nic špatného…

Problém se stringly kódem spočívá v tom, že vám kompilátor nemůže pomoci. Představte si, že potřebujete přistupovat name k firmě na více místech kódu, a reálně budete potřebovat minimálně dvě: jedno pro nastavení a jedno pro čtení, nyní musíte ten řetězec name psát na dvou místech. Jistě, můžete vytvořit proměnnou pro uložení řetězce a použít tuto proměnnou, ale to vyžaduje další úsilí, nebude se to kontrolovat při psaní a všichni víme, že vývojáři jsou líní…

Takže obvykle zůstane tento řetězec na více místech. Pak ho chcete přejmenovat a musíte ho lovit pomocí vyhledávání a nahrazování… Pokud nějaký vynecháte, vaše aplikace spadne.

Tady je seznam funkcí překladače, které se syntaxí stringly nevyužijete:

  • auto-complete
  • refactor
  • výstraha kompilátoru v případě chybného překlepu
  • chyba kompilátoru, pokud tuto entitu nebo vlastnost ve svém modelu odstraníte/přejmenujete

Abyste mohli použít ne-kompilátor.stringly verzi kódu, budete muset jednoduše požádat překladač o vygenerování tříd pro vaše entity (můžete postupovat podle pokynů společnosti Apple v části „Select a Code Generation Option“). To je nyní výchozí nastavení při vytváření nové entity.

Udržujte NSPersitentContainer.viewContext pouze pro čtení

Z výkonnostních důvodů nechcete provádět dlouhé operace s viewContextem, protože poběží v hlavním vlákně a potenciálně zablokují uživatelské rozhraní.

Řešením je zvyk provádět úpravy svých objektů pouze uvnitř bloku, který naplánujete pomocí metody NSPersistentContainer.performBackgroundTask(). Když v tomto bloku uložíte změny, ty se sloučí zpět do bloku NSPersitentContainer.viewContext a vy můžete aktualizovat uživatelské rozhraní.

Hlídejte si tato vlákna

Jedním z hlavních taháků s CoreData je jejich použití způsobem bezpečným pro vlákna. I když jakmile pochopíte koncept, je to poměrně snadné.

Jedná se o to, že jakýkoli datový objekt, který získáte z CoreData, lze použít pouze ve vlákně, ke kterému je připojen managedobjectContextobjekt.

Jedním ze způsobů, jak zajistit, abyste pro přístup k objektu použili správné vlákno, je použití bloku NSManagedObjectContext.performBlockAndWait, který zajistí, že váš kód bude použit na správném vlákně.

Pokud se v tomto bloku nacházíte, budete muset použít NSManagedObject.objectID a NSManagedObjectContext.existingObject(withId:) pro předávání objektu napříč vlákny a kontextem.

Pokud to neuděláte, s největší pravděpodobností skončíte s deadlocky. Představte si:

  • Naplánujete úlohu z hlavního vlákna s V případě, že něco naplánujete z hlavního vlákna NSManagedObjectContext.performBlockAndWait(). V tomto okamžiku je hlavní vlákno zablokováno a čeká na dokončení tohoto bloku.
  • Uvnitř tohoto bloku předáte objekt foo z hlavního kontextu do metody bar() a odešlete jej jinému vláknu.
  • Abyste byli dobrými občany CoreData, bar() získá kontext připojený k foo a odešle svou úlohu pomocí foo.managedObjectContext.performBlockAndWait()
  • Bum, jste ve slepé uličce, protože tento kontext se vykonává pouze v hlavním vlákně a hlavní vlákno čeká na jeho dokončení.

Další konvence, která pomáhá, je předávání objektů mezi řadiči zobrazení pouze v případě, že existují v kontextu hlavního vlákna.

Pomocí argumentu launch můžete sledovat kontext a vlákno

Pokud se budete řídit výše uvedenými radami, mělo by to omezit pravděpodobnost, že bude instance NSManagedObject zavolána na nesprávném vlákně. Stále se to však může stát. Proto se při vývoji ujistěte, že jste zapnuli příznak ladění vláken

-com.apple.CoreData.ConcurrencyDebug 1

V tomto článku se dozvíte, jak přesně to udělat, a řekneme vám o dalších příznacích, které můžete nastavit pro větší ladění.

Používejte agregované dotazy

CoreData nabízí groupBy, sum a count jako agregované funkce, které lze spustit během dotazu. Jejich nastavení může být trochu složitější, ale vyplatí se to.

Alternativou je vytvořit dotaz pro získání všech objektů, které odpovídají vašim kritériím, a pak spustit smyčku for přes všechny entity a provést výpočty sami. Jednodušší na kódování, ale mnohem méně efektivní.

Když spustíte požadavek na souhrnné načtení, CoreData jej přeloží do databázového dotazu a provede matematiku přímo na úrovni databáze. Díky tomu je to superrychlé.

Tento článek je dobrým úvodem do toho, jak to udělat.

Udržujte kód související s CoreData v kategoriích

Jakmile máte v Xcode vytvořenou podtřídu NSManagedObject, nejspíš zjistíte, že máte kód, který by měl být ve třídě reprezentující váš objekt (pro výpočet hodnot, import dat ze serveru atd.). Ale požádali jste CoreData, aby za vás vytvořila třídy…

Takže budete muset vytvořit kategorii pro objekt, který potřebuje další chování. Vkládejte do ní pouze kód související s CoreData, abyste si udrželi čistotu.

Přijměte transakční model pro změny svých dat

Můžete své objekty měnit, jak chcete, ale aby tyto změny přetrvaly, musíte zavolat NSManagedObjectContext.save().

Zjistíte, že rychle zapomenete zavolat save() a budete mít v kontextu nějaké visící změny. V tuto chvíli jsou všechny sázky zrušeny, až příště zavoláte save(), nebudete si jisti, co odevzdáváte.

Přijměte transakční model, abyste za sebou nenechávali špinavý kontext. Za tímto účelem vytvořte kategorii na NSPersistentContainer takto:

extension NSPersistentContainer {
@objc func performBackgroundSaveTask(_ block: @escaping (NSManagedObjectContext) -> Void) {
performBackgroundTask { (context: NSManagedObjectContext) in
block(context)
context.save()
}
}
}

Od nynějška se ujistěte, že všechny změny, které provedete, budou provedeny v bloku naplánovaném pomocí performBackgroundSaveTask(). Tak budete mít jistotu, že změny budou uloženy v okamžiku, kdy je provedete.

Neukládejte kontext z metody kategorie modelu

Nikdy nevolejte save() z kategorie entit. Není nic horšího než zavolat metodu na objektu, aniž byste si uvědomili, že se s ní uloží celý kontext. Odevzdání některých změn, které možná nebudete chtít uložit. Volání uložení by mělo být provedeno v okamžiku, kdy jsou zahájeny úpravy objektu.