- lub 10 lekcji z bitwy learned CoreData lessons
- CoreData jest strukturą zarządzania grafem obiektów
- CoreData może przechowywać dane w różnych formatach
- Kilka definicji terminów
- Use NSPersistentContainer
- Użyj relacji
- Unikaj kodu stringly tak bardzo, jak to możliwe
- Keep your NSPersitentContainer.viewContext read-only
- Obserwuj te wątki
- Użyj argumentu uruchomienia, aby śledzić kontekst i gwintowanie
- Używaj zapytań zbiorczych
- Przechowuj kod związany z danymi podstawowymi w kategoriach
- Zastosuj model transakcyjny dla swoich zmian w danych
- Nie zapisuj kontekstu z wnętrza metody kategorii modelu
lub 10 lekcji z bitwy learned CoreData lessons
Używałem CoreData w wielu projektach podczas mojej kariery dewelopera platformy Apple (pierwszy raz było to w MacOS 10.5…), i przez lata nauczyłem się rzeczy w trudny sposób (aka: „walenie głową w mur przez zbyt długi czas”). Pomyślałem więc, że podzielę się z Wami moją wiedzą. Mam nadzieję, że oszczędzę ci kilku siniaków na czole.
Uwaga, że to nie jest przewodnik jak skonfigurować CoreData. Ma to być artykuł z kilkoma dobrymi praktykami, które możesz podnieść dla swoich projektów.
Oto szybki przegląd Apple: „Użyj Core Data, aby zapisać stałe dane aplikacji do użytku w trybie offline, buforować dane tymczasowe i dodać funkcjonalność cofania do aplikacji na jednym urządzeniu.”
Aby podać nieco więcej szczegółów, CoreData to technologia Apple do lokalnego zapisywania danych strukturalnych. Używasz edytora modelu, aby zdefiniować swoją strukturę danych, tak jak edytor schematu bazy danych, a następnie możesz użyć tych obiektów w swoim kodzie do przechowywania danych.
CoreData jest strukturą zarządzania grafem obiektów
Co to oznacza, że kiedy tworzysz swój model, definiujesz swoje obiekty i ich relacje. Na przykład, jeśli masz encję Company i encję Employee w swoim modelu danych, te dwie encje będą połączone przez relację, która łączy te dwie encje. Prawdopodobnie o nazwie employees
na encji Company
i employer
na encji Employee
. Po prostu dodajesz właściwość relacji do encji i to wszystko. Nie musisz tworzyć tabeli złączenia jak w bazie danych.
Piękno CoreData w porównaniu do tradycyjnej bazy danych polega na tym, że kiedy zapytasz o firmę X, będziesz w stanie uzyskać pracowników bezpośrednio poprzez dostęp do company.employees
. Nie ma potrzeby budowania zapytań łączących lub ponawiania zapytań.
CoreData może przechowywać dane w różnych formatach
Tak, dobrze słyszałeś, CoreData może zapisywać dane w różnych formatach/miejscach. Każdy z nich ma swoje wady i zalety.
- Sklep binarny
- SklepSQLite
- Sklep w pamięci
Możesz przeczytać ten artykuł, aby poznać wady i zalety każdego z nich. Ja zawsze używam SQLite, ale widzę, jak sklep in-memory byłby super szybki.
Kilka definicji terminów
NSManagedObject
: jest to klasa bazowa reprezentująca obiekty, które przechowujesz w CoreData.
Żądanie pobierania: jest to odpowiednik zapytania w świecie CoreData. Jest reprezentowany przez klasę NSFetchedObject
.
Use NSPersistentContainer
Do czasu iOS 10.0 konfigurowanie stosu CoreData wiązało się z dość dużą ilością kodu typu boilerplate. Musiałeś walczyć o siebie, aby zdefiniować strategię, której należy użyć, aby poradzić sobie z wielowątkowym użyciem CoreData.
Z NSPersistentContainer
, Apple enkapsuluje większość kodu boilerplate. Oszczędzając ci debaty o tym, jak najlepiej skonfigurować swój stos, aby uzyskać optymalną wydajność.
Ten artykuł zawiera wszystkie informacje, których potrzebujesz, aby użyć go poprawnie.
Użyj relacji
Zawsze o tym wspominam, ponieważ przy dwóch okazjach, dla projektów, które odziedziczyłem, model CoreData został skonfigurowany jak baza danych. Nie były zdefiniowane żadne relacje. Zamiast tego identyfikator powiązanego obiektu był przechowywany, a zatem wymagane było dodatkowe żądanie pobierania, aby uzyskać powiązany obiekt. Myślę, że wielu programistów podchodzi do CoreData jak do bazy danych i popełnia ten błąd. Kończy się to tym, że cały projekt staje się dużo bardziej złożony i dużo wolniejszy.
Prześledź ten artykuł, abyś mógł zacząć.
Unikaj kodu stringly tak bardzo, jak to możliwe
Kod stringly odnosi się do kodu, który używa ciągów znaków, aby uzyskać dostęp do danych. W systemie iOS masz kilka interfejsów API typu stringly: UserDefaults
, CoreData
to kilka, które przychodzą na myśl.
Jeśli wrócimy do przykładu firmy, poniżej znajdują się dwie opcje tworzenia firmy i ustawiania jej nazwy:
//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 pierwszy rzut oka, obie te opcje działają, kompilują się, nie ma w nich nic złego…
Problem z kodem stringly polega na tym, że kompilator nie może ci pomóc. Wyobraź sobie, że musisz uzyskać dostęp do name
firmy w wielu miejscach w kodzie, a realistycznie będziesz potrzebował co najmniej dwóch: jednego, aby ustawić i jednego, aby go odczytać, musisz teraz wpisać ten name
ciąg w dwóch miejscach. Jasne, że możesz utworzyć zmienną do przechowywania łańcucha i użyć tej zmiennej, ale to wymaga dodatkowego wysiłku, nie będzie to wpisane sprawdzone i wszyscy wiemy, że programiści są leniwi…
Więc to zwykle pozostawia ten ciąg w wielu miejscach. Następnie chcesz zmienić jego nazwę i musisz go upolować za pomocą wyszukiwania i zastępowania… Jeśli przegapisz jeden, twoja aplikacja ulegnie awarii.
Oto lista funkcji kompilatora, z których nie skorzystasz ze składnią stringly:
- auto-complete
- refactor
- ostrzeżenie kompilatora w przypadku błędnej pisowni
- błąd kompilatora, jeśli usuniesz/zmienisz nazwę tej encji lub właściwości w swoim modelu
Aby móc korzystać z wersji kodu nie-stringowej wersji kodu będziesz musiał po prostu poprosić kompilator o wygenerowanie klas dla twoich encji (możesz podążać za instrukcjami Apple pod „Select a Code Generation Option”). Jest to teraz domyślne podczas tworzenia nowego Entity.
Keep your NSPersitentContainer.viewContext read-only
Z powodów wydajnościowych nie chcesz wykonywać długich operacji na viewContext, ponieważ będzie on działał na głównym wątku i potencjalnie blokował UI.
Rozwiązaniem jest nabranie nawyku dokonywania modyfikacji swoich obiektów tylko wewnątrz bloku, który zaplanujesz za pomocą metody NSPersistentContainer.performBackgroundTask()
. Kiedy zapiszesz swoje zmiany w tym bloku, zostaną one scalone z powrotem do NSPersitentContainer.viewContext
i będziesz mógł zaktualizować swój UI.
Obserwuj te wątki
Jednym z głównych problemów z CoreData jest używanie go w sposób bezpieczny dla wątków. Choć gdy już załapiesz koncepcję, jest to dość łatwe.
Do czego to się sprowadza, to fakt, że każdy obiekt danych, który uzyskasz z CoreData, może być używany tylko w wątku, do którego dołączony jest managedobjectContext
obiektu.
Jednym ze sposobów zapewnienia, że używasz właściwego wątku, aby uzyskać dostęp do obiektu, jest użycie NSManagedObjectContext.performBlockAndWait
, który zapewni, że twój kod jest używany we właściwym wątku.
Już w tym bloku będziesz musiał użyć NSManagedObject.objectID
i NSManagedObjectContext.existingObject(withId:)
, aby przekazać obiekt przez wątki i kontekst.
Jeśli tego nie zrobisz, najprawdopodobniej skończysz z martwymi blokadami. Wyobraź sobie:
- Planujesz zadanie z głównego wątku z W przypadku, gdy coś zaplanowane z głównego wątku
NSManagedObjectContext.performBlockAndWait()
. W tym momencie główny wątek jest zablokowany, czekając na zakończenie tego bloku. - Wewnątrz tego bloku przekazujesz obiekt
foo
z głównego kontekstu do metodybar()
i wysyłasz go do innego wątku. - Aby być dobrym obywatelem CoreData
bar()
otrzyma kontekst dołączony do foo i wyśle swoje zadanie za pomocąfoo.managedObjectContext.performBlockAndWait()
- Bum, jesteś deadlocked, ponieważ ten kontekst wykonuje się tylko na głównym wątku, a główny wątek czeka na to complete.
Inną konwencją, która pomaga, jest przekazywanie obiektów między kontrolerami widoku tylko wtedy, gdy istnieją one w głównym kontekście wątku.
Użyj argumentu uruchomienia, aby śledzić kontekst i gwintowanie
Jeśli zastosujesz się do powyższych wskazówek, powinno to ograniczyć szanse na wywołanie instancji NSManagedObject
w niewłaściwym wątku. Ale to wciąż może się zdarzyć. Upewnij się więc, że kiedy jesteś w fazie rozwoju, włączysz asercję debugowania wątków
-com.apple.CoreData.ConcurrencyDebug 1
Ten artykuł powie ci dokładnie, jak to zrobić i opowie o innych flagach, które możesz ustawić, aby uzyskać więcej debugowania.
Używaj zapytań zbiorczych
CoreData oferuje groupBy
, sum
i count
jako funkcje zbiorcze, które mogą być uruchamiane podczas zapytania. Ustawienie tego może być trochę skomplikowane, ale warto.
Alternatywą jest stworzenie zapytania, aby uzyskać wszystkie obiekty, które pasują do twoich kryteriów, a następnie uruchomienie pętli for przez wszystkie encje i zrobienie matematyki samodzielnie. Łatwiejsze do zakodowania, ale o wiele mniej wydajne.
Gdy uruchamiasz zbiorcze żądanie pobierania, CoreData tłumaczy je na zapytanie do bazy danych i uruchamia matematykę bezpośrednio na poziomie bazy danych. To czyni go super szybkim.
Ten artykuł jest dobrym wprowadzeniem do tego, jak to zrobić.
Przechowuj kod związany z danymi podstawowymi w kategoriach
Gdy masz już swoją podklasę NSManagedObject
utworzoną przez Xcode, najprawdopodobniej odkryjesz, że masz kod, który powinien znajdować się w klasie, która reprezentuje twój obiekt (do obliczania wartości, importowania danych z twojego serwera itp.) Ale poprosiłeś CoreData o stworzenie klas dla ciebie…
Więc będziesz musiał stworzyć kategorię dla obiektu, który potrzebuje dodatkowego zachowania. Umieszczaj tam tylko kod związany z CoreData, abyś mógł utrzymać go w czystości.
Zastosuj model transakcyjny dla swoich zmian w danych
Możesz modyfikować swoje obiekty tak bardzo, jak chcesz, ale aby utrwalić te zmiany, musisz wywołać NSManagedObjectContext.save()
.
Zauważysz, że szybko zapomnisz wywołać save()
i będziesz miał kilka dyndających zmian w swoim kontekście. W tym momencie wszystkie zakłady są wyłączone, następnym razem, gdy zadzwonisz do save()
, nie będziesz pewien, co popełniasz.
Zaadoptuj model transakcji, aby uniknąć pozostawienia brudnego kontekstu za sobą. Aby to zrobić, utwórz kategorię na NSPersistentContainer
w taki sposób:
extension NSPersistentContainer {
@objc func performBackgroundSaveTask(_ block: @escaping (NSManagedObjectContext) -> Void) {
performBackgroundTask { (context: NSManagedObjectContext) in
block(context)
context.save()
}
}
}
Upewnij się od teraz, że wszelkie zmiany, których dokonujesz, są wykonywane w bloku zaplanowanym za pomocą performBackgroundSaveTask()
. W ten sposób masz gwarancję, że zmiany zostaną zapisane, gdy je wprowadzisz.
Nie zapisuj kontekstu z wnętrza metody kategorii modelu
Nigdy nie wywołuj save()
z wnętrza kategorii encji. Nie ma nic gorszego niż wywołanie metody na twoim obiekcie, nie zdając sobie sprawy, że zapisze ona cały kontekst razem z nim. Wprowadzanie pewnych zmian, których możesz nie chcieć zapisać. Wywołanie save powinno być wykonane w momencie, gdy rozpoczyna się modyfikowanie obiektu.