CloudKit - How to Save Record If Not Exists - ios

I am trying to make a Record Type that contains unique values, and would act as the target reference objects to another Record Type. For example, Record Type - Movies would contain unique list of movies submitted by users. And FavoriteMovies would contain a Users reference and a Movies reference. Users could select from a list of existing Movies, or add new ones to it.
The problem happens if I create a new Movies record, while another user creates a new record with the same name (after I retrieved the list of movies, but before I attempt to add a new one). The two new records are considered different records with different recordIDs. This means that once I saved the new one, there will be two instances of Movies with the save name.
I'm not able to find a way to perform a Save If Not Exists type operation to the Movies Record Type. I could do a save in the completionBlock of a query, but those two actions would not be an atomic transaction to guarantee uniqueness. As far as I know this is also the case with chaining CKQueryOperation with CKModifyRecordsOperation.
Is there a way to insert a record only if the value does not exists in a single transaction?

If I understood correctly your use case, you can make movieRecord.recordID.recordName the movie's name and use CKModifyRecordsOperation with savePolicy IfServerRecordUnchanged to effectively Save If Not Exists. It would then return an error that you can ignore if you try to save a record that already exists on the server:
let saveRecordsOperation = CKModifyRecordsOperation()
saveRecordsOperation.recordsToSave = [movieRecord]
saveRecordsOperation.savePolicy = .IfServerRecordUnchanged
With the savePolicy IfServerRecordUnchanged this operation will save a new Movie record if it doesn't exist yet on the server (Save If Not Exists) but will return the error below on any subsequent try to overwrite a Movie record that already exists on the server (provided it is not a newer modified version of a record that was fetched from the server):
<CKError 0x14d23980: "Server Record Changed" (14/2017); server message = "record to insert already exists">
You could deal with this conflict in the perRecordCompletionBlock but in your specific use case you can just do nothing about the conflict error so each Movie record will be the first saved record with that CKRecordID.

Related

CoreStore how to fetch or query object from unspecified dynamic object

enter code hereI read this guide which documented pretty good. I need to search through my database and get a record actually execute fetch or query.
Can I search through all records instead of specifying appropriate From clause.
For example
var undefinedObject = CoreStore.fetchAll(
From(GoThroughAllMyDataBaseEntities),
Where("%K == %#", "localId", "some string id")
)
print(undefinedObject.id) // as object will be undefined I need to figure out how to get id property from it.
Side note: all my entities are child objects of parent entity which has id property.
So let's say I have next entities in CoreData:
BaseEntity (which includes id)
Playlist
Song
in the code above I don't care which will be returned to me I just what to see one that matches this condition: Where("%K == %#", "localId", "some string id")
Also my localId property in each objects are very unique strings. They are extracted from NSManagedObjectID.
So there is no way have the same duplicated identifier localy in CoreData
If there is no way to do it, then I will need to loop all my Playlists and Songs records.
managedObjectContext.objectWithID(objectID) may not work thought in some cases as there is no guaranty that CoreData record has not been deleted and app recreated a copy of the same record, so physically a copy of record has another objectID address in CoreData, but still has localID property copied from other record.

Fetching CKRecordZone from a CKRecord

Scenario:
I have a CKRecord which I have fetched from the server. The record exists inside a custom zone for which I do not know the identifier and do not have a CKRecordZone object for.
I need to make a call to CKDatabase.perform(query:inZoneWith:completion:) to get the records in the database which are components of the root shared record (which requires such a call) however without having a CKRecordZoneID (from a CKRecordZone) I am forced to iterate through every CKRecordZone in the shared database and perform the query until a matching record is found.
In summary: I want to take a CKRecord and find the CKRecordZone it exists in. Is this possible? Or is my method flawed and can I perform a query without the CKRecordZoneID?.
To find the CKRecordZoneID of a given record, the recordID property is helpful:
(record).recordID.zoneID yields the CKRecordZoneID that the CKRecord exists in.

Core Data Different Object ID For First Time

I am using Entity's Object ID in order to uniquely identify local notifications and modify them. I observed that first time when I save my entity, it has following object ID:
<x-coredata:///Task/tE1C5A230-A419-42D5-AF78-3327A09D13BD2>
If I don't exit my application, and try to modify notification, object ID doesn't change and I can modify my notification.
Now, if I restart my app and try to access that entity again, it has different object ID:
<x-coredata://D6703834-ECB4-487B-84F8-330A215E16B7/Task/p13>
So I can't modify notification, as object ID for entity is different. Interesting thing is whenever I access that entity, Object ID remains same as the last one.
So my Question here is why Core data shows different object ID for the first time entity is created? When I try to access entity after opening app again for many times, the object ID (different than the first one) remains constant. I am curious to know why is it happening so?
Please note:
I know there are many posts on SO pointing out that using Object ID is not a reliable approach. Still I want to know reason that why two IDs are being shown.
the first OID is a temporary OID - a temporary id denotes objects that have not been saved yet. the 2nd id is a permanent one and is assigned to a MO AFTER it has been saved:
so...
var objectID = object.objectID
if objectID.temporaryID {
object.managedObjectContext.save() //try do catch left out
}
objectID = object.objectID
assert(objectID.temporaryID == false)

CloudKit: Preventing Duplicate Records

I am working through an app that pulls data from an external web service into a private CloudKit database. The app is a single user app, however I am running into a race condition that I am not sure how to avoid.
Every record in my external data has a unique identifier that I map to my CKRecord instances. The general app startup flow is:
Fetch current CKRecords for the relevant record type.
Fetch external records.
For every external record, if it doesn't exist in CloudKit, create it via batch create (modification operation).
Now, the issue is, if this process is kicked off on two of a user's devices simultaneously, since both the CK and external fetch is async, there is a strong possibility that I'll get duplicate records.
I know I can use zones to atomically commit all of my CKRecord instances, but I don't think that solves my issue because if all of these fetches happen at essential the same time, the save is not really the issue.
My questions are:
Does anyone know of a way to "lock" the private database for writes across all of a user's devices?
Alternatively, is there a way to enforce uniqueness on any CKRecord field?
Or, is there a way to use a custom value as the primary key, in that case I could use my external ID as the CK ID and allow the system to prevent duplicates itself.
Thanks for the help in advance!
Answers:
No, you cannot lock the private database
Cloudkit already enforces and assumes uniqueness of your record ID
You can make the record ID anything you like (in the non zone part of it).
Explanation:
Regarding your issue of duplication. If you are the one creating the record IDs (from the external records you mentioned for example) then at worst you should have one record over write the other with the same data if you have a race condition. I do not think that is an issue for the extreme case two devices kick off this process at the same time. Basically you logic of first fetching existing records and then modifying them seems sound to me.
Code:
//employeeID is a unique ID to identify an employee
let employeeID = "001"
//Remember the recordID needs to be unique within the same database.
//Assuming you have different record types, it is better to prefix the record name with the record type so that it is unique
let recordName = "Employee-\(employeeID)"
//If you are using a custom zone
let customZoneID = CKRecordZoneID(zoneName: "SomeCustomZone", ownerName: CKCurrentUserDefaultName)
let recordIDInCustomZone = CKRecordID(recordName: recordName, zoneID: customZoneID)
//If you are using the default zone
let recordIDInDefaultZone = CKRecordID(recordName: recordName)
I had similar issue of duplicates downloaded when I tried to read in a database of more than 100 records; the solution is found in the Apple's Atlas example which uses a Boolean to check if the last process finished before it launches the next. You find a block much like this...
#synchronized (self)
{
// Quickly returns if another loadNextBatch is running or we have the oldest post
if(self.isLoadingBatch || self.haveOldestPost) return;
else self.isLoadingBatch = YES;
}
Incidentally here the code to create your own record key.
CKRecordID *customID = [[CKRecordID alloc] initWithRecordName: [globalEOConfirmed returnEOKey:i]];
newrecord = [[CKRecord alloc] initWithRecordType:#"Blah" recordID:customID];

inserting data in many to many relationship in entity framework

I have two tables (users , messages) which there is many to many relationship for them.
i want to add new messages in message table and allocate massages to current users.
Also i insert data like below:
Message newMessage = Message.CreateMessage("MessageText", "DateTime");
newMessage.Users.Add(new User{..... });
context.SaveChange();
this code will execute a query which add a new user in users table while the users table has some specified users and i don't want to add new user but as i mentioned i want to add new messages in message table and allocate massages to current users.
how should i do that?
The problem is you are creating a new User object with an existing primary key. When SaveChanges() is called EF detects the changes of the entities its already tracking and new entities added. Since your new User object was not tracked by EF, it tries to insert it.
You need to explicitly tell EF that the created user is an existing one. To do that you need to attach it.
Message newMessage = Message.CreateMessage("MessageText", "DateTime");
var user = new User{ Id = "foo" };
context.Users.Attach(user);
newMessage.Users.Add(user);
context.SaveChange();

Resources