📜 ⬆️ ⬇️

Core Data in detail

Recently, I started working on a large project using Core Data. The usual thing is that people on projects change, experience is lost, and the nuances are forgotten. It is impossible to deepen everyone into studying a specific framework - everyone has their own working problems. Therefore, I prepared a small presentation, from those points that I consider important or insufficiently covered in the tutorials. I share with everyone and I hope that this will help write effective code and not make mistakes. It is assumed that you are already a bit in the subject.

I'll start with the banal.

Core Data is a framework that manages and stores data in an application. You can think of Core Data as a shell over a physical relational repository that represents data as objects, while Core Data itself is not a database.

Core Data Objects


image

To create a repository, the application uses the NSPersistentStoreCoordinator or NSPersistentContainer classes . NSPersistentStoreCoordinator creates a storage of the specified type based on the model, you can specify the location and additional options. NSPersistentContainer can be used with IOS10, allows you to create with a minimum amount of code.

It works as follows: if there is a database along the specified path, the coordinator checks its version and, if necessary, makes a migration. If the base does not exist, then it is created based on the NSManagedObjectModel model. To make it all work correctly, before making changes to the model, create a new version in Xcode through the menu Editor -> Add Model Version. If you bring the path, you can find and open the base in the emulator.

Example with NSPersistentStoreCoordinator
var persistentCoordinator: NSPersistentStoreCoordinator = { let modelURL = Bundle.main.url(forResource: "Test", withExtension: "momd") let managedObjectModel = NSManagedObjectModel(contentsOf: modelURL!) let persistentCoordinator = NSPersistentStoreCoordinator(managedObjectModel: managedObjectModel!) let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] let storeURL = URL(fileURLWithPath: documentsPath.appending("/Test.sqlite")) print("storeUrl = \(storeURL)") do { try persistentCoordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: storeURL, options: [NSSQLitePragmasOption: ["journal_mode":"MEMORY"]]) return persistentCoordinator } catch { abort() } } () 

Example with NSPersistentContainer
 var persistentContainer: NSPersistentContainer = { let container = NSPersistentContainer(name: "CoreDataTest") container.loadPersistentStores(completionHandler: { (storeDescription, error) in print("storeDescription = \(storeDescription)") if let error = error as NSError? { fatalError("Unresolved error \(error), \(error.userInfo)") } }) return container } () 


Core Data uses 4 types of storage:

- SQLite
- Binary
- In-Memory
- XML ​​(Mac OS only)

If, for example, for security reasons you do not want to store data in a file form, but you also want to use caching during the session and data in the form of objects, an “In-Memory” storage is quite suitable. Actually, it is not forbidden to have several repositories of different types in one application.

I would like to say a few words about the NSManagedObjectContext object. In general, Apple provides a rather vague wording for NSManagedObjectContext - an environment for working with Core Data objects. All this comes from the desire to dissociate from associations with relational databases, and to present Core Data as an easy-to-use tool that does not require an understanding of keys, transactions, and other database attributes. But in the language of relational databases NSManagedObjectContext can, in a sense, be called a transaction manager. You probably noticed that it has save and rollback methods, although most likely you use only the first one.

A misunderstanding of this simple fact leads to the use of a single-context model, even in situations where it is not enough. For example, you are editing a large document, and at the same time you needed to load a couple of reference books. At what point do you call save? If we were working with a relational database, then there would be no questions, since each operation would be performed in its own transaction. In Core Data, there is also a quite convenient way to solve this problem - this is a branch of the child context. But unfortunately, it is for some reason rarely used. Here is a good article on this topic.

Inheritance


For some reason I do not understand, there are a very large number of manuals and examples where inheritance for Entity / NSManagedObject (tables) is not used at all. Meanwhile, it is a very convenient tool. If you do not use inheritance, then you can assign values ​​to attributes (fields) only through the KVC mechanism, which does not check the names and types of attributes, and this can easily lead to runtime errors.

Class overrides for NSManagedObject are done in Core Data Designer:

image

Inheritance and Code Generation


After specifying the class name for Entity, you can use code generation and get a class with ready-made code:

image

image

If you want to see the auto-generated code, but at the same time, do not want to add files to the project, you can use another way: set the Entity option "Codegen". In this case, the code must be searched in ../DerivedData/ ...

image

Use code generation to create classes, typos in variable names can lead to runtime errors.

Something like this will be generated:

 @objc public class Company: NSManagedObject {    @NSManaged public var inn: String?  @NSManaged public var name: String?  @NSManaged public var uid: String?  @NSManaged public var employee: NSSet? } 

In swift, @NSManaged has the same meaning as dynamic in Objective C.
Core Data itself takes care of obtaining data (has internal accessors) for the attributes of its classes. If you have transit fields, then you need to add functions to calculate them.

Classes inherited from NSManagedObject (tables) did not have an “ordinary” constructor before IOS10, unlike other classes. To create an object of type Company, it was necessary to write a rather cumbersome construct using the NSEntityDescription. Now there is a more convenient method of initialization through context (NSManagedObjectContext). Code below. Pay attention to the advantage of inheritance when assigning attributes to the KVC mechanism:

 // 1 - создание записи через NSEntityDescription, присвоение значений через KVO let company1 = NSEntityDescription.insertNewObject(forEntityName: "Company", into: moc) company1.setValue("077456789111", forKey: "inn") company1.setValue("Натура кура", forKey: "name") // 2 - создание записи через NSEntityDescription, присвоение значений через свойства let company2 = NSEntityDescription.insertNewObject(forEntityName: "Company", into: moc) as! Company company2.inn = "077456789222" company2.name = "Крошка макарошка" // 3 - создание записи через инициализатор (IOS10+), присвоение значений через свойства let company3 = Company(context: moc) company3.inn = "077456789222" company3.name = "Крошка макарошка" 

Namespace for NSManagedObject


Another thing worth mentioning is the namespace.

image

You will have no difficulty if you are working on ObjectiveC or Swift. Usually, this field is filled in correctly by default. But in mixed projects, it may be a surprise for you that for classes in swift and ObjectiveC you need to set different options. In Swift, the "Module" must be filled. If this field is not filled in, a prefix with the name of the project will be added to the class name, which will cause an execution error. In Objetive C “Module” leave empty, otherwise NSManagedObject will not be found when accessing it through the class name.

Connections between objects


In principle, the topic of relationships is well covered, but I want to focus on how to add child entities to the parent. Therefore, first quickly recall the mechanism for creating relationships. Consider the traditional example, the company - employees, one-to-many communication:


image

image

Apple very much insists on specifying inverse relationships. At the same time, inversion does not enhance connectivity, but helps Core Data to track changes on both sides of the connection; this is important for caching and updating information.

It is also important to correctly specify the removal rule. The delete rule is an action that will be executed on this object when the parent object is deleted.


In this example, if you delete a company, all employees will be deleted (cascade). When you delete an employee, the link to him in the company will be reset (pre screen)

Ways to add child entities to parent


1) The first way is to add via NSSet. For example, add 2 employees to the company:

 let set = NSMutableSet();    if let employee1 = NSEntityDescription.insertNewObject(forEntityName: "Employee", into: moc) as? Employee { employee1.firstName = "Дима" employee1.secondName = "Васильев" set.add(employee1) } if let emploee2 = NSEntityDescription.insertNewObject(forEntityName: "Employee", into: moc) as? Employee { employee2.firstName = "Наташа" employee2.secondName = "Ростова" set.add(employee2) }    company.employee = set 

This method is convenient for the initial initialization of an object or when filling a database. There is a small nuance. If the company already had employees, and you assigned a new set, then the former employees will reset the link to the company, but they will not be deleted. As an option, you can get a list of employees and work with this set already.

 let set = company.mutableSetValue(forKey: "employee") 

2) Adding child objects through parent ID

 if let employee = NSEntityDescription.insertNewObject(forEntityName: "Employee", into: moc) as? Employee { employee.firstName = "Маша" employee.secondName = "Богданова" employee.company = company } 

The second method is convenient when adding or editing a child object in
separate form.

3) Adding child objects through auto-generated methods

 extension Company {    @objc(addEmployeeObject:)  @NSManaged public func addEmployee(_ value: Employee)    @objc(removeEmployeeObject:)  @NSManaged public func removeFromEmployee(_ value: Employee)    @objc(addEmployee:)  @NSManaged public func addEmployee(_ values: NSSet)    @objc(removeEmployee:)  @NSManaged public func removeFromEmployee(_ values: NSSet) } 

For the sake of completeness, it is useful to know about this method, but somehow it was not useful to me, and I delete the extra code in order not to overload the project.

Requests with a condition on a child element


In Core Data, you cannot make an arbitrary query between any data, as we can do in SQL. But between dependent objects it is easy to follow using the standard predicate. Below is an example of a query that selects all companies in which there is an employee with the specified name:

 public static func getCompanyWithEmployee(name: String) -> [Company] { let request = NSFetchRequest<NSFetchRequestResult>(entityName: self.className()) request.predicate = NSPredicate(format: "ANY employee.firstName = %@", name) do { if let result = try moc.fetch(request) as? [Company] { return result } } catch { } return [Company]() } 

The method call in the code will look like this:

 // выбрать комании, где работает Миша let companies = Company.getCompanyWithEmployee(name: "Миша") 

Do not use transit fields in requests, their values ​​are not defined at the time of the request. Errors will not happen, but the result will be wrong.

Setting Attributes (Fields)


You may have noticed that the attributes of the Entity have several options.
C optional everything is clear from the title.

The option “use scalar type” appeared in swif. In Objective-C, scalar types are not used for attributes, since they cannot be nil. Attempting to assign a scalar value to an attribute via KVC will cause an execution error. From here it becomes clear why attribute types in Core Data do not have a strict correspondence with language types. In swift and mixed projects, scalar-type attributes can be used.

Transit attributes are calculated fields that are not stored in the database. They can be used for encryption. These attributes get values ​​through the redefined accessor, or by assigning primitives as needed (for example, overriding willSave and awakeFromFetch).

Attribute accessors:


If you do not need to use calculated fields, for example, do encryption or something else, then you can not even think about what are accessories accessories. Meanwhile, the operations of obtaining and assigning values ​​to attributes are not “atomic”. To understand what I mean, see the code below:

 // чтение let name = company.name // чтение company.willAccessValue(forKey: "name") let name = company.primitiveValue(forKey: "name") company.didAccessValue(forKey: "name") // присвоение company.name = "Азбука укуса" // присвоение company.willChangeValue(forKey: "name") company.setPrimitiveValue("Азбука укуса", forKey: "name") company.didChangeValue(forKey: "name") 

Use primitives in the NSManagedObject event, instead of the usual assignment, to avoid looping. Example:

 override func willSave() {   let nameP = encrypt(field: primitiveValue(forKey: "name"), password: password)   setPrimitiveValue(nameP, forKey: "nameC")   super.willSave() }  override func awakeFromFetch() {   let nameP = decrypt(field: primitiveValue(forKey: "nameC"), password: password)   setPrimitiveValue(nameP, forKey: "name")   super.awakeFromFetch() } 

If suddenly you have to screw the awakeFromFetch function into a project, you will be surprised that it works very strangely, but in fact it is not called when you execute the request. This is due to the fact that Core Data has a very intelligent caching mechanism, and if the sample is already in memory (for example, due to the fact that you just filled this table), then the method will not be called. Nevertheless, my experiments have shown that in terms of calculated values, you can safely rely on the use of awakeFromFetch, the same Apple documentation says. If for testing and development you need to forcefully call awakeFromFetch, add managedObjectContext.refreshAllObjects () before the request.

That's all.

Thanks to everyone who read to the end.

Source: https://habr.com/ru/post/436510/