I am building a chat application.
I want to not have duplicates of my objects
I have mainly 3 objects: a channel, a message and an entity. Both the channel and the message have an entity (the channel can have two)
The channel has a primary key, while the message does not (the message has 2 identifiers - a local one and one from the server. when the one from the server arrives, I nullify the temp one. This all means that the Message cannot have a primary key.
The problem comes when trying to set a primary key to the Entity class. It doesn't let me, because if I try to add a message that has attached to it an Entity that already exists in the database (was added because a channel with that entity was added), it crashes.
Have a system that enables me to handle my case. Maybe a custom setter that I can override when adding an Entity to check if it exists already or not.
I got a lot of duplicated instances
Simple adds of objects
https://gist.github.com/danipralea/e6dc03acd3a8df0e4aeb331bec26d5c8
Realm version: 2.1.2
Xcode version: 8.2
iOS/OSX version: 9.0
Dependency manager + version: 1.2.0.beta.1
Thank you for reaching out, @danipralea. I'm going to look into the problem you're experiencing and find someone who will be able to help resolve it with you. In the meantime, we appreciate your patience. 馃樃
@istx25 could you try creating a sample Xcode project, pasting the code from @danipralea's gist, and attempting to reproduce the issue to see if we experience the same behavior? If so, engineering can take a closer look at it. If not, hopefully @danipralea can refine the sample to demonstrate the duplicate instances referred to here.
For sure, I'll post the Xcode project here at some point today.
@danipralea: Which dependency(ies) are Mappable, MSChannelContext and MappingDefaultIdentifer coming from?
Mappable is ObjectMapper, MSChannelContext is another object also descending from Realm's Object class and MappingDefaultIdentifer is just -1
@danipralea sorry for a delay with the reply.
I as can understand from the info you've sent both JSON representation of the Channel and the Message contain the JSON for Entity objects, so in this case you can try to check if there is an Entity object with the primary key specified in JSON and use existing one in your mapping function. Another option is to create a new Entity with .add(:update:true method. See more in docs.
@stel thank you for your reply. I do not add the Entity object manually. I never do. They get added whenever I add a Channel or a Message object. So I have no way of controlling how the Entity object gets added.
I use ObjectMapper for mapping. I don't see it having this option of checking if the Entity object is already in the database. It's actually pretty decoupled of any Realm functionality.
You should be able to handle this in map() function either in Channel or in Message class. I'm not sure if ObjectMapper handles this case automatically.
@stel I did that and receive the same error:
let transform = TransformOf<Entity, Any>(fromJSON: { (value: Any?) -> Entity? in
if let entityDictionary = value as? [String : Any] {
if let entity = Mapper<Entity>().map(JSONObject: entityDictionary) {
store.add(entity, update: true)
return entity
} else {
print("no entity")
}
}
return nil
}, toJSON: { (value: Entity?) -> String? in
return nil
})
sender <- (map["entity"], transform)
// And also tried
let transform = TransformOf<Entity, Any>(fromJSON: { (value: Any?) -> Entity? in
if let entityDictionary = value as? [String : Any], let identifier = entityDictionary["id"] as? Int {
print("identifier : \(identifier)")
if let existingEntity = store.entityWith(identifier: identifier) {
return existingEntity
} else {
if let entity = Mapper<Entity>().map(JSONObject: entityDictionary) {
print("entity : \(entity)")
store.add(entity, update: true)
return entity
} else {
print("no entity")
}
}
}
return nil
}, toJSON: { (value: Entity?) -> String? in
return nil
})
When I try to add a new Message object, I get an error saying that an entity with identifier = 2 already exists.
Hmm.. this code looks good to me, when do you get the error? Is store.add(entity, update: true) executed? You can set exceptions breakpoint in Xcode and check when the Entity is created the second time.
it's created when I add a message that has attached an existing entity (the current user basically)
Could you please post the code where you get this error and specify the exact line? Setting Exception Breakpoint in Xcode will help to identify that. Also make sure that you use this custom transform both in Message and Channel classes.
for now, it's not related to the Channel object. I just get one by adding a message with the current entity (the current user). Absolutely nothing fancy.
Unfortunately without any code sample or reproduction case I can't suggest anything :( Please use Xcode's debugger to find the exact place where duplicated object is created.
@stel This is what I'm using to add a message:
store.add(message: message)
// and the method extension is
func add(message: Message) {
do {
try write {
// Here we mark if a message is first in a day, so we can mark it as such in the interface
let messages = store.messages(forChannelId: message.channelId)
var isFirst = true
for m in messages {
if let mDate = m.createdAt, let messageDate = message.createdAt, mDate.sameDay(as: messageDate) {
isFirst = false
break
}
}
message.isFirstMessageOfTheDay = isFirst
add(message)
}
} catch let error {
print("error saving message : \(error)")
}
}
@danipralea to being able to help we need either an example Xcode project that reproduces your issue or at least a related code sample and the exact error message.
This code sample is not related to the previous one (https://github.com/realm/realm-cocoa/issues/4524#issuecomment-273902243), you don't call add(message:) method there at all.
Anyways, assuming store as a Realm instance, you need to call add(message, update: true) if there is an object with existing primary key.
the Message object does not have a primary key, only the Entity has one, that's why it's added without update:true
@danipralea did you set an exception breakpoint in Xcode? Which line in your code produces this error?
@stel thanks again for your reply.
The code snippet I posted 2 days ago generates the error. just the add message.
To give the context again:
I have 2 messages:
Message 1:
identifier = 1, entity has id = 1 (it's from another user than the current one
Message 2:
identifier = 2, entity has id = 2 (it's from me, cause my user has id=2)
If I try to add a new message, let's say
Message 3:
identifier = -1 (cause it's a temporary message yes, until I get a response from the server)
temporaryIdentifier = SOME_UDID
entity has id = 2
This is where it crashes, because entity with id =2 that was added by Message 2 already exists, and when I insert Message 3, it crashes. Because the entity with id = 2 already exists
Bringing attention back to this issue. cc @stel
Thank you both @istx25 and @stel
Sorry for a delay with reply @danipralea, but unfortunately I haven't received the info I requested.
I've made an example that shows how you need to handle primary keys in a described case, hope it helps ;)
class Entity: Object {
dynamic var identifier: Int = -1
override static func primaryKey() -> String? {
return "identifier"
}
}
class Message: Object {
dynamic var entity: Entity?
dynamic var identifier: Int = -1
}
...
// Create Message1 and Message2 with the corresponding Entities ...
let entity1 = Entity()
entity1.identifier = 1
let entity2 = Entity()
entity2.identifier = 2
let message1 = Message()
message1.identifier = 1
message1.entity = entity1
let message2 = Message()
message2.identifier = 2
message2.entity = entity2
// ... and save it to Realm
try! realm.write {
realm.add(message1)
realm.add(message2)
}
// Create Message3 and use existing Entity2
let message3 = Message()
message3.entity = realm.object(ofType: Entity.self, forPrimaryKey: 2)
try! realm.write {
realm.add(message3)
}
If you need to update Entity everytime you save a new message, you can use this approach instead:
try! realm.write {
let message3 = realm.create(Message.self)
message3.entity = realm.create(Entity.self, value: ["identifier": 2], update: true)
}
@stel I can't use a primaryKey for the message, because the temporary message does not have an identifier yet. only a temporaryIdentifier
@danipralea right, I've updated my example, however it's not related to your case, so just try to use the same approach with ObjectMapper.
@stel Thank you very much for your reply. does it work if the entity with id = 2 (my current user) has not been inserted yet? Cause that can be the case as well - where I open the app and load first a conversation with messages all except from me (the current user).
Do I need to make a check first and add it if it's not in the realm?
realm.object(ofType: Entity.self, forPrimaryKey: 2)
returns nil if no instance with the given primary key exists, while
realm.create(Entity.self, value: ["identifier": 2], update: true)
creates an object, or updates the existing one.
@stel It also happens for saving into the database:
*** Terminating app due to uncaught exception 'RLMException', reason: 'Can't create object with existing primary key value '2'.'
*** First throw call stack:
(
0 CoreFoundation 0x0000000113b36d4b __exceptionPreprocess + 171
1 libobjc.A.dylib 0x000000011359821e objc_exception_throw + 48
2 Realm 0x000000011103997e _ZL23createOrGetRowForObjectIZ19RLMAddObjectToRealmE3$_0EmRK12RLMClassInfoT_bPb + 334
3 Realm 0x0000000111038d65 RLMAddObjectToRealm + 677
4 Realm 0x0000000111000875 _ZL26RLMGetLinkedObjectForValueP8RLMRealmP8NSStringP11objc_objectm + 565
5 Realm 0x0000000111003d3b _ZZ13RLMDynamicSetP13RLMObjectBaseP11RLMPropertyP11objc_objectmENK3$_0clEv + 1403
6 Realm 0x0000000110ff84d7 _ZL13RLMWrapSetterIZ13RLMDynamicSetP13RLMObjectBaseP11RLMPropertyP11objc_objectmE3$_0EvS1_P8NSStringOT_ + 183
7 Realm 0x0000000110ff82c7 _Z13RLMDynamicSetP13RLMObjectBaseP11RLMPropertyP11objc_objectm + 183
8 Realm 0x0000000111039602 RLMAddObjectToRealm + 2882
It also happens for saving into the database
It does happen if you run this code for the second+ time when there are already entities with the specified primary keys. This is an expected behavior. That's why there is create(_, value:, update:).
@danipralea is this still unclear? Let us know if we can clarify this further.
@danipralea: Do you require anymore assistance?
I still have to implement the other suggestion of Dmitry. I will get back to you guys as soon as I do that.
Thank you very much for taking such care in the issue/issues
No worries. Just let us know when you get a chance. And we're happy to help. 鉂わ笍
I'm closing this since we haven't heard back in a while. @danipralea please reopen if you're still having trouble with this.