-
Notifications
You must be signed in to change notification settings - Fork 2.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support "Cascading/Inheritable" Writes #8650
Comments
➤ PM Bot commented: Jira ticket: RCOCOA-2409 |
Alternately, if there's something that's going to blow up about my approach above, I'd love to know that as well. There's a little extra overhead involved in capturing all the blocks, and you could certainly end up with a write transaction that has 700 billion operations in it if you abused this, but that's no different than abusing any other API. |
I am probably misunderstanding the intent here, so a question: this seems more relationship than subclass to me. Is the setup "Consider a tree of Object subclasses:" where each object is an actual subclass of the object above it?
so Version inherits properties from Job and Project and Client and Object? |
@Jaycyn No. Each of the tree levels is a direct subclass of |
Ah! Make way more sense then. Thanks for the clarification on that part.
What does 'underneath' mean in this use case - as in related objects? For example, is this how the
If so, when the client object is deleted, you want to cascade delete the relatedProject object as well. Correct? |
@Jaycyn that's correct. The client owns projects, which own jobs, which own versions. Any one level can be deleted directly and, when that happens, all lower levels need to be deleted as well. But because each level is complex, with many side-effects to clean up (e.g. jobs hold other objects that must be deleted), I have a dedicated function to delete each type. I want to re-use those functions when I delete higher-level objects. For example, when deleting a Client I would simply invoke my deleteProject function, which in turn would invoke deleteJob, and so forth. This lets me re-use the deletion code for each level. When the deletion functions are called as part of the "cascade", they will use the existing open write transaction. When I call the deletion function directly and there is no open write transaction, they'll open a new one. |
We've explored the cascade delete process a number of times but I don't think that's a single feature that could be added that would work across the board. In one of our larger projects we have something similar where removing an object should also remove corresponding objects as one should not exist without the other. These objects are complex with dozens of properties and relationships. For our case we simply pass the realm that's in a write 'state' to our function to clean up the parent object, before then deleting the parent object. I am sure you have worked through this but it wasn't really a lot of code so I thought I'd throw it out there. This is kinda like the alternatives you mentioned. Here are 4 sample objects where each rely on the existence of it's "parent" object. e.g. a
then the code to delete the client (and in turn delete all of the objects relying on that parent object) is
One of the points in the question was:
but doesn't doing it like the above avoid that issue altogether? Why code for opening and committing write transactions when the delete can be accomplished within a single write or does your use case prevent that. On that note, per your feature request, you could just use an extension and roll all the cascade delete code into that - that makes deleting a client a one-liner.
call with
|
The point of my suggestion is to let the Rationale:I suggested the API addition for two reasons:
|
There's two core reasons why we stubbornly require explicit write transactions rather than just letting you write at any time:
The approach you're taking is the sort of thing we're trying to discourage for the second reason. If you have two Jobs, it'd be perfectly natural to just write Ultimately though, this is all just recommendations from our perspective and not rules that you must follow or the Realm Police will arrest you. If you don't have trouble remembering that two adjacent calls to a delete function should be manually wrapped in a write block even though it'll work without it, then you should feel free to write the more elegant code, especially if having one of the deletions run but not the other isn't a meaningful correctness problem in your specific scenario and it's just some slightly suboptimal code. Our SwiftUI stuff even has a similar function, although that should not necessarily be taken as an endorsement of it being a good idea. |
@tgoyne I may not be explaining my approach clearly. Batching operations in a single write transaction is EXACTLY what I'm doing here. I'm trying to make the function flexible enough that it can either be called directly to delete a single object, OR it can be called during the deletion routine of a parent object and inherit that parent function's already-open write transaction. I'm explicitly minimizing the number of write transactions. I'm trying to do it ALL in one transaction. |
Here's an example of where I'm using this technique. When I delete This logic is encapsulated in Now, sometimes I need to just delete flags directly: the user clicked a "delete flag" UI command. Other times, (such as in the code pane on the left in my screenshot) I need to delete the flags as part of a larger operation where I'm modifying/deleting the objects that "own" the flag objects. I don't want to repeat the deletion logic; I want to be able to call Bottom Line:This "cascading write" approach is working well for me and I think it ought to be an official part of the SDK: |
Another Way To Think About ItRight now, if you call "You can't possibly write right now! We have to be PREPARED to write!" (I'm sure there's edge cases.) |
The reason for the behavior above is to ensure the durability aspect of ACID. Imagine the following code: func updateName() {
realm.write {
person.name = "Peter"
}
// We've committed a write - we should be confident this is persisted
}
func updateAddress() {
guard addressNeedsUpdate else {
fatalError("updateAddress was called when update is not needed")
}
realm.write {
person.address = "some new address"
}
}
// somewhere
realm.write {
updateName()
updateAddress()
} Since I'm going to close this issue as the approach here is similar to that in #2502 and in both cases, real support for nested transactions will require a fair amount of Core work, which is tracked in realm/realm-core#1149. |
@nirinchev That makes sense! To confirm: is there a point at which a single write transaction becomes too large? Obviously there's some point where we run out of RAM, etc. but is there a practical guideline for how many changes should be included in a single write before it's better to batch that write into several pieces? With my approach here, I can end up with 10,000+ changes in a single write. I'm unclear how that translates to sync changesets and whether there's a point where I might create a changeset that's too large to function performantly if I use these cascading writes. |
We don't have some very strict guidance here, but as a rule of thumb, you probably want to avoid making changes to more than a few thousand objects in a single transaction when using sync. I'm not sure how easy it would be to do with the cascading write approach short of adding some change tracking logic to it - perhaps if you're looping over objects at the top level, you could inject code that does commitWrite+beginWrite every x iterations. |
@nirinchev got it, thanks! Just to clarify: is there a difference between "changes" and "deletes"? The only time my app can potentially touch a huge swath of Objects in one transaction is if a user deletes a top-level model entity that has many descendent children, grandchildren, etc. (The root object in a branch of the OutlineView, essentially.) Do deletes count as "changes" for the purposes of this advice? |
Problem
Consider a tree of
Object
subclasses:Each object is complex, with many properties and relationships. The user can delete any object at any time and all descendent objects should be deleted as well.
Because each object's deletion flow must be carefully customized to cleanup all side-effects, we would logically have methods that specialize in each operation:
When we delete, say, a
Client
, we also need to delete all theProject
,Job
, andVersion
objects underneath that client. The problem is that we can't simply re-use these methods because we can't open a write transaction while we're inside another write transaction—we can't "inherit" the open write transaction.Solution
I'd like the ability to piggyback on an open write transaction, if there is one, and create a new transaction otherwise. Something like this:
This API would grant a lot of flexibility for situations where cascades are necessary.
Alternatives
There are two alternatives:
Repeat the deletion logic for each lower-level object in the deletion function for the higher-level objects. Disaster waiting to happen.
Manually re-implement what I put above, either with an extension on
Realm
or by sticking the same code around each write in the deletion functions for everything but top-level objects.How important is this improvement for you?
I would like to have it but have a workaround
Feature would mainly be used with
Local Database only
The text was updated successfully, but these errors were encountered: