- eller 10 slag lært CoreData-lektioner
- CoreData er en ramme for forvaltning af objektgrafer
- CoreData kan gemme dataene i forskellige formater
- Et par begrebsdefinitioner
- Brug NSPersistentContainer
- Brug relationer
- Undgå stringly-kode så meget som muligt
- Hold din NSPersitentContainer.viewContext skrivebeskyttet
- Se de tråde
- Brug launch-argumentet til at spore kontekst og threading
- Brug aggregerede forespørgsler
- Hold coreData-relateret kode i kategorier
- Optag en transaktionsmodel for dine ændringer af dine data
- Spar ikke konteksten inde fra en modelkategorimetode
eller 10 slag lært CoreData-lektioner
Jeg har brugt CoreData over flere projekter i løbet af min udviklerkarriere på Apple-platformen (første gang var i MacOS 10.5…), og i årenes løb har jeg lært tingene på den hårde måde (aka: “at banke mit hoved mod væggen alt for længe”). Så jeg tænkte, at jeg ville dele min læring med dig. Forhåbentlig sparer jeg dig for et par blå mærker i panden.
Bemærk, at dette ikke er en how-to guide til opsætning af CoreData. Det er ment som en artikel med noget god praksis, som du kan hente til dine projekter.
Her er Apples hurtige oversigt: “Brug Core Data til at gemme din applikations permanente data til offlinebrug, til at cache midlertidige data og til at tilføje fortrydelsesfunktionalitet til din app på en enkelt enhed.”
For at give lidt flere detaljer er CoreData Apples teknologi til at gemme dine strukturerede data lokalt. Du bruger modeleditoren til at definere din datastruktur ligesom en database-skemaeditor, hvorefter du kan bruge disse objekter i din kode til at gemme dine data.
CoreData er en ramme for forvaltning af objektgrafer
Det betyder, at når du opretter din model, definerer du dine objekter og deres relationer. Hvis du f.eks. har en virksomhedsenhed og en medarbejderenhed i din datamodel, vil disse to enheder være forbundet af en relation, der forbinder de to enheder. Sandsynligvis med navnet employees
på Company
-entiteten og employer
på Employee
-entiteten. Du skal blot tilføje relationsegenskaben på entiteterne, og det er det hele. Du behøver ikke at oprette en forbindelsestabel som i en database.
Det smukke ved CoreData i forhold til en traditionel database er, at når du forespørger for at få virksomhed X, kan du få de ansatte direkte ved blot at få adgang til company.employees
. Det er ikke nødvendigt at opbygge en join-forespørgsel eller at foretage en ny forespørgsel.
CoreData kan gemme dataene i forskellige formater
Ja, du hørte det rigtigt, CoreData kan gemme dine data i forskellige formater/steder. Hver har sine fordele og ulemper.
- Binary store
- SQLite Store
- In Memory store
Du kan læse denne artikel for at få fordele og ulemper ved hver enkelt. Jeg bruger altid SQLite, men jeg kunne godt se, at et in-memory-lager ville være superhurtigt.
Et par begrebsdefinitioner
NSManagedObject
: Dette er den basisklasse, der repræsenterer de objekter, du har gemt i CoreData.
Fetch request: Dette svarer til en forespørgsel i CoreData’s verden. Den repræsenteres af klassen NSFetchedObject
.
Brug NSPersistentContainer
Der var indtil iOS 10.0 en hel del boilerplate-kode involveret i oprettelsen af CoreData-stakken. Man måtte selv klare sig for at definere den strategi, der skulle bruges til at håndtere multitrådet brug af CoreData.
Med NSPersistentContainer
indkapsler Apple det meste af boilerplate-koden. Det sparer dig for diskussionen om, hvordan du bedst opsætter din stak for optimal ydeevne.
Denne artikel indeholder alle de oplysninger, du har brug for for at bruge den korrekt.
Brug relationer
Jeg nævner altid dette, fordi CoreData-modellen ved to lejligheder, for projekter, som jeg arvede, var opsat som en database. Der var ikke defineret nogen relationer. I stedet blev ID’et for det relaterede objekt gemt, og der var således behov for en ekstra hentningsanmodning for at hente det relaterede objekt. Jeg tror, at mange udviklere nærmer sig CoreData som en database og begår denne fejl. Det ender med at gøre hele projektet meget mere komplekst og meget langsommere.
Følg denne artikel for at komme i gang.
Undgå stringly-kode så meget som muligt
Stringly-kode henviser til kode, der bruger strenge til at få adgang til data. På iOS har du et par stringly-API’er: UserDefaults
, CoreData
er nogle få, som jeg kommer til at tænke på.
Hvis vi vender tilbage til virksomhedseksemplet, er der nedenfor to muligheder for at oprette en virksomhed og angive dens navn:
//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"
På første øjekast virker begge disse muligheder, de kompileres, der er intet galt med disse…
Problemet med stringly-kode er, at kompileren ikke kan hjælpe dig. Forestil dig at du har brug for at få adgang til name
af virksomheden flere steder i koden, og realistisk set har du brug for mindst to: en til at sætte den og en til at læse den, du skal nu skrive denne name
streng to steder. Selvfølgelig kan du oprette en variabel til at gemme strengen og bruge den variabel, men det kræver en ekstra indsats, den vil ikke blive skrevet kontrolleret, og vi ved alle, at udviklere er dovne…
Så det efterlader normalt denne streng flere steder. Så vil du omdøbe den, og du er nødt til at jagte den med søg og erstat… Hvis du overser en, vil din app gå ned.
Her er listen over compilerfunktioner, som du ikke vil drage fordel af med stringly-syntaksen:
- auto-complete
- refactor
- compileradvarsel i tilfælde af stavefejl
- compilerfejl, hvis du fjerner/omdøbt denne enhed eller egenskab i din model
For at kunne bruge den ikkestringly-version af koden skal du blot bede compileren om at generere klasser til dine entiteter (du kan følge Apples instruktioner under “Vælg en kodegenereringsmulighed”). Dette er nu standard, når du opretter en ny Entity.
Hold din NSPersitentContainer.viewContext skrivebeskyttet
Af ydelsesmæssige årsager ønsker du ikke at udføre lange operationer på viewContext, da det vil køre på hovedtråden og potentielt blokere brugergrænsefladen.
Løsningen er at gøre det til en vane kun at foretage ændringer i dine objekter inden for en blok, som du planlægger med metoden NSPersistentContainer.performBackgroundTask()
. Når du gemmer dine ændringer i den blok, bliver de slået sammen igen i NSPersitentContainer.viewContext
, og du kan opdatere din brugergrænseflade.
Se de tråde
Et af de vigtigste træk ved CoreData er at bruge det på en trådsikker måde. Men når du først har forstået konceptet, er det ret nemt.
Hvad det går ud på, er, at ethvert dataobjekt, du får fra CoreData, kun kan bruges på den tråd, som objektets managedobjectContext
er knyttet til.
En måde at sikre, at du bruger den rigtige tråd til at få adgang til dit objekt, er at bruge NSManagedObjectContext.performBlockAndWait
, som sikrer, at din kode bruges på den rigtige tråd.
Når du er i denne blok, skal du bruge NSManagedObject.objectID
og NSManagedObjectContext.existingObject(withId:)
til at videregive objektet på tværs af tråde og kontekst.
Hvis du ikke gør dette, vil du højst sandsynligt ende med deadlocks. Forestil dig:
- Du planlægger en opgave fra hovedtråden med I det tilfælde, hvor noget planlagt fra hovedtråden
NSManagedObjectContext.performBlockAndWait()
. På dette tidspunkt er hovedtråden blokeret og venter på, at denne blok skal afsluttes. - Inden for denne blok videregiver du objektet
foo
fra hovedkonteksten til metodenbar()
og sender det til en anden tråd. - For at være en god CoreData-borger vil
bar()
få konteksten knyttet til foo og afsende sin opgave ved hjælp affoo.managedObjectContext.performBlockAndWait()
- Boom, du er gået i hårdknude, da denne kontekst kun udføres på hovedtråden, og hovedtråden venter på denne færdig.
En anden konvention, der hjælper, er kun at videregive objekter mellem viewcontrollere, hvis de findes i hovedtrådskonteksten.
Brug launch-argumentet til at spore kontekst og threading
Hvis du følger ovenstående tips, burde dette begrænse chancerne for, at en NSManagedObject
-instans kaldes på den forkerte tråd. Men det kan stadig ske. Så sørg for, når du er i udvikling, at aktivere thread debugging assertion
-com.apple.CoreData.ConcurrencyDebug 1
Denne artikel fortæller dig præcis, hvordan du gør det, og fortæller dig om andre flag, du kan opsætte for mere debugging.
Brug aggregerede forespørgsler
CoreData tilbyder groupBy
, sum
og count
som aggregerede funktioner, der kan køres under en forespørgsel. Det kan være lidt kompliceret at konfigurere den, men det er det hele værd.
Alternativet er at oprette en forespørgsel for at få alle de objekter, der svarer til dine kriterier, og derefter køre en for-løkke gennem alle enhederne og selv lave regnestykket. Nemmere at kode, men meget mindre effektivt.
Når du kører en aggregeret hentningsanmodning, oversætter CoreData den til en databaseforespørgsel og kører matematikken direkte på databaseniveau. Det gør det super hurtigt.
Denne artikel er en god intro til, hvordan du gør det.
Hold coreData-relateret kode i kategorier
Når du har din NSManagedObject
-underklasse oprettet af Xcode, vil du højst sandsynligt opdage, at du har kode, der bør være i den klasse, der repræsenterer dit objekt (til beregning af værdier, import af data fra din server osv.). Men du har bedt CoreData om at oprette klasserne for dig…
Så du bliver nødt til at oprette en kategori for det objekt, der har brug for ekstra adfærd. Læg kun kode relateret til CoreData deri, så du kan holde det rent.
Optag en transaktionsmodel for dine ændringer af dine data
Du kan ændre dine objekter så meget du vil, men for at persistere disse ændringer skal du kalde NSManagedObjectContext.save()
.
Du vil hurtigt glemme at kalde save()
, og du vil have nogle dinglende ændringer i din kontekst. På dette tidspunkt er alle væddemål ude af spil, næste gang du kalder save()
, vil du ikke være sikker på, hvad du overdrager.
Ansæt en transaktionsmodel for at undgå at efterlade en beskidt kontekst. For at gøre det skal du oprette en kategori på NSPersistentContainer
på følgende måde:
extension NSPersistentContainer {
@objc func performBackgroundSaveTask(_ block: @escaping (NSManagedObjectContext) -> Void) {
performBackgroundTask { (context: NSManagedObjectContext) in
block(context)
context.save()
}
}
}
Sørg fra nu af for, at alle ændringer, du foretager, udføres i en blok, der er planlagt med performBackgroundSaveTask()
. På den måde er du garanteret, at ændringerne gemmes, når du foretager dem.
Spar ikke konteksten inde fra en modelkategorimetode
Kald aldrig save()
inde fra en enhedskategori. Der er intet værre end at kalde en metode på dit objekt uden at være klar over, at den vil gemme hele konteksten med den. Committe nogle ændringer, som du måske ikke ønsker at gemme. Kaldet save bør foretages på det tidspunkt, hvor ændringerne i objektet påbegyndes.