10 alapelv a CoreData használatához anélkül, hogy szétrobbanna a fejed

A CoreData-t több projekt során is használtam már az Apple platformos fejlesztői karrierem során (először a MacOS 10.5…), és az évek során a nehezebb úton tanultam meg dolgokat (aka: “túl sokáig vertem a fejem a falba”). Úgyhogy gondoltam, megosztom veletek a tanulságokat. Remélhetőleg megkíméllek néhány horzsolástól a homlokodon.

Megjegyzem, hogy ez nem egy útmutató a CoreData beállításához. Célja, hogy egy cikk legyen néhány jó gyakorlattal, amit átvehetsz a projektjeidhez.

Itt van az Apple gyors áttekintése: “Használd a CoreData-t az alkalmazásod állandó adatainak offline használatra történő mentéséhez, az ideiglenes adatok gyorsítótárba helyezéséhez, valamint az alkalmazásod visszavonási funkciójának egyetlen eszközön történő hozzáadásához.”

A CoreData egy kicsit részletesebben: a CoreData az Apple technológiája a strukturált adatok helyi mentésére. A modellszerkesztővel definiálod az adatstruktúrádat, akárcsak egy adatbázis-séma szerkesztővel, majd ezeket az objektumokat használhatod a kódodban az adatok tárolására.

A CoreData egy objektumgráf-kezelő keretrendszer

Ez azt jelenti, hogy amikor létrehozod a modellt, definiálod az objektumaidat és azok kapcsolatait. Például, ha az adatmodelljében van egy Company (Vállalat) és egy Employee (Alkalmazott) entitás, akkor ezt a két entitást egy kapcsolat fogja összekapcsolni, amely összeköti a kettőt. Valószínűleg employees néven a Company entitáson és employer néven a Employee entitáson. Egyszerűen hozzáadja a kapcsolat tulajdonságot az entitásokhoz, és kész. Nem kell létrehoznia egy összekötő táblát, mint egy adatbázisban.

A CoreData szépsége egy hagyományos adatbázishoz képest az, hogy amikor lekérdezi az X vállalatot, akkor a company.employees elérésével közvetlenül megkapja az alkalmazottakat. Nem kell join-lekérdezést készíteni vagy újra lekérdezni.

A CoreData különböző formátumokban tárolhatja az adatokat

Igen, jól hallotta, a CoreData különböző formátumokban/településeken tudja tárolni az adatokat. Mindegyiknek megvan a maga előnye és hátránya.

  1. Bináris tároló
  2. SQLite tároló
  3. Memórián belüli tároló

Az egyes tárolók előnyeit és hátrányait ebben a cikkben olvashatja el. Én mindig SQLite-ot használok, de el tudom képzelni, hogy egy memórián belüli tároló szupergyors lenne.

Pár fogalom meghatározása

NSManagedObject: ez az alaposztály, amely a CoreData-ban tárolt objektumokat képviseli.

Fetch request: ez a lekérdezés megfelelője a CoreData világában. Ezt a NSFetchedObject osztály képviseli.

Use NSPersistentContainer

Az iOS 10.0-ig a CoreData stack beállítása meglehetősen sok boilerplate kóddal járt. A CoreData többszálú használatához szükséges stratégia meghatározásához magadnak kellett megküzdened.

A NSPersistentContainer-mal az Apple a boilerplate kód nagy részét kapszulázza. Megspórolja neked a vitát arról, hogyan állíthatod be a stacket az optimális teljesítmény érdekében.

Ez a cikk tartalmazza az összes szükséges információt a helyes használathoz.

Használd a kapcsolatokat

Azért említem ezt mindig, mert két alkalommal, olyan projekteknél, amelyeket én örököltem, a CoreData modell úgy volt beállítva, mint egy adatbázis. Nem voltak kapcsolatok definiálva. Ehelyett a kapcsolódó objektum azonosítóját tárolták, és így a kapcsolódó objektum lekérdezéséhez külön lekérdezésre volt szükség. Szerintem sok fejlesztő úgy közelíti meg a CoreData-t, mint egy adatbázist, és elköveti ezt a hibát. Ez végül az egész projektet sokkal összetettebbé és lassabbá teszi.

Ez a cikk segít az elindulásban.

Kerülje a stringes kódot, amennyire csak lehetséges

A stringes kód olyan kódra utal, amely karakterláncokat használ az adatok eléréséhez. Az iOS-en van néhány stringly API: UserDefaults, CoreData néhány, ami eszembe jut.

Ha visszatérünk a céges példához, az alábbiakban két lehetőség van egy cég létrehozására és nevének beállítására:

//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"

Előre mindkét lehetőség működik, lefordítják, nincs velük semmi baj…

A stringly kóddal az a baj, hogy a fordító nem tud segíteni. Képzeld el, hogy a kódban több helyen is hozzá kell férned a name céghez, és reálisan legalább kettőre lesz szükséged: egy a beállításához és egy az olvasásához, most két helyen kell beírnod azt a name stringet. Persze létrehozhatsz egy változót a karakterlánc tárolására, és használhatod azt a változót, de ez extra erőfeszítést igényel, nem lesz beírva ellenőrizve, és mindannyian tudjuk, hogy a fejlesztők lusták…

Szóval általában több helyen marad az a karakterlánc. Aztán át akarod nevezni, és le kell vadászni kereséssel és cserével… Ha egyet kihagysz, az alkalmazásod összeomlik.

Itt van a lista azokról a fordítói funkciókról, amelyeket nem fogsz kihasználni a stringly szintaxissal:

  • auto-complete
  • refactor
  • compiler warning in case of misspelling
  • compiler error if you remove/renamed this entity or property in your model

To be able to use the non-stringes kódváltozatot, egyszerűen meg kell kérnie a fordítót, hogy generáljon osztályokat az entitásokhoz (követheti az Apple utasításait a “Kódgenerálási opció kiválasztása” alatt). Mostantól ez az alapértelmezett egy új Entity létrehozásakor.

Tartsd az NSPersitentContainer.viewContext read-only

A teljesítmény miatt nem akarsz hosszú műveleteket végezni a viewContext-en, mivel az a főszálon fog futni, és esetleg blokkolja az UI-t.

A megoldás az, hogy szokj hozzá, hogy csak olyan blokkban végezz módosításokat az objektumaidon, amelyet a NSPersistentContainer.performBackgroundTask() módszerrel ütemezel. Amikor elmented a módosításokat ebben a blokkban, azok visszaolvadnak a NSPersitentContainer.viewContext-ba, és frissítheted a felhasználói felületet.

Vigyázz a szálakra

A CoreData egyik fő húzása a szálbiztos használat. Bár ha egyszer megértetted a koncepciót, akkor elég könnyű.

Az egész lényege, hogy minden adatobjektum, amit a CoreData-tól kapsz, csak abban a szálban használható, amelyhez az objektum managedobjectContext-je kapcsolódik.

Egy módja annak, hogy biztosítsuk, hogy a megfelelő szálat használjuk az objektum eléréséhez, az NSManagedObjectContext.performBlockAndWait használata, amely biztosítja, hogy a kódunkat a megfelelő szálon használjuk.

Amikor ebben a blokkban vagyunk, a NSManagedObject.objectID és a NSManagedObjectContext.existingObject(withId:) segítségével kell átadni az objektumot a szálakon és a kontextuson keresztül.

Ha ezt nem tesszük, akkor nagy valószínűséggel holtpontra jutunk. Képzeljük el:

  • Egy feladatot a főszálból ütemezünk be a Abban az esetben, ha valami a főszálból ütemezett NSManagedObjectContext.performBlockAndWait(). Ekkor a főszál blokkolva várja, hogy ez a blokk befejeződjön.
  • A blokkban átadod a foo objektumot a fő kontextusból a bar() metódusnak, és elküldöd egy másik szálnak.
  • Jó CoreData állampolgárként a bar() megkapja a foo-hoz csatolt kontextust és a foo.managedObjectContext.performBlockAndWait()
  • Bumm, holtpontra kerültél, mivel ez a kontextus csak a főszálon hajtódik végre és a főszál vár ennek befejezésére.

Egy másik konvenció, ami segít, hogy a nézetvezérlők között csak akkor adunk át objektumokat, ha azok a főszál kontextusában léteznek.

A launch argumentumot használjuk a kontextus és a szálak követésére

Ha követjük a fenti tippeket, akkor ez korlátozza annak az esélyét, hogy egy NSManagedObject példányt rossz szálon hívjunk meg. De ez még mindig előfordulhat. Ezért a fejlesztés során mindenképpen kapcsolja be a szálak hibakeresésére vonatkozó kijelentést

-com.apple.CoreData.ConcurrencyDebug 1

Ez a cikk pontosan elmondja, hogyan kell ezt megtenni, és tájékoztat a további hibakeresés érdekében beállítható egyéb zászlókról.

Az aggregált lekérdezések használata

A CoreData a groupBy, sum és count aggregált függvényeket kínálja, amelyeket lekérdezés közben lehet futtatni. Ennek beállítása kissé bonyolult lehet, de megéri.

Az alternatíva az, hogy létrehoz egy lekérdezést a kritériumoknak megfelelő összes objektum lekérdezéséhez, majd egy for-hurok futtatásával végigfuttatja az összes entitást, és maga végzi el a matematikát. Könnyebb kódolni, de sokkal kevésbé hatékony.

Az aggregált lekérdezés futtatásakor a CoreData lefordítja azt adatbázis-lekérdezéssé, és a matematikát közvetlenül az adatbázis szintjén végzi el. Ez szupergyorsabbá teszi.

Ez a cikk egy jó bevezető arról, hogyan kell ezt csinálni.

Tartsd a coreadatokkal kapcsolatos kódot kategóriákban

Mihelyt az Xcode létrehozta a NSManagedObject alosztályodat, valószínűleg rájössz, hogy van olyan kódod, amelynek az objektumodat reprezentáló osztályban kellene lennie (értékek kiszámításához, adatok importálásához a szerverről stb.). De megkérted a CoreData-t, hogy hozza létre helyetted az osztályokat…

Szóval létre kell hoznod egy osztályt az objektumhoz, amelynek extra viselkedésre van szüksége. Csak a CoreData-val kapcsolatos kódot tedd bele, így tisztán tarthatod.

Vállalj tranzakciós modellt az adataid módosításaihoz

Módosíthatod az objektumaidat, amennyit csak akarod, de a változások perzisztenciájához meg kell hívnod a NSManagedObjectContext.save()-t.

Még hamar elfelejted meghívni a save()-t, és lesz néhány lógó változás a környezetedben. Ezen a ponton minden fogadásnak vége, legközelebb, amikor meghívod a save()-t, nem leszel biztos benne, hogy mit is rögzítesz.

Válaszd a tranzakciós modellt, hogy elkerüld a piszkos kontextus hátrahagyását. Ehhez hozzon létre egy kategóriát a NSPersistentContainer-on a következőképpen:

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

Mostantól kezdve ügyeljen arra, hogy minden változtatását egy performBackgroundSaveTask()-val ütemezett blokkban hajtsa végre. Így garantált, hogy a módosítások akkor mentődnek el, amikor elvégezted őket.

Ne mentsd el a kontextust egy modellkategória metódusából

Ne hívd meg a save()-et egy entitáskategóriából. Nincs annál rosszabb, mint amikor úgy hívsz meg egy metódust az objektumodon, hogy nem veszed észre, hogy ezzel együtt a teljes kontextust is elmented. Néhány olyan változtatás átadása, amit nem biztos, hogy el akarsz menteni. A mentés meghívását azon a ponton kell elvégezni, ahol az objektum módosítását elkezdtük.