realm-swift
realm-swift copied to clipboard
The same primaryKey will crash
I add an object, and I had added the same primarykey object, it will crash. the error info:
Terminating app due to uncaught exception 'RLMException', reason: 'Attempting to create an object of type 'Person' with an existing primary key value '0'.'
Realm framework version: 3.5.0
Xcode version: 9.3
iOS/OSX version: 11.3
Don't think that is a bug. Its an exception warning you to not reuse the same primary key.
Does not add the same primary key to update the data?So I want to update the data, I can use "realm.add(person, update:true)"
I would say Realm should at least throw an error instead of crashing the app. That's why we are performing add inside try block. This is really confusing for developers. Just look at the example below. We are calling add inside write transaction which can throw. But add method doesn't throw anything on error. It would be great if this could be unified.
try realm.write {
realm.add(object, update: false)
}
Maybe I'm missing something, but how is this an enhancement and not a bug? Coming from SQLite, one reason for using primary keys (or unique constraints) is enforcement of integrity.
But this enforcement should not crash the app. It should throw a catchable error.
Otherwise, I'm forced to check by myself if an object with a given primary key is found - querying or trying to load the object, which might be costly. And it's also not thread safe, because between the check and the insertion another thread may add.
Write transactions are blocking, so if you do the check within a write transaction, there's no danger that a background thread will have added the object in the meantime. Additionally, looking up an object by a primary key is not expensive - because Realm objects are loaded lazily, you are not going to copy the entire object in memory by looking it up.
Finally, while I agree that the error should be catch-able, I don't see how:
do {
try realm.add(...)
} catch {
??
}
is much better than
if realm.object(...) == nil {
realm.add(...)
}
Thanks. Based on the above, I agree that this issue can be categorized as an enhancement. But I think it should be clearly documented that Swift apps can't catch this error and it will crash the app.
The add() function is not marked as throws, and if you try to prefix it with try the compiler will warn you that it does not throw anything.
Trying to create an object which already exists is a precondition violation rather than an unpreventable runtime error, and so we follow the lead of the standard library in making it a fatal error.
@tgoyne you're right about add() not throwing, but my main point is that enforcing a db constraint is not something that should crash the app.
It's a matter of semantics.
@noamtamim I also tried work arounds and found that there is no way we can catch this. If we want to be safe from crash we need to check for the primary key existence in realm and handle it ourselves. I agree that it should be clearly documented that Swift apps can't catch this error.
Define programmer error versus an exceptional situation where a Swift error should be thrown. For me, a programmer error is a breach of programming contract whereas an exceptional situation is one where the program logic itself is correct but given the circumstances, the program cannot finish execution.
Like in the Swift docs, a user using a vending machine but the machine having no change is NOT a breach of contract. The logic itself is correct, but the circumstances do not support the execution. If the vending machine input states a precondition that the user should check whether the machine has change, then it would be a breach of contract--which, honestly, is lousy contract.
There is no contract about the primary key constraints. That is, no indication in the document that the precondition must be met. A user trying to insert an object that already has the same primary key, in my opinion, is NOT programmer error.
Comparing with an excerpt from Apple documentation:
A call to this function must balance a call to enter(). It is invalid to call it more times than enter(), which would result in a negative count.
Clearly states something is invalid.
@funct7 thank you for the very insightful explanation. For me, the main "proof" that this is not really meant to be a breach of contract is that Objective-C apps can catch this exception. The fact that Swift apps can't, is just an unfortunate side effect of the bridging mechanism.
Moreover, the only way for an app to check if the primary key is already used, is calling another function, which is a waste of CPU cycles and disk reads.
I wonder what's the analogue of a crash in the vending machine example. Maybe shouting on the user, "no soup for you, come back one year".
Our use of exceptions to report precondition failures in obj-c is mostly so that we can write tests which validate error handling. Swift does not expose a way to catch obj-c exceptions because idiomatic obj-c does not catch exceptions or throw exceptions which are intended to be caught, and instead uses NSError out parameters for errors which the caller can handle. Historically this was because it was basically impossible to write exception-safe obj-c code with manual reference counting, and ARC continues to not be exception-safe by default.
What to do when add() or create() is called with a primary key that already exists is controlled by the update parameter. The default of .error is perhaps not the correct choice, but both the fact that it's the default and what it does when encountering a duplicate primary key ("Throw an exception. This is the default when no policy is specified for add() or create().") are documented, so I'm somewhat confused by the claim that this is not part of the function's documented contract.
This is the default when no policy is specified for add() or create().") are documented, so I'm somewhat confused by the claim that this is not part of the function's documented contract.
Checked the documentation for UpdatePolicy. I have to admit, I assumed .error meant throwing an error. So yes, it is documented, and I didn't thoroughly check the documentation. My bad. (Not being sarcastic.)
That aside, I still think killing an app for an existing primary key is quite unexpected. I'm genuinely wondering if there are other database frameworks that work like that. (CoreData, Android Room/SQLite don't seem to work that way at least...)