iOS (Swift): Core data fetching with relationships - ios

If I have a Person entity and a Book entity where a Person can have many Books.
final class Person: NSManagedObject {
#NSManaged public fileprivate(set) var name: String
#NSManaged public fileprivate(set) var books: Set<Book>
}
final class Book: NSManagedObject {
#NSManaged public fileprivate(set) var name: String
#NSManaged public fileprivate(set) var person: Person
static func add(bookNamed name: String, to person: Person) {
guard let context = person.managedObjectContext else { fatalError("Can not obtain managed object Context") }
let book = NSEntityDescription.insertNewObject(forEntityName: "Book", into: context) as Book
book.name = name
book.person = person
}
}
In some UIViewController, I then add a few Books to a Person:
let alex = NSEntityDescription.insertNewObjectForEntityForName("Person", inManagedObjectContext: context) as Person
Alex.name = "Alex"
Book.add("first book", to: alex)
Book.add("second book", to: alex)
Book.add("third book", to: alex)
and then a couple of Books to another Person
let john = NSEntityDescription.insertNewObjectForEntityForName("Person", inManagedObjectContext: context) as Person
john.name = "John"
Book.add("another first book", to: john)
Book.add("another second book", to: john)
I then reload my app and use a fetch request to obtain all the people (i.e. alex and john) in a table view. I then click on alex, which takes me to another view controller, which has an instance of Person that I assign as alex so that I can view the books associated to this instance.
Question Are the Books all loaded into memory even if I have 1000s? Or do I need to perform another fetch to get the Books belonging to alex if I want to display them all? I just want a bit of clarity on what is happening with relationships between two entities as it's slightly confused me recently.
Thanks for any help.

CoreData uses a proxy object model. When you execute a fetch request it transparently creates all directly accessible objects, as proxies. Since they're proxies and not the full object, they are not actually loaded from the data base at creation time, rather they will be loaded from the database when needed (when properties are actually referenced) Likewise, they can be unloaded at any time if they're unmodified and unused.
In this case, that means that your fetch request will create unloaded proxies for each Person. When you display the persons name in the first table view, the data for that Person will be loaded (and cached) in the proxy object. At that time, proxies for each Book referenced by the person will be created as an empty proxy object. When you subsequently select the book and display the details of the book, the actual Book data will be fetched (cached) from the database.
Note: all this is very dependent on the actual datastore in use and is only true of stores, such as sqlite, that allow partial loading. If using an XML or plist store the entire object graph is instantiated and loaded with no empty proxies.

Related

Database structure in Firebase Swift

I am trying to think of the best solution for storing user's data in firebase, considering scalability, queries etc...
For example: Each user can create a list of categories. I was thinking of storing it like so:
Categories (Collection)
- User ID (Document)
- Category ID
- title
- imageURL
I would like to access the categories collection, find a list of all user ID's and when accessing a user ID I find their list of all the categories they have created...
I am quite new to firebase...
Or should I create Categories(collections) - User ID (document) - Category Id (collection) - title etc (document) ? Just does not seem right....
I appreciate any assistance or tips!
Edit: This is how I am currently saving the data to Firebase from my IOS app:
Model:
struct Category: Codable, Identifiable {
#DocumentID var docId: String?
var id: String? = UUID().uuidString
var authorId: String
var title: String
var categoryImageUrl: String?
Save data function:
guard let userId = Auth.auth().currentUser?.uid else { return } // REMOVE
let db = Firestore.firestore() // REMOVE
let collectionRef = db.collection("Categories").document(userId)
do {
try collectionRef.setData(from: category)
}
catch {
print(error)
}
It's hard to give a comprehensive answer, but one thing to keep in mind is that the client-side SDKs for Firestore don't have an API to get a list of (sub)collection. So it's important that your application can know the collection names without that, either because they are hard-coded (most common) or because they are stored elsewhere (such as in the parent document).

Joining multiple relationships in swift

please help.
I have 3 NSManaged objects.
say Employee, Payslip, PayItem
Each Employee can have multiple Payslips, each Payslip can have multiple PayItems.
so the relationship is Employee <->> Payslip <<- PayItem
They are all set up as NSManagedOjects.
Then lets say I have 3 instances of each: (imagine I'm initialising each by adding this to the NSManagedObject class:
convenience init(context: NSManagedObjectContext)
{
let entity = NSEntityDescription.entity(forEntityName: <entity>, in: context)!
self.init(entity: entity, insertInto: context)'
}
Then I can declare.
var employee = Employee(context: context)
var payslip = Payslip(context: context)
var payItem = PayItem(context: context)
I can then:
employee.addToPayslip(payslip) //Using the function created for me by default.
But if I try:
payslip.payItem = payItem
I always get the error:
Failed to call designated initializer on NSManagedObject class
'PayItem'
To summarise, I'm trying to link Employee to a payslip, that is one to many, then a payslip to a payitem, that is one to many. Why am I having such a tough time?
So it turned out I had a function that was returning an uninitialised version of PayItem. It took a while to track down, but zapping this resolved my problem.

Adding An Item To An NSSet For A Core Data One To Many Relationship

I have a core data relationship where one entity holds many of another entity. As far as I am aware each instance of the many class is held inside an NSSet? inside the one class. (?)
My question is - what is the best way to add items to this set? I figure this must be a very common problem - but I cannot seem to find an easy method.
This is my attempt: (This is all taken from the one class)
static var timeSlotItems: NSSet? //The Set that holds the many?
...
static func saveTimeSlot(timeSlot: TimeSlot) { //TimeSlot is the many object
retrieveValues()
var timeSlotArray = Array(self.timeSlotItems!)
timeSlotArray.append(timeSlot)
var setTimeSlotItems = Set(timeSlotArray)
self.timeSlotItems = setTimeSlotItems // This is the error line
}
Where retrieveValues() just updates all the coreData values in the class.
TimeSlot is the many object which I want to add.
I get an error on the last line, the error is: "cannot invoke initializer for type Set<_> with an argument of list of type Array"
Am I conceptually wrong at all? Thanks!
Nowadays it is this easy...
For a to-many item named say "Reply", CoreData knows to add a call "addToReplys".
Hence...
p = one Post. your core data Post items have many core data Reply items.
for jr in yourJson {
r = convert jr to a core data Reply
p.addToReplys( r )
so it's just
p.addToReplys( r )
Full example
For one-to-many this is easy. Just use the reverse to-one relationship.
timeSlot.item = self
For many-to-many I use this convenience method:
// Support adding to many-to-many relationships
extension NSManagedObject {
func addObject(value: NSManagedObject, forKey key: String) {
let items = self.mutableSetValueForKey(key)
items.addObject(value)
}
func removeObject(value: NSManagedObject, forKey key: String) {
let items = self.mutableSetValueForKey(key)
items.removeObject(value)
}
}
which is used like this:
self.addObject(slot, forKey:"timeSlotItems")
You've declared both timeSlotItems and saveTimeSlot: as static, so I'm not sure what your intention is there. I suspect it's not what you need.
In the same way that Core Data automatically runtime-generates optimized accessors for attributes, it also generates accessors for relations.
You don't say what the name of the "one" side of the to-many relation is, but if I assume that it's something like Schedule, where Schedule has a to-many relation to TimeSlot called timeSlotItems, then Core Data will runtime-generate the following accessors for you:
class Schedule: NSManagedObject {
#NSManaged public var timeSlotItems: Set<TimeSlot>
#NSManaged public func addTimeSlotItemsObject(value: TimeSlot)
#NSManaged public func removeTimeSlotItemsObject(value: TimeSlot)
#NSManaged public func addTimeSlotItems(values: Set<TimeSlot>)
#NSManaged public func removeTimeSlotItems(values: Set<TimeSlot>)
}

Realm query Object property field by its property

I'm developing an application for iOS using Swift and chose Realm as a database solution for it. I asked one question about Realm and now I have another.
Suppose we have a schema like this:
class Person: Object {
dynamic var id: String = NSUUID().UUIDString
dynamic var name: String?
dynamic var cars: Car?
class Car: Object {
dynamic var name: String?
I have one class (Person) that contains any number of objects of another class (Car). Car that are "linked" with the Person has some properties in context of that Person (and they can be different for same Car for different Persons or for two similar Cars for one Person). Using List<...>() we can not store such properties for each Item, am I right?
If we use Car only for one Person and only once we can create another class that includes only additional properties for Cars and links them with ID of Person plus ID of Car. But it does't work if we have two similar Cars with different additional properties.
So, how I see the solution. Have one table (class) stores ID of Person, ID of one Car and additional properties for this Car. For another Car for the same Person it has the same Person ID, Car ID (same or not) and another additional properties for this instance of a Car.
There is a problem (and a question that I mean). Having that structure I want to query all Cars from that table with their additional properties that have Person ID equals to some_id. How should I do this? Or maybe what another structure (maybe using List<...>) I should have to achieve such kind of behavior?
What is FastList exactly ?
If you want Items to have a property of Lists collection.
You have to redefine your Realm model. something like this.
class Car:Object{
dynamic var createDate: NSDate = NSDate()
}
class Person:Object{
let cars = List<Car>()
}
and query by predicate like this
let realm = Realm()
var ownedCarsFilterByDate = realm.objects(Person).filter("ANY cars.createDate = '\(date)'")
Edited to updated question
Your solution is to create table class, which has 'Person' , 'Car' and 'Context Attribute'.
Your model would be like this
class PersonAndCarRelation:Object{
dynamic var person: Person?
dynamic var car: Car?
dynamic var contextAttribute = ""
}
and you can query all cars associated with person
let personID = "123456789"
let personAndCarArray = realm.objects(PersonAndCarRelation).filter("person.id == \(personID)")
for personAndCar in personAndCarArray{
let personName = personAndCar.person.name
let carName = personAndCar.car.name
let context = personAndCar.contextAttribute
println("I am \(personName). I have a \(carName) with \(context)")
}

Magical Record import (next step)

I've put next step in the title as this is not the same problem as my previous question with almost the exact same title.
I have a Person entity.
Person
--------
name - mappedKeyName: FullName
email - mappedKeyName: EmailAddress
personID - mappedKeyName: Id
--------
photos
And a Photo entity.
Photo
--------
image
createDate - mappedKeyName: Date
photoID - mappedKeyName: Id
--------
owner (type Person) - mappedKeyName: UserId - relatedByAttribute: personID
There are other objects that relate to Person too and the JSON for these comes as so...
{
ObjectId : blah,
Owner : {
Id : 12345asdfg,
FullName : Oliver,
EmailAddress : oliver#oliver.com
}
}
With this JSON my setup works with the import. Any person records that don't exist (with the Id) are created. And any that do exist are updated.
However, the photos JSON object comes like this...
{
Id : thisIsThePhotoID,
Date : today,
UserId : 12345asdfg
}
When the objects come down like this the Magical record import stops when it gets to the person import.
The code crashes at...
- (id) MR_relatedValueForRelationship:(NSRelationshipDescription *)relationshipInfo
{
NSString *lookupKey = [self MR_lookupKeyForRelationship:relationshipInfo];
return lookupKey ? [self valueForKeyPath:lookupKey] : nil; // it stops here.
}
The value of lookupKey is #"personID".
Printing out relationshipInfo at the breakpoint gives...
$6 = 0x1fd695e0 (<NSRelationshipDescription: 0x1fd695e0>),
name owner,
isOptional 0,
isTransient 0,
entity Photo,
renamingIdentifier owner,
validation predicates (),
warnings (),
versionHashModifier (null)
userInfo {
mappedKeyName = UserId;
relatedByAttribute = personID;
},
destination entity Person,
inverseRelationship photos,
minCount 1,
maxCount 1,
isOrdered 0,
deleteRule 1
I really have no idea why this isn't working. I don't get any sensible errors to report.
MagicalRecord cannot map the relationship automatically with this JSON format:
{
Id : thisIsThePhotoID,
Date : today,
UserId : 12345asdfg
}
In order for MagicalRecord to map the relationship to a Person object, it would have to be an object in the JSON as well, for example:
{
Id : thisIsThePhotoID,
Date : today,
User : {
UserId : 12345asdfg
}
}
This way MagicalRecord knows it's an object and it will do the appropriate lookup in your existing database for the Person record with the above ID and map the relationship.
So there are two issues with this, though. If you cannot change the JSON output you have to create a category class on Photo where you manually map the relationship yourself. I'll get to that after the second issue.
The second issue is that the above JSON format assumes you already have parsed the users and stored the records in your database. If you have not MagicalRecord will create a new Person record with the above ID, but since no other attributes exist on that object (notice the UserId key is the only attribute in the dictionary) it will be fairly empty and not include the name and email address. You can always extend your JSON (if you have that possibility) to include those attributes as well in the Person dictionary inside the Photo dictionary:
{
Id : thisIsThePhotoID,
Date : today,
User : {
UserId : 12345asdfg,
FullName : Oliver,
EmailAddress : oliver#oliver.com
}
}
The JSON payload is quite small so it doesn't hurt to do that if you can. Plus it will only create a new Person record if one doesn't exist in the database already.
And then for the manual mapping. If you cannot change the JSON to the above format you have to manually override the relationship mapping as the JSON is not prepared the way MagicalRecord does mapping.
Create a category class for Photo called Photo+Mapping.h/.m. I like to stick with +Mapping for these. Then the class should be Photo (Mapping) in the header and implementation file and you're good to go.
MagicalRecord has a number of instance methods available to override (see the latter part of this article on MagicalRecord importing written by the author of MagicalRecord), among them are import<;attributeName>;: and import<;relationshipName>;:. There are also a willImport:, didImport: and shouldImport: methods on the class itself which allows you to override any mapping.
For your case you can use import<;relationshipName>;: or shouldImport:. I took these two because one has a bit of a benefit depending on whether you have already mapped all your Person objects and they're available for relationship mapping on the Photo object.
Here are the examples of what you can do (you can choose to combine a few of them if you wish, it doesn't hurt to do so). A note here: ALWAYS use the current NSManagedObjectContext when overriding mapping (easily accessible with MagicalRecord through self.managedObjectContext) otherwise you will end up with context issues.
Be sure to import Person:
#import "Photo+Mapping.h"
#import "Person.h"
// Assuming you only want to import the Photo object if you already have a Person stored this is a great method to tell MagicalRecord whether to continue with importing or not
-(BOOL)shouldImport:(id)data {
Person *person = [Person findFirstByAttribute:data[#"UserId"] value:#"personID" inContext:self.managedObjectContext];
if (!person) {
// no Person object exists so don't import the Photo object - again this is up to you since you might want to create the record if not
return NO;
}
// you can set the relationship here (you might as well) or use the importPerson: method below (doing a second lookup, which is unnecessary at this point)
[self setPerson:person];
return YES;
}
// If you use this method you're doing the lookup to check whether a record exist when MagicalRecord is trying to map the Person relationship
-(void)importPerson:(id)data {
Person *person = [Person findFirstByAttribute:data[#"UserId"] value:#"personID" inContext:self.managedObjectContext];
if (!person) {
// if no Person record exists for the associated UserId, you should create one (or not - if you choose not to, it's wise to throw away this Photo object)
person = [Person createInContext:self.managedObjectContext];
[person setPersonID:data[#"UserId"]];
}
// set the relationship
[self setPerson:person];
}
// finally you can also use the following method when MagicalRecord is done mapping and get rid of the Photo object if the Person relationship is nil:
-(void)didImport:(id)data {
if (!self.person) {
[self deleteInContext:self.managedObjectContext];
}
}
Hope this helps! Let me know if you have any questions.

Resources