Search code examples
iosswiftcore-datarestkit

Connect one-to-many by foreign key using Restkit


I've got these models:

extension Notification {

    @NSManaged var pk: NSNumber?
    @NSManaged var subtitle: String?
    @NSManaged var title: String?
    @NSManaged var user: User?

}

extension User {
    // here's some attributes
    @NSManaged var pk: NSNumber?
    @NSManaged var notifications: NSSet?
    // and here's really some more..
}

A response for listing a user's notification could look like:

[
    {
        "pk": 2,
        "title": "Hi mate",
        "subtitle": "O hoy",
        "user": 2
    }
]

"user": 2 means that it is belong to the user with pk = 2.

How do I properly map this notification to the user? My attempt is:

let mapping = RKEntityMapping(forEntityForName: String(Notification), inManagedObjectStore: RKObjectManager.sharedManager().managedObjectStore)

mapping.identificationAttributes = [ "pk" ]

mapping.addAttributeMappingsFromDictionary([
    "pk": "pk",
    "title": "title",
    "subtitle": "subtitle",
])

mapping.addConnectionForRelationship("user", connectedBy: [
  "user.pk": "pk"
])

I can do

mapping.addConnectionForRelationship("user", connectedBy: [
  "pk"
])

but then it connects it to a user with the same pk as the Notification.

The error I get is:

Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Cannot connect relationship: invalid attributes given for source entity 'Notification': user.pk'

How can I achieve this?

Am I forced to add a transient property users in the notification and then map userPk to pk?

EDIT: I've got it working using transient properties now, and that's the only way?

mapping.identificationAttributes = [ "pk" ]

mapping.addAttributeMappingsFromDictionary([
  "pk": "pk",
  "title": "title",
  "subtitle": "subtitle",
  "user": "userPk"
])

mapping.addConnectionForRelationship("user", connectedBy: [
  "userPk": "pk"
])

userPk is now a transient property on Notification


Solution

  • As long as you only get the ID of the user, the yes, you need to be using the method addConnectionForRelationship and a transient property. However, if your backend would serve you the user object rather than the ID, you can use addPropertyMapping method on the RKEntityMapping. Then I would suggest a pattern similar to this:

    import Foundation
    import CoreData
    import RestKit
    
    protocol Mappable {
        static var entityName: String {get}
        static var identityAttribute: String? {get}
        static func mapping(managedObjectStore: RKManagedObjectStore) -> RKEntityMapping
        static func identificationAttributes() -> [String]?
        static func createPropertyMappings(managedObjectStore: RKManagedObjectStore, objectMapping: RKObjectMapping) -> [RKRelationshipMapping]?
        static func attributesDictionary() -> Dictionary<String, String>?
        static func requestDescriptor(managedObjectStore: RKManagedObjectStore) -> RKRequestDescriptor
    }
    
    class ManagedObject: NSManagedObject, Mappable {
    
        //MARK: - ManagedObjectProtocol Methods
        class func mapping(managedObjectStore: RKManagedObjectStore) -> RKEntityMapping {
            let mapping = RKEntityMapping(
                forEntityForName: entityName,
                inManagedObjectStore: managedObjectStore
            )
    
            if let identityAttribute = identityAttribute {
                mapping.identificationAttributes = [identityAttribute]
            }
    
            if let attributeMapping = attributes {
                mapping.addAttributeMappingsFromDictionary(attributeMapping)
            }
    
            if let propertyMappings = createPropertyMappings(managedObjectStore, objectMapping: mapping) {
                for propertyMapping in propertyMappings {
                    mapping.addPropertyMapping(propertyMapping)
                }
            }
    
            return mapping
        }
    
        class var entityName: String { 
            fatalError("must override") 
        }
    
        class var identityAttribute: String? {
            //override me
            return nil
        }
    
        class func attributesDictionary() -> Dictionary<String, String>? {
            //override me
            return nil
        }
    
        class func createPropertyMappings(managedObjectStore: RKManagedObjectStore, objectMapping: RKObjectMapping) -> [RKRelationshipMapping]? {
            //override me if needed
            return nil
        }
    
        class func requestDescriptor(managedObjectStore: RKManagedObjectStore) -> RKRequestDescriptor {
            let inversedMapping = mapping(managedObjectStore).inverseMapping()
            let descriptor = RKRequestDescriptor(mapping: inversedMapping, objectClass: self, rootKeyPath: nil, method: RKRequestMethod.Any)
            return descriptor
        }
    
    }
    
    class Notification: ManagedObject {
    
        //MARK: - ManagedObjectProtocol Methods
        class var entityName: String {
            return "Notification"
        }
    
        override class func attributesDictionary() -> Dictionary<String, String>? {
            let dictionary = [
                "pk": "pk",
                "title": "title",
                "subtitle": "subtitle"
            ]
            return dictionary
        }
    
        override class func createPropertyMappings(managedObjectStore: RKManagedObjectStore, objectMapping: RKObjectMapping) -> [RKRelationshipMapping]? {
            let bookmarksPropertyMapping = RKRelationshipMapping(
                fromKeyPath: "user",
                toKeyPath: "user",
                withMapping: User.mapping(managedObjectStore)
            )
    
            return [bookmarksPropertyMapping]
        }
    }
    
    class User: ManagedObject {
        //MARK: - ManagedObjectProtocol Methods
        class var entityName: String {
            return "User"
        }
    
        override class func attributesDictionary() -> Dictionary<String, String>? {
            let dictionary = [
                "pk": "pk"
            ]
            return dictionary
        }
    }