Search code examples
ioscore-datamany-to-manypreload

Best practice for preloading core data with many to many relationship


I am developing an IOS application which has core data and I have to preload data many to many relationship to the core data. I have three tables with supermarket, product and intermediate table called supermarketToproduct which stores the relationship between supermarkets to products. As I stated, there is many to many relationship between supermarket and product entities hence I needed to create an intermediate table to indicate the relationships. My question is that what is the best practice to preload JSON formatted data into my core data with many to many relationship. In Apples reference documents, it is stated that there is no need to create intermediate table as coredata creates one for us. However, How can I define many to many relationship while preloading the date without intermediate table? By the way, all the data is static so no need to insert new data into tables, I only need fetch the data and its related supermarket or product. Please note that joined table has two independent attributes called priceofProductforGivenMarket and primaryProductforGivenMarket Here is the JSON representation of my data, also it is the model that I would create in core data:

Supermarket:
name
location
phone number

Product:
name
price
company

supermarketToproduct:
 nameofsupermarket
 nameofproduct 
 priceofProductforGivenMarket
 primaryProductforGivenMarket

Solution

  • I would create three entities, Supermarket, Product and SupermaketProductDetails, with attributes and relationships as follows:

    Supermarket:
    Attributes: name, location, phone number
    Relationship (to many): productDetails
    
    Product:
    Attributes: name, price, company
    Relationship (to many): supermarketDetails
    
    SupermarketProductDetails:
    Attributes:  priceofProductforGivenMarket, primaryProductforGivenMarket
    Relationships: (to one) supermarket, (to one) product
    

    In Xcode, indicate that the "destination" for the productDetails relationship in the Supermarket entity is the SupermarketProductDetails entity, with inverse supermarket, likewise set the SupermarketProductDetails entity as the destination for the supermarketDetails in the Product entity, with inverse product.

    Then I would parse your JSON data for supermarkets first, creating Supermarket objects and setting the name, location and phone number but without entering anything for the products relationship. Likewise, parse the products JSON and create all the Products objects. Then I would parse the join table, and create the SupermarketProductDetails objects. Set the attributes based on your JSON data, and execute a fetch to get the Supermarket entity with the right name, likewise fetch the Product entity with the right name, and then set these relationships directly.

    EDIT: Assume that you parse each line in your join table into four NSStrings: nameofsupermarket, nameofproduct, priceofProductforGivenMarket and primaryProductforGivenMarket. Then for each line...

    // First fetch the correct Supermarket...
    NSFetchRequest *supermarketFetch = [NSFetchRequest fetchRequestWithEntityName:@"Supermarket"]
    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"name like %@",nameofsupermarket];
    supermarketFetch.predicate = predicate;
    NSError *error;
    NSArray *results = [context executeFetchRequest:supermarketFetch error:&error];
    // should really check for errors, and that we get one and only one supermarket
    NSLog(@"Supermarket fetch returned %i results",[results count]);  // should be only 1!
    mySupermarket = (NSManagedObject *)[results firstObject];
    if (![[mySupermarket valueForKey:@"name"] isEqualToString:nameofsupermarket]) {
        NSLog(@"Wrong supermarket fetched");
        }
    
    // Now fetch the product...
    NSFetchRequest *productFetch = [NSFetchRequest fetchRequestWithEntityName:@"Product"]
    predicate = [NSPredicate predicateWithFormat:@"name like %@",nameofproduct];
    productFetch.predicate = predicate;
    results = [context executeFetchRequest:productFetch error:&error];
    // should really check for errors, and that we get one and only one product
    NSLog(@"Product fetch returned %i results",[results count]);  // should be only 1!
    myProduct = (NSManagedObject *)[results firstObject];
    if (![[myProduct valueForKey:@"name"] isEqualToString:nameofproduct]) {
        NSLog(@"Wrong product fetched");
        }
    
    // Now create the SupermarketProductDetails entity:
    NSManagedObject *mySupermarketProductDetails = [NSEntityDescription insertNewObjectForEntityForName:@"SupermarketProductDetails" inManagedObjectContext:context];
    // set attributes...
    [mySupermarketProductDetails setValue:primaryProductForGivenMarket forKey:@"primaryProductForGivenMarket"];
    [mySupermarketProductDetails setValue:priceOfProductForGivenMarket forKey:@"priceOfProductForGivenMarket"];
    // set relationships...
    [mySupermarketProductDetails setValue:mySupermarket forKey:@"supermarket"];
    [mySupermarketProductDetails setValue:myProduct forKey:@"product"];
    [context save:&error];
    // should check for errors...
    

    Note that you only need to set one side of these relationships - CoreData updates the other side for you (i.e. it will add mySupermarketDetails to the set of supermarketDetails for myProduct, etc). Note also that the "value" for a (to-one) relationship (e.g. supermarket) is the destination object itself (e.g. mySupermarket); you don't use the name or any other key. Coredata is (hidden in the underlying sql tables) using unique objectIDs to do the linking up.

    (There are probably more efficient means of doing this, rather than doing two fetches for every SupermarketProductDetails entry, but this will work.)

    EDIT2: Note that the above assumes that your entities are all implemented as NSManagedObjects. If you have created separate subclasses for each entity, then you can simplify some of the above code. For example, valueForKey: and setValue:forKey: can be replaced by the equivalent property accessor methods using dot notation, eg.:

    [mySupermarketProductDetails setValue:primaryProductForGivenMarket forKey:@"primaryProductForGivenMarket"];
    

    would become:

    mySupermarketProductDetails.primaryProductForGivenMarket = primaryProductForGivenMarket;
    

    and

    [mySupermarket valueForKey:@"name"]
    

    can be replaced by:

    mySupermarket.name
    

    Likewise, objects should be created with the appropriate subclass rather than NSManagedObject. eg.

    NSManagedObject *mySupermarketProductDetails = [NSEntityDescription insertNewObjectForEntityForName:@"SupermarketProductDetails" inManagedObjectContext:context];
    

    would become

    SupermarketProductDetails *mySupermarketProductDetails = [NSEntityDescription insertNewObjectForEntityForName:@"SupermarketProductDetails" inManagedObjectContext:context];