Do all attributes with a custom type in core-data have to be a relationship?
Asked Answered
F

2

2

This is a followup question from the comments in the following How to map nested complex JSON objects and save them to core data?.

Imagine that I already have this code for my app.

class Passenger{
    var name: String
    var number: String
    var image : UIImage
    // init method
}
class Trip {
    let tripNumber : Int
    let passenger : Passenger

    init(tripNumber: Int, passenger: Passenger) {
        self.tripNumber = tripNumber
        self.passenger = passenger
    }
}

Now I've decided to add persistence for my app. I just want to have a table of Trips. I want to show the passengers under trips, but don't need a table to query passengers directly. It's just a custom object/property of trip. Every time I access passenger it would be through Trips.

So is there a way that I can create a new subclass of NSManagedObject named 'TripEntity' and store my passengers — WITHOUT 1. creating another NSManagedObject subclass for 'Passenger' 2. Creating a relationship with an inverse relationship between Passenger and Trip? Simply put I just want it to be an attribute. Conceptually to me it's also just an attribute. It's not really a relationship...

Or is that once you're using Core-data then every custom type needs to be explicitly a subclass of NSManagedObject? Otherwise it won't get persisted. I'm guessing this also what object graph means. That your graph needs to be complete. Anything outside the graph is ignored...

I'm asking this because the JSON object that I actually want to store is gigantic and I'm trying to reduce the work needed to be done.

Febrifugal answered 31/12, 2018 at 17:20 Comment(3)
Could you clarify: 1) can the same Passenger object be assigned to more than one trip? If so, you will need to work out some way of uniquing them because if they are encoded in an attribute, each Trip object will have distinct Passengers. 2) can each trip have more than one passenger? If not, you could just add each of the Passenger properties as an attribute of Trip, and avoid the encode hassle. 3) How big are the UIImages - they might be better stored in the file system and with a filepath stored in CoreData.Crocoite
1) If I'm getting a list of trips, then yes. A Passenger object can be assigned to multiple trips across a period of time. That being said I don't understand the problem you're mentioning. Can you elaborate? 2) Yes. It can be more than one. 3) Good point.Febrifugal
NOTE: Make sure you also see the non-accepted answer as well. It's solution is better, but don't fully match the specificity of the questionFebrifugal
I
1

Any custom property needs to be something that can be represented as one of the Core Data property types. That includes obvious things like strings and numeric values. It also includes "binary", which is anything that can be transformed to/from NSData (Data in Swift).

From your description, the best approach is probably to

  1. Adopt NSCoding for your Passenger class.
  2. Make the passenger property a Core Data "transformable" type. (as an aside, should this be an array of passengers? You have a single related passenger, but your question describes it as if there's more than one).
  3. After doing #2, set the "custom class" field for the passenger property to be the name of your class-- Passenger.

If you do these two things, Core Data will automatically invoke the NSCoding methods to convert between your Passenger class and a binary blob that can be saved in Core Data.

Inquisition answered 31/12, 2018 at 17:34 Comment(2)
Thanks again Tom. After reading your first paragraph I finally understood why it's named transformable. That being said prior to this I did come across here and here. I didn't understand what those are exactly about...but don't I need to implement ValueTransformer?Febrifugal
nv. I found my answer here another answer from yourself :)Febrifugal
P
5

You can add passengers to one Trip entity but as the attribute types are restricted you have to use a transformable type which can be quite expensive to archive and unarchive the objects.

The most efficient way if the source data is JSON is to create Core Data entities for Passenger and Trip and add inverse relationships. Then make all NSManagedObject classes adopt Codable and add init(from decoder and encode(to encoder: methods to each class.

For example let's assume that the Trip class has a to-many relationship to Passenger it could look like

@NSManaged public var tripNumber: Int32
@NSManaged public var passengers: Set<Passenger>

and in the Passenger class there is an attribute trip

@NSManaged public var trip: Trip?

this is the required Decodable initializer in Trip. The decoder can decode arrays as well as sets.

private enum CodingKeys: String, CodingKey { case tripNumber, passengers }

public required convenience init(from decoder: Decoder) throws {
    guard let context = decoder.userInfo[.context] as? NSManagedObjectContext else { fatalError("Context Error") }
    let entity = NSEntityDescription.entity(forEntityName: "Trip", in: context)!
    self.init(entity: entity, insertInto: context)
    let values = try decoder.container(keyedBy: CodingKeys.self)
    tripNumber = try values.decode(Int32.self, forKey: .tripNumber)
    passengers = try values.decode(Set<Passenger>.self, forKey: .passengers)
    passengers.forEach{ $0.trip = self }
}

The inverse relationship is set via the forEach line.
You have to add an extension of JSONDecoder to be able to pass the current NSManagedObjectContext in the userInfo object.

The corresponding encoder is – pretty straightforward –

public func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(tripNumber, forKey: .tripNumber)
    try container.encode(passengers, forKey: .passengers)
}

NSManagedObject classes adopting Codable are very comfortable for pre-populating the database from JSON data or making JSON backups.

Pappas answered 31/12, 2018 at 17:51 Comment(8)
Thanks again Vadian. :| So you're basically suggesting that I do what I don't want to do. I suspect the amount of code for making in conform to NSCoding is also the same. OK. Also is passengers.forEach{ $0.trip = self } necessary? I thought it was that just by having an inverse relationship, it gets automatically set for you...Febrifugal
As I said you can't add a custom class or struct as a Core Data attribute, you have to use a transformable type which needs extra work to serialize and deserialize the objects. So when working with JSON you are going to deserialize the JSON and serialize it again to make it transformable compatible. That smells inefficient. In a custom initializer you have to establish one relationship. The inverse connection is made implicitly.Pappas
doesn't passengers = try values.decode(Set<Passenger>.self, forKey: .passengers) establish one relationship?Febrifugal
It does if trip will be set in the corresponding Passenger initializer. But a to-one relationship is more effort than a to-many.Pappas
Oh you mean if my Passenger type also had a property of Trip type? ie it was like this: class Passenger{ var name: String var number: String var image : UIImage var trip : Trip // init method } That makes sense!Febrifugal
Let us continue this discussion in chat.Febrifugal
Yes, for an inverse relationship you need attributes of the related types in both classes (directions).Pappas
While this answer is more complete, it offers a solution I was trying to avoid in the question. To just keep the readers not confused I'm accepting Tom's answer. I wish I could accept 2 answers.Febrifugal
I
1

Any custom property needs to be something that can be represented as one of the Core Data property types. That includes obvious things like strings and numeric values. It also includes "binary", which is anything that can be transformed to/from NSData (Data in Swift).

From your description, the best approach is probably to

  1. Adopt NSCoding for your Passenger class.
  2. Make the passenger property a Core Data "transformable" type. (as an aside, should this be an array of passengers? You have a single related passenger, but your question describes it as if there's more than one).
  3. After doing #2, set the "custom class" field for the passenger property to be the name of your class-- Passenger.

If you do these two things, Core Data will automatically invoke the NSCoding methods to convert between your Passenger class and a binary blob that can be saved in Core Data.

Inquisition answered 31/12, 2018 at 17:34 Comment(2)
Thanks again Tom. After reading your first paragraph I finally understood why it's named transformable. That being said prior to this I did come across here and here. I didn't understand what those are exactly about...but don't I need to implement ValueTransformer?Febrifugal
nv. I found my answer here another answer from yourself :)Febrifugal

© 2022 - 2024 — McMap. All rights reserved.