2011-08-19 8 views
9

Ho un problema molto strano con relazioni inverse in Core Data e sono riuscito a ridurre il mio problema a un esempio minimo, a partire da un nuovo progetto in xcode basato sul modello di finestra con supporto per Core Data (cioè, c'è molto poco lì).Relazione inversa non impostata (nel gestore KVO)

Supponiamo di disporre di un modello Core Data con tre entità: Dipartimento, Dipendente e Repository (una sorta di entità che rappresenta alcune statistiche sul dipartimento). Per semplicità, abbiamo solo relazioni uno-a-uno:

DepartmentSummary Department  Employee 
--------------------------------------------------------- 
        employee <----> department 
department <----> summary 

Questo è tutto ciò che c'è nel modello. In application:didFinishLaunchingWithOptions: creiamo un impiegato e di un reparto e impostare KVO:

NSManagedObject* employee = 
[NSEntityDescription 
    insertNewObjectForEntityForName:@"Employee" 
    inManagedObjectContext:[self managedObjectContext]]; 
[employee addObserver:self forKeyPath:@"department" options:0 context:nil]; 

NSManagedObject* department = 
    [NSEntityDescription 
    insertNewObjectForEntityForName:@"Department" 
    inManagedObjectContext:[self managedObjectContext]]; 
[department setValue:employee forKey:@"employee"]; 

Lo scopo del gestore KVO è quello di creare una sintesi per il reparto non appena reparto del dipendente è impostato:

- (void) observeValueForKeyPath:(NSString *)keyPath 
         ofObject:(id)object 
         change:(NSDictionary *)change 
         context:(void *)context 
{ 
    [self createSummary:object]; 
} 

createSummary è semplice: si crea un nuovo oggetto sintesi e associa con il reparto, e poi controlla che il rapporto inverso dal dipartimento per l'oggetto di riepilogo è anche impostato:

- (void) createSummary:(NSManagedObject*)employee 
{ 
    NSManagedObject* department = [employee valueForKey:@"department"]; 
    NSManagedObject* summary = 
    [NSEntityDescription 
     insertNewObjectForEntityForName:@"DepartmentSummary" 
     inManagedObjectContext:[self managedObjectContext]]; 

    [summary setValue:department forKey:@"department"]; 

    NSAssert([department valueForKey:@"summary"] == summary, 
      @"Inverse relation not set"); 
} 

Questa affermazione ha esito negativo. In effetti, se il risultato della stampa gli oggetti grandi e Info reparto della sintesi è stato impostato, otteniamo

entity: DepartmentSummary; 
    id: ..DepartmentSummary/..AA14> ; 
    data: { 
    department = "..Department/..AA13>"; 
    } 

per la sintesi, come previsto, ma

entity: Department; 
    id: ..Department/..AA13> ; 
    data: { 
    employee = "..Employee/..AA12>"; 
    summary = nil; 
    } 

per il reparto (con un nil sommario). Se invece ritardiamo la chiamata a createSummary in modo che non viene eseguito fino alla prossima iterazione del runloop:

- (void) observeValueForKeyPath:(NSString *)keyPath 
         ofObject:(id)object 
         change:(NSDictionary *)change 
         context:(void *)context 
{ 
    [self performSelector:@selector(createSummary:) 
       withObject:object 
       afterDelay:0]; 
} 

allora tutto funziona come previsto.

Ritardare l'asserzione invece fa non aiuto: la relazione inversa realtà non c'è impostare nel grafico oggetto, anche se lo fa arrivare impostato nel database (se si dovesse salvare il database, e riavviare l'app, ora tutto ad un tratto appare la relazione inversa).

Si tratta di un bug nei dati principali? Questo comportamento documentato mi è sfuggito? Sto utilizzando i dati di base in modi che non erano previsti?

Nota che il gestore KVO viene chiamato mentre Core Data è (automaticamente) la fissazione di un (altro) inversa: abbiamo impostato manualmente il campo del reparto employee, Core Data imposta automaticamente campo department del dipendente, e che a sua volta innesca la Gestore KVO. Forse è troppo per gestire Core Data :) Infatti, quando impostiamo

[employee setValue:department forKey:@"department"]; 

invece, tutto funziona di nuovo come previsto.

Qualsiasi suggerimento sarebbe apprezzato.

+0

Cosa succede se si imposta immediatamente il riepilogo, ma si ritarda la * assertion * fino al prossimo runloop? – jtbandes

+0

Ottima domanda. Modificherò la domanda per rispondere - in sostanza, ritardare l'asserzione non aiuta. – edsko

+0

ciao, ho notato lo stesso, funzionava prima ... – RolandasR

risposta

4

Questo è un classico problema di Core Data. I documenti specificano esplicitamente:

Poiché Core Data si occupa della manutenzione della coerenza del grafico dell'oggetto per te, è sufficiente modificare una parte di una relazione e tutti gli altri aspetti sono gestiti per te.

Tuttavia, in pratica si tratta di una bugia dalla faccia calva, in quanto inaffidabile.

Le mie risposte alle vostre domande è quindi:

È questo un bug in Core Data?

SI.

Questo comportamento documentato mi è sfuggito?

NO.

Sto utilizzando i dati di base in modi che non erano previsti?

NO.

Hai già fornito la soluzione "corretta" al tuo problema, la stessa soluzione che utilizzo ogni volta che cambio i valori delle relazioni in ogni singola app di Core Data che creo. Per letteralmente centinaia di casi, il modello consigliato è:

[department setValue:employee forKey:@"employee"]; 
[employee setValue:department forKey:@"department"]; 

cioè, impostare la relazione inversa te stesso ogni volta che si cambia il rapporto.

Qualcuno può avere più luce su questo argomento, o un modulo di più canone per aggirare il problema, ma nella mia esperienza non c'è modo di garantire che una relazione sia attivamente disponibile a meno che non sia stabilita manualmente (come mostra il tuo problema). Ancora più importante, questa soluzione ha anche altri due vantaggi:

  1. Funziona il 100% del tempo.
  2. Rende il codice più leggibile.

L'ultimo punto è contro-intuitivo. Da un lato, sembra complicare il codice e renderlo più lungo aggiungendo le righe a quello che potrebbe essere, secondo i documenti, una breve chiamata di una sola riga. Ma nella mia esperienza ciò che fa è salvare un viaggio del programmatore nell'editor di Core Data per dare visivamente la caccia e confermare le relazioni tra modelli, il che è più prezioso in termini di tempo. È meglio essere chiari ed espliciti rispetto ad avere un modello mentale di ciò che dovrebbe accadere quando si cambia una relazione.

Vorrei anche suggerire l'aggiunta di una categoria semplice da NSManagedObject:

@interface NSManagedObject (inverse) 

- (void)setValue:(id)value forKey:(NSString *)key inverseKey:(NSString *)inverse; 

@end 

@implementation NSManagedObject (inverse) 

- (void)setValue:(id)value forKey:(NSString *)key inverseKey:(NSString *)inverse { 
    [self setValue:value forKey:key]; 
    [value setValue:self forKey:inverse]; 
} 

@end 

come in:

[department setValue:employee forKey:@"employee" inverse:@"department"]; 

ci sono alcuni casi per espandere su quella categoria, ma vorrei gestire, ad esempio, cancellazione in un modo diverso del tutto.

In breve:gestire tutte le proprie relazioni in modo esplicito ogni volta. Core Data non è affidabile in questo senso.

+0

Hmmm. Interessante. Ho trovato che è più che solo quando si impostano le relazioni però. Un problema simile si verifica quando si elimina un record; la relazione inversa potrebbe non essere impostata a zero. Lo gestisci anche manualmente? Sarei preoccupato che inizierò a perdere i casi. Per ora, mi sono assicurato di non cambiare mai il grafico dell'oggetto in un gestore KVO; Calcolo ciò che deve essere fatto e quindi lo programma per la prossima iterazione del runloop. Funziona, ma devo ammettere che questa esecuzione fuori ordine rende il codice più complicato. – edsko

+0

Sì, durante l'eliminazione, ho sempre impostato le relazioni su zero manualmente. Nella mia esperienza, Core Data pianifica in modo affidabile gli oggetti da eliminare in base alle regole dichiarate nei documenti, quindi non è un problema. Per chiarire, Core Data infatti fa tutto ciò che afferma, solo che fa certe operazioni "quando conveniente" piuttosto che "proprio ora", come si scopre nel codice. Se hai bisogno di una relazione disponibile al più presto, dovresti impostarla tu stesso. Se ne hai solo bisogno la prossima volta che il runloop imposta l'interfaccia utente, lascia che sia il sistema automatico a gestirlo. – SG1

+0

Inoltre, se fossi in te, tornerei a modificare le relazioni con i gestori KVO. Questo è assolutamente normale. Non vorrei * inventare il mio runtime per la propagazione delle modifiche al Core Data. La vera intuizione qui è capire che è necessario gestire le relazioni manualmente, non che è necessario "attendere" e quindi gestire le modifiche in seguito. – SG1

0

Che ne dici di salvare ManagedObjectContext subito dopo l'inserimento?

+0

Né il salvataggio del MOC né la chiamata di processPendingChanges: aiuta. – edsko