JSON e Core Data no iPhone

93

Eu tenho um gráfico de objeto de dados principal (consistindo em duas entidades vinculadas por um relacionamento para muitos).

Eu estava curioso, como um desenvolvedor de iPhone relativamente inexperiente, se alguém poderia recomendar uma abordagem e uma implementação JSON adequada para o iPhone, que me permitiria:

  1. converter os registros de dados principais em uma string JSON (enquanto mantém o relacionamento entre as entidades); e

  2. converter a string JSON de volta em objetos de dados centrais (novamente preservando o relacionamento entre as entidades).

Eu procurei, sem sucesso, por um tutorial / amostra de código neste ponto, então qualquer ajuda seria recebida com gratidão.

Urizen
fonte
3
Para quem está procurando por iOS5, existe agora NSJSONSerialization developer.apple.com/library/mac/#documentation/Foundation/… stackoverflow.com/questions/6726899/nsjsonserialization-in-ios5
nicerobot
Eu sei que esta questão é um pouco antiga, mas criei uma biblioteca simples chamada OSReflectionKit , que permite serializar / desserializar objetos de / para JSON, usando NSJSONSerialization ou NSDictionary. Ele também oferece suporte a objetos de dados principais.
Alexandre OS

Respostas:

103

Primeiro, escolha uma biblioteca JSON para usar, eu pessoalmente gosto do TouchJSON, mas várias outras por aí também são muito boas. A parte complicada, embora não muito difícil, é converter seus objetos gerenciados em estruturas adequadas para a conversão. Escrevi isso bem rápido, então pode haver um ou dois erros :)

Os métodos que você chama são:

- (NSString*)jsonStructureFromManagedObjects:(NSArray*)managedObjects;
- (NSArray*)managedObjectsFromJSONStructure:(NSString*)json withManagedObjectContext:(NSManagedObjectContext*)moc;

E a implementação é a seguinte:

- (NSDictionary*)dataStructureFromManagedObject:(NSManagedObject*)managedObject
{
  NSDictionary *attributesByName = [[managedObject entity] attributesByName];
  NSDictionary *relationshipsByName = [[managedObject entity] relationshipsByName];
  NSMutableDictionary *valuesDictionary = [[managedObject dictionaryWithValuesForKeys:[attributesByName allKeys]] mutableCopy];
  [valuesDictionary setObject:[[managedObject entity] name] forKey:@"ManagedObjectName"];
  for (NSString *relationshipName in [relationshipsByName allKeys]) {
    NSRelationshipDescription *description = [[[managedObject entity] relationshipsByName] objectForKey:relationshipName];
    if (![description isToMany]) {
      NSManagedObject *relationshipObject = [managedObject valueForKey:relationshipName];
      [valuesDictionary setObject:[self dataStructureForManagedObject:relationshipObject] forKey:relationshipName];
      continue;
    }
    NSSet *relationshipObjects = [managedObject objectForKey:relationshipName];
    NSMutableArray *relationshipArray = [[NSMutableArray alloc] init];
    for (NSManagedObject *relationshipObject in relationshipObjects) {
      [relationshipArray addObject:[self dataStructureForManagedObject:relationshipObject]];
    }
    [valuesDictionary setObject:relationshipArray forKey:relationshipName];
  }
  return [valuesDictionary autorelease];
}

- (NSArray*)dataStructuresFromManagedObjects:(NSArray*)managedObjects
{
  NSMutableArray *dataArray = [[NSMutableArray alloc] init];
  for (NSManagedObject *managedObject in managedObjects) {
    [dataArray addObject:[self dataStructureForManagedObject:managedObject]];
  }
  return [dataArray autorelease];
}

- (NSString*)jsonStructureFromManagedObjects:(NSArray*)managedObjects
{
  NSArray *objectsArray = [self dataStructuresFromManagedObjects:managedObjects];
  NSString *jsonString = [[CJSONSerializer serializer] serializeArray:objectsArray];
  return jsonString;
}

- (NSManagedObject*)managedObjectFromStructure:(NSDictionary*)structureDictionary withManagedObjectContext:(NSManagedObjectContext*)moc
{
  NSString *objectName = [structureDictionary objectForKey:@"ManagedObjectName"];
  NSManagedObject *managedObject = [NSEntityDescription insertNewObjectForEntityForName:objectName inManagedObjectContext:moc];
  [managedObject setValuesForKeysWithDictionary:structureDictionary];

  for (NSString *relationshipName in [[[managedObject entity] relationshipsByName] allKeys]) {
    NSRelationshipDescription *description = [relationshipsByName objectForKey:relationshipName];
    if (![description isToMany]) {
      NSDictionary *childStructureDictionary = [structureDictionary objectForKey:relationshipName];
      NSManagedObject *childObject = [self managedObjectFromStructure:childStructureDictionary withManagedObjectContext:moc];
      [managedObject setObject:childObject forKey:relationshipName];
      continue;
    }
    NSMutableSet *relationshipSet = [managedObject mutableSetForKey:relationshipName];
    NSArray *relationshipArray = [structureDictionary objectForKey:relationshipName];
    for (NSDictionary *childStructureDictionary in relationshipArray) {
      NSManagedObject *childObject = [self managedObjectFromStructure:childStructureDictionary withManagedObjectContext:moc];
      [relationshipSet addObject:childObject];
    }
  }
  return managedObject;
}

- (NSArray*)managedObjectsFromJSONStructure:(NSString*)json withManagedObjectContext:(NSManagedObjectContext*)moc
{
  NSError *error = nil;
  NSArray *structureArray = [[CJSONDeserializer deserializer] deserializeAsArray:json error:&error];
  NSAssert2(error == nil, @"Failed to deserialize\n%@\n%@", [error localizedDescription], json);
  NSMutableArray *objectArray = [[NSMutableArray alloc] init];
  for (NSDictionary *structureDictionary in structureArray) {
    [objectArray addObject:[self managedObjectFromStructure:structureDictionary withManagedObjectContext:moc]];
  }
  return [objectArray autorelease];
}

Agora, isso é recursivo, então você pode facilmente acabar traduzindo todo o armazenamento persistente se não for cuidadoso. Observe seus relacionamentos e certifique-se de que eles apenas vão "para baixo" na árvore de objetos, de modo que você obtenha apenas os objetos que deseja traduzir.

Marcus S. Zarra
fonte
Obrigado novamente por outra excelente resposta e por seu livro muito útil! :)
Urizen
2
Oi Marcus. Acabei de experimentar o código acima (com algumas pequenas alterações para fazê-lo compilar e a execução parece continuar indefinidamente até o aplicativo travar). Desculpe incomodá-lo, mas estou curioso para saber se você poderia me indicar a direção certa para resolver este problema. Parece acontecer com a recursão no método datastructureFromManagedObject ...
Urizen
1
Depende da sua estrutura de dados. Se o seu modelo produzir um loop, ele funcionará para sempre. Revise seu modelo de dados e certifique-se de que é um design de árvore ou coloque paradas lógicas no código recursivo para evitar loop.
Marcus S. Zarra
1
Você realmente tentou executar este código? Existem tantos erros. dataStructureForManagedObject nem mesmo existe. Achei que pudesse ser apenas um erro de digitação, mas se você alterá-lo para dataStructureFromManagedObject, a coisa toda vai se repetir indefinidamente entre pares de relacionamento. Estou faltando algum código extra aqui?
Chris Mitchelmore
1
Este exemplo de código foi escrito no navegador há dois anos. A intenção era inspirar não ser copiar e colar. Quanto a um loop infinito, isso significa que você tem um loop em seu modelo e, então, precisa adicionar uma lógica específica do modelo ao seu aplicativo para interromper o ciclo. Existem várias maneiras de fazer isso que não estão neste exemplo.
Marcus S. Zarra
12

Eu só queria apontar um pequeno erro de digitação, que fez com que o código travasse, e espero que isso economize alguns minutos.

- (NSArray*)dataStructuresFromManagedObjects:(NSArray*)managedObjects {

    NSMutableArray *dataArray = [[NSArray alloc] init];
    for (NSManagedObject *managedObject in managedObjects) {
        [dataArray addObject:[self dataStructureFromManagedObject:managedObject]];
    }
    return [dataArray autorelease];
}

o NSMutableArray *dataArray = [[NSArray alloc] init]; // This should be NSMutableArray

realmente deveria ser NSMutableArray *dataArray = [[NSMutableArray alloc] init];

isso é tudo.

obrigado

creativeKoder
fonte
10

Sincronizando Core Data with Rails é uma apresentação detalhada que inclui código de amostra para serializar / desserializar seus objetos Core Data para / de JSON (pule para o slide 55 para a parte Core Data). Seu código de amostra assume um modelo bastante simples, sem relacionamentos, embora eu ache que seria muito fácil de estender.

A apresentação também apresenta alguns detalhes sobre como manter seu modelo de dados principais em sincronia com um aplicativo da web baseado em REST, com ponteiros para algumas bibliotecas úteis, incluindo ObjectiveResource e ASIHTTPRequest . Não tenho certeza se é isso que você está tentando fazer, mas vale a pena dar uma olhada até mesmo no código de Core Data.

Christopher Pickslay
fonte
7

Se você tiver um NSDateem seu objeto gerenciado, conforme mencionado acima em um dos comentários, terá problemas para serializar o objeto que contém o NSDate. Uma solução simples é adicionar um JSONDataRepresentationmétodo para NSDateusar categorias objetivas-c.

Adicione estes dois arquivos ao seu projeto:

NSdate.h:

#import <Foundation/Foundation.h>

@interface NSDate (jsondatarepresentation) 

- (NSData*) JSONDataRepresentation;

@end

NSDate.m:

#import "NSDate.h"

@implementation NSDate (jsondatarepresentation)

- (NSData*) JSONDataRepresentation {
    return [[[NSNumber numberWithDouble:[self timeIntervalSince1970]] stringValue] dataUsingEncoding:NSUTF8StringEncoding];
}

@end
Joshaidan
fonte
2

Me deparei com este post que funciona muito bem.

http://touchalicious.com/blog/2009/10/25/turn-core-data-models-into-json.html

Como isso é recursivo, os relacionamentos muitos para muitos continuarão se repetindo entre si. Para evitar isso, adicionei uma chave "isExportable" ao dicionário de informações do usuário dos relacionamentos em meu modelo de dados principais. Você pode então verificar essa chave e optar por não percorrer relacionamentos sem ela.

insira a descrição da imagem aqui

if ([property isKindOfClass:[NSRelationshipDescription class]])
    {
        NSRelationshipDescription *relationshipDescription = (NSRelationshipDescription *)property;

        if ([[[relationshipDescription userInfo] objectForKey:@"isExportable"] boolValue] == YES)
        {
            NSString *name = [relationshipDescription name];

            if ([relationshipDescription isToMany])
            {
                NSMutableArray *arr = [properties valueForKey:name];
                if (!arr)
                {
                    arr = [[NSMutableArray alloc] init];
                    [properties setValue:arr forKey:name];
                }

                for (NSManagedObject *o in [self mutableSetValueForKey:name])
                {
                    [arr addObject:[o propertiesDictionary]];
                }
            }
            else
            {
                NSManagedObject *o = [self valueForKey:name];
                [properties setValue:[o propertiesDictionary] forKey:name];
            }
        }
    }
}
Brandon Schlenker
fonte
2

Só pensei em postar uma atualização rápida para esta questão. Segui as respostas de Marcus e Brandon e vim com isso para exportação JSON (ainda usa TouchJSON):

- (NSData*)jsonStructureFromManagedObjects:(NSArray*)managedObjects
{
    NSArray *objectsArray = [self dataStructuresFromManagedObjects:managedObjects];
    NSData *jsonData      = [[CJSONSerializer serializer] serializeArray:objectsArray error:nil];
    return jsonData;
}

- (NSArray*)dataStructuresFromManagedObjects:(NSArray*)managedObjects
{
    NSMutableArray *dataArray = [[NSMutableArray alloc] init];
    for (NSManagedObject *managedObject in managedObjects) {
        [dataArray addObject:[self dataStructureFromManagedObject:managedObject]];
    }
    return dataArray;
}

- (NSDictionary*)dataStructureFromManagedObject:(NSManagedObject*)managedObject
{
    NSDictionary *attributesByName        = [[managedObject entity] attributesByName];
    NSDictionary *relationshipsByName     = [[managedObject entity] relationshipsByName];
    NSMutableDictionary *valuesDictionary = [[managedObject dictionaryWithValuesForKeys:[attributesByName allKeys]] mutableCopy];
    [valuesDictionary setObject:[[managedObject entity] name] forKey:@"ManagedObjectName"];

    for (NSString *relationshipName in [relationshipsByName allKeys]) {

        NSRelationshipDescription *description = [[[managedObject entity] relationshipsByName] objectForKey:relationshipName];

        if ([[[description userInfo] objectForKey:@"isExportable"] boolValue] == YES) {

            if (![description isToMany]) {
                NSManagedObject *relationshipObject = [managedObject valueForKey:relationshipName];
                if (relationshipObject) {
                    [valuesDictionary setObject:[self dataStructureFromManagedObject:relationshipObject] forKey:relationshipName];
                }

                continue;
            }

            NSSet *relationshipObjects        = [managedObject valueForKey:relationshipName];
            NSMutableArray *relationshipArray = [[NSMutableArray alloc] init];

            for (NSManagedObject *relationshipObject in relationshipObjects) {
                [relationshipArray addObject:[self dataStructureFromManagedObject:relationshipObject]];
            }

            [valuesDictionary setObject:relationshipArray forKey:relationshipName];

        }

    }
    return valuesDictionary;
}

Não consegui fazer a importação funcionar, talvez isso tenha algo a ver com o fato de que estou usando Magical Record, não tenho certeza, então estou apenas fazendo um loop através do fluxo JSON de entrada e criando objetos manualmente ...

Carl Taylor
fonte
1

Marcus S. Zarra me inspirou a trazer a ideia recursiva para uma versão funcional. Nesta versão, você não precisa definir uma chave no CoreData e pode recortar e colá-la em seu projeto :-)

// MARK: - encoding and decoding CoreData entity to dictionary

func dataStructureFromManagedObject( managedObject:NSManagedObject?, parentEntity: NSEntityDescription? = nil) -> NSMutableDictionary {
    if (managedObject != nil) {
        var attributesByName: NSDictionary = managedObject!.entity.attributesByName
        var relationshipsByName: NSDictionary  = managedObject!.entity.relationshipsByName
        var valuesImmutableDictionary: NSDictionary = managedObject!.dictionaryWithValuesForKeys( attributesByName.allKeys)
        var valuesDictionary: NSMutableDictionary = valuesImmutableDictionary.mutableCopy() as NSMutableDictionary
        valuesDictionary.setObject( managedObject!.entity.name!, forKey: "ManagedObjectName")
        for relationshipNameObject in relationshipsByName.allKeys {
            var relationshipName: NSString = relationshipNameObject as  NSString
            var relationshipDescription: NSRelationshipDescription? = relationshipsByName.objectForKey( relationshipName) as? NSRelationshipDescription
            if !relationshipDescription!.toMany {
                // ono to one
                if parentEntity == nil || (relationshipDescription! as NSRelationshipDescription).destinationEntity != parentEntity! {
                    // no parent or relationship is "downward" -> object for relationship must be added
                    var relationshipObject: NSManagedObject? = managedObject!.valueForKey( relationshipName) as? NSManagedObject
                    var relationshipObjectDictionary: NSMutableDictionary = self.dataStructureFromManagedObject( relationshipObject, parentEntity: managedObject?.entity)
                    valuesDictionary.setObject( relationshipObjectDictionary, forKey: relationshipName)
                } else {
                    // relationship is "upward" -> nothing to do
                }
            } else {
                // one to many -> all objects must be added
                var relationshipObjects: NSSet = managedObject!.mutableSetValueForKey( relationshipName)
                var relationshipArray:NSMutableArray = []
                for relationshipObjectRaw in relationshipObjects {
                    var relationshipObject:NSManagedObject? = relationshipObjectRaw as? NSManagedObject
                    if relationshipObject != nil && !relationshipObject!.entity.isKindOfEntity( managedObject!.entity) {
                        relationshipArray.addObject(self.dataStructureFromManagedObject( relationshipObject, parentEntity: managedObject?.entity))
                    }
                }
                valuesDictionary.setObject( relationshipArray, forKey: relationshipName)
            }
        }
        return valuesDictionary
    } else {
        return NSMutableDictionary()
    }
}

func managedObjectFromStructure( structureDictionary: NSDictionary, moc: NSManagedObjectContext, parentObject: NSManagedObject? = nil) -> NSManagedObject {
    if structureDictionary.count > 0 {
        var objectName:NSString = structureDictionary.objectForKey( "ManagedObjectName") as NSString
        var managedObject:NSManagedObject = NSEntityDescription.insertNewObjectForEntityForName( objectName, inManagedObjectContext: moc) as NSManagedObject
        var relationshipsByName: NSDictionary  = managedObject.entity.relationshipsByName
        var realObjectStructure:NSMutableDictionary = structureDictionary.mutableCopy() as NSMutableDictionary
        realObjectStructure.removeObjectForKey( "ManagedObjectName")
        for key in realObjectStructure.allKeys {
            // search for "ManagedObjectName" relationship entrys and delete them before filling the managedObject from this structure
            for relationshipName in relationshipsByName.allKeys {
                if relationshipName as NSString == key as NSString {
                    realObjectStructure.removeObjectForKey( key)
                }
            }
        }
        managedObject.setValuesForKeysWithDictionary( realObjectStructure)
        // the main object with attributes is created. Now care about the relationships
        for relationshipName in managedObject.entity.relationshipsByName.keys {
            var description:NSRelationshipDescription = relationshipsByName.objectForKey( relationshipName) as NSRelationshipDescription
            if !description.toMany {
                // to one relationship
                if parentObject == nil || description.destinationEntity != parentObject!.entity {
                    // no parent or relationship is "downward" -> recurse structure to add
                    var childStructureDictionary:NSDictionary = structureDictionary.objectForKey( relationshipName) as NSDictionary
                    if childStructureDictionary.count > 0 {
                        // dictionary not empty -> object must be created and added
                        var childObject:NSManagedObject? = self.managedObjectFromStructure( childStructureDictionary, moc: moc, parentObject: managedObject)
                        // validateForUpdate
                        var error:NSError?
                        if !managedObject.validateForUpdate( &error) {
                            println("Error: Object not in valid state for update!!! -> \(error)")
                        } else {
                            managedObject.setValue( childObject, forKey: relationshipName as NSString)
                        }
                    } else {
                        // relationship is "upward" -> nothing to do
                    }
                }
            } else {
                // to many relationship
                var relationshipSet:NSMutableSet = managedObject.mutableSetValueForKey( relationshipName as NSString)
                var relationshipArray:NSArray = structureDictionary.objectForKey( relationshipName as NSString) as NSArray
                for childStructureDictionary in relationshipArray {
                    if childStructureDictionary.count > 0 {
                        // dictionary not empty -> object must be created and added
                        var childObject:NSManagedObject = self.managedObjectFromStructure( childStructureDictionary as NSDictionary, moc: moc, parentObject: managedObject)
                        // validateForUpdate
                        var error:NSError?
                        if !managedObject.validateForUpdate( &error) {
                            println( "Error: Object not in valid state for update!!! -> \(error)")
                        } else {
                            relationshipSet.addObject( childObject)
                        }
                    } else {
                        // no object was behind the relationship -> nothing to do
                    }
                }
                // save set
                managedObject.setValue( relationshipSet, forKey: relationshipName as NSString)
            }
        }
        // final check validateForUpdate
        var error:NSError?
        if !managedObject.validateForUpdate( &error) {
            println( "Error: Object not in valid state for update although all previous check are passed!!! -> \(error)")
        }
        return managedObject
    } else {
        println( "Error: structure for object was empty. this should not happen at this point")
        var objectName:NSString = structureDictionary.objectForKey( "ManagedObjectName") as NSString
        var managedObject:NSManagedObject = NSEntityDescription.insertNewObjectForEntityForName( objectName, inManagedObjectContext: moc) as NSManagedObject
        return managedObject
    }
}

func dataStructuresFromManagedObjects( managedObjects: NSArray) -> NSArray {
    var dataArray:NSMutableArray = []
    for managedObject in managedObjects {
        dataArray.addObject( self.dataStructureFromManagedObject(managedObject as? NSManagedObject))
    }
    return dataArray
}

A chave aqui é passar a entidade pai como argumento para a recursão, para que possamos decidir qual relacionamento devemos preencher com os dados. Portanto, as duas funções: dataStructureFromManagedObjecte managedObjectFromStructurepodem codificar e decodificar qualquer objeto de entidade de CoreData em um dicionário e de volta em um objeto.

MPajak
fonte