Données de base en détail

Récemment, j'ai commencé à travailler sur un grand projet utilisant Core Data. La chose habituelle est que les gens sur les projets changent, l'expérience est perdue et les nuances sont oubliées. Il est impossible d'approfondir tout le monde dans l'étude d'un cadre spécifique - chacun a ses propres problèmes de travail. Par conséquent, j'ai préparé une courte présentation, à partir des points que je considère importants ou insuffisamment traités dans les tutoriels. Je partage avec tout le monde et j'espère que cela aidera à écrire du code efficace et à ne pas faire d'erreurs. On suppose que vous êtes déjà un peu dans le sujet.

Je vais commencer par le banal.

Core Data est un cadre qui gère et stocke les données dans une application. Vous pouvez considérer Core Data comme un wrapper sur un stockage relationnel physique qui représente des données sous forme d'objets, tandis que Core Data lui-même n'est pas une base de données.

Objets de données de base


image

Pour créer du stockage, l'application utilise les classes NSPersistentStoreCoordinator ou NSPersistentContainer . NSPersistentStoreCoordinator crée un stockage du type spécifié en fonction du modèle, vous pouvez spécifier l'emplacement et des options supplémentaires. NSPersistentContainer peut être utilisé avec iOS10, offre la possibilité de créer avec un minimum de code.

Il fonctionne comme suit: si une base de données existe sur le chemin spécifié, le coordinateur vérifie sa version et, si nécessaire, effectue une migration. Si la base de données n'existe pas, elle est créée sur la base du modèle NSManagedObjectModel. Pour que tout cela fonctionne correctement, avant d'apporter des modifications au modèle, créez une nouvelle version dans Xcode via le menu Editeur -> Ajouter une version de modèle. Si vous obtenez le chemin, vous pouvez trouver et ouvrir la base dans l'émulateur.

Exemple avec 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() } } () 

Exemple 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 utilise 4 types de stockage:

- SQLite
- binaire
- en mémoire
- XML ​​(Mac OS uniquement)

Si, par exemple, pour des raisons de sécurité, vous ne souhaitez pas stocker de données sous forme de fichier mais en même temps utiliser la mise en cache pendant la session et des données sous forme d'objets, le stockage de type «In-Memory» est tout à fait adapté. En fait, il n'est pas interdit d'avoir plusieurs types de stockages différents dans une même application.

Je veux dire quelques mots sur l'objet NSManagedObjectContext . En général, Apple fournit une formulation très vague pour NSManagedObjectContext - un environnement pour travailler avec des objets Core Data. Tout cela du désir de se dissocier des associations avec les bases de données relationnelles, et de présenter Core Data comme un outil facile à utiliser qui ne nécessite pas une compréhension des clés, des transactions et d'autres attributs Bazdan. Mais dans le langage des bases de données relationnelles, NSManagedObjectContext peut, dans un sens, être appelé un gestionnaire de transactions. Vous avez probablement remarqué qu'il dispose de méthodes de sauvegarde et de restauration, bien que vous n'utilisiez probablement que la première.

Une méconnaissance de ce simple fait conduit à l'utilisation d'un modèle à contexte unique, même dans des situations où il ne suffit pas. Par exemple, vous modifiez un document volumineux et en même temps, vous avez dû télécharger quelques répertoires. À quel moment appelez-vous save? Si nous travaillions avec une base de données relationnelle, il n'y aurait pas de question, puisque chaque opération serait effectuée dans sa propre transaction. Core Data a également un moyen très pratique de résoudre ce problème - il s'agit d'une branche du contexte enfant. Mais malheureusement, pour une raison quelconque, cela est rarement utilisé. Voici un bon article sur ce sujet.

Héritage


Pour une raison que je ne comprends pas, il existe un très grand nombre de manuels et d'exemples où l'héritage pour Entity / NSManagedObject (tables) n'est utilisé en aucune façon. En attendant, c'est un outil très pratique. Si vous n'utilisez pas l'héritage, vous pouvez affecter des valeurs aux attributs (champs) uniquement via le mécanisme KVC, qui ne vérifie pas les noms et les types d'attributs, ce qui peut facilement entraîner des erreurs d'exécution.

La redéfinition de classe pour NSManagedObject est effectuée dans le concepteur de données de base:

image

Héritage et génération de code


Après avoir spécifié le nom de classe pour Entity, vous pouvez utiliser la génération de code et obtenir une classe avec du code prêt à l'emploi:

image

image

Si vous voulez voir le code généré automatiquement, mais ne voulez pas ajouter de fichiers au projet, vous pouvez utiliser une autre façon: définissez l'option "Codegen" pour Entity. Dans ce cas, le code doit être recherché dans ../ DerivedData / ...

image

Utilisez la génération de code pour créer des classes, les fautes de frappe dans les noms de variables peuvent entraîner des erreurs d'exécution.

Voici un code comme celui-ci:

 @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? } 

Dans swift, @NSManaged a la même signification que dynamique dans Objective C.
Core Data s'occupe lui-même de la réception des données (a des accesseurs internes) pour les attributs de ses classes. Si vous avez des champs de transit, vous devez ajouter des fonctions pour les calculer.

Les classes héritées de NSManagedObject (tables) n'avaient pas de constructeur «normal» avant IOS10, contrairement aux autres classes. Pour créer un objet de type Company, il a fallu écrire une construction plutôt maladroite en utilisant NSEntityDescription. Il existe maintenant une méthode d'initialisation plus pratique via le contexte (NSManagedObjectContext). Le code est ci-dessous. Notez l'avantage de l'héritage lors de l'attribution d'attributs par rapport au mécanisme KVC:

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

Espace de noms pour NSManagedObject


Un autre élément à mentionner est l'espace de noms.

image

Vous n'aurez aucune difficulté si vous travaillez sur ObjectiveC ou Swift. Habituellement, ce champ est correctement rempli par défaut. Mais dans les projets mixtes, cela peut vous surprendre que pour les classes en swift et ObjectiveC, vous ayez besoin de proposer différentes options. Dans Swift, le «Module» doit être rempli. Si ce champ n'est pas rempli, un préfixe avec le nom du projet sera ajouté au nom de la classe, ce qui provoquera une erreur d'exécution. Dans l'Objectif C, laissez le «Module» vide, sinon NSManagedObject ne sera pas trouvé lors de l'accès via le nom de la classe.

Liens entre objets


En principe, le sujet des relations est bien couvert, mais je veux me concentrer sur la façon d'ajouter des entités enfants au parent. Par conséquent, tout d'abord, un rappel rapide du mécanisme de création de liens. Prenons un exemple traditionnel, une entreprise est des employés, une relation un-à-plusieurs:

  • Créez une connexion de chaque côté (tableau)
  • Après cela, le champ Inverse devient disponible, il doit être rempli dans chaque tableau.

image

image

Apple insiste pour spécifier des relations inverses. Dans le même temps, l'inversion ne renforce pas la cohérence, mais aide Core Data à suivre les changements des deux côtés de la connexion, elle est importante pour la mise en cache et la mise à jour des informations.

Il est également important de spécifier correctement la règle de suppression. Une règle de suppression est une action qui sera effectuée avec cet objet lorsque l'objet parent est supprimé.

  • Cascade - suppression de tous les objets enfants, lorsque le parent est supprimé.
  • Refuser - interdiction de supprimer un parent s'il y a un enfant
  • Nullify - annuler la référence parent
  • Aucune action - aucune action spécifiée, il donnera un avertissement lors de la compilation

Dans cet exemple, lorsqu'une entreprise est supprimée, tous les employés seront supprimés (cascade). Lorsque vous supprimez un employé, le lien vers lui dans l'entreprise sera réinitialisé (pré-écran)

Façons d'ajouter des entités enfants au parent


1) La première méthode consiste à ajouter via NSSet. Par exemple, ajoutez 2 employés à l'entreprise:

 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 

Cette méthode est pratique pour initialiser l'objet ou remplir la base de données. Il y a une légère nuance. Si l'entreprise avait déjà des employés et que vous avez affecté un nouvel ensemble, les anciens employés réinitialiseront le lien vers l'entreprise, mais ils ne seront pas supprimés. Alternativement, vous pouvez obtenir une liste d'employés et travailler déjà avec cet ensemble.

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

2) Ajout d'objets enfants via l'ID parent

 if let employee = NSEntityDescription.insertNewObject(forEntityName: "Employee", into: moc) as? Employee { employee.firstName = "" employee.secondName = "" employee.company = company } 

La deuxième méthode est pratique lors de l'ajout ou de la modification d'un objet enfant dans
formulaire séparé.

3) Ajout d'objets enfants via des méthodes générées automatiquement

 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) } 

Par souci d'exhaustivité, il est utile de connaître cette méthode, mais d'une certaine manière, cela ne m'a pas été utile, et je supprime le code supplémentaire afin de ne pas encombrer le projet.

Requêtes de clause enfant


Dans Core Data, vous ne pouvez pas effectuer de requête arbitraire entre des données, comme nous pouvons le faire en SQL. Mais entre les objets dépendants, il est facile de suivre à l'aide d'un prédicat standard. Voici un exemple de requête qui sélectionne toutes les entreprises dans lesquelles il y a un employé avec le nom spécifié:

 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]() } 

L'appel de méthode dans le code ressemblera à ceci:

 //  ,    let companies = Company.getCompanyWithEmployee(name: "") 

N'utilisez pas de champs de transit dans les requêtes; leurs valeurs ne sont pas définies au moment de l'exécution de la requête. Aucune erreur ne se produira, mais le résultat sera incorrect.

Définition des attributs (champs)


Vous avez probablement remarqué que les attributs d'entité ont plusieurs options.
Avec facultatif, tout est clair d'après le nom.

L'option d'utilisation du type scalaire est apparue dans swif. Objective-C n'utilise pas de types scalaires pour les attributs, car ils ne peuvent pas être nuls. Toute tentative d'attribution d'une valeur scalaire à un attribut via KVC entraînera une erreur d'exécution. Cela montre clairement pourquoi les types d'attribut dans Core Data n'ont pas une correspondance stricte avec les types de langage. Dans les projets rapides et mixtes, des attributs de type scalaire peuvent être utilisés.

Les attributs de transit sont des champs calculés qui ne sont pas stockés dans la base de données. Ils peuvent être utilisés pour le cryptage. Ces attributs reçoivent des valeurs via un accesseur substitué, ou en affectant des primitives selon les besoins (par exemple, willSave et awakeFromFetch substitués).

Accesseurs d'attribut:


Si vous n'avez pas besoin d'utiliser des champs calculés, par exemple, pour effectuer le chiffrement ou autre chose, alors vous n'avez pas à penser à ce que sont les accessoires d'attribut. Pendant ce temps, les opérations d'obtention et d'affectation de valeurs aux attributs ne sont pas «atomiques». Pour comprendre ce que je veux dire, consultez le code ci-dessous:

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

Utilisez des primitives dans les événements NSManagedObject au lieu de l'affectation habituelle pour éviter le bouclage. Un exemple:

 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() } 

Si vous devez soudainement visser la fonction awakeFromFetch dans un projet, vous serez surpris que cela fonctionne très bizarrement, mais en fait, elle n'est pas appelée du tout lorsque vous exécutez la demande. Cela est dû au fait que Core Data dispose d'un mécanisme de mise en cache très intelligent, et si la sélection est déjà en mémoire (par exemple, parce que vous venez de remplir ce tableau), la méthode ne sera pas appelée. Cependant, mes expériences ont montré qu'en termes de valeurs calculées, vous pouvez compter en toute sécurité sur l'utilisation de awakeFromFetch, comme le dit la documentation d'Apple. Si pour les tests et le développement, vous devez forcer awakeFromFetch, ajoutez managedObjectContext.refreshAllObjects () avant la demande.

C’est tout.

Merci à tous ceux qui ont lu jusqu'à la fin.

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


All Articles