realm / realm-java

Realm is a mobile database: a replacement for SQLite & ORMs
http://realm.io
Apache License 2.0
11.47k stars 1.75k forks source link

"Cascade delete" implementation #2717

Closed laindow closed 8 years ago

laindow commented 8 years ago

Hello guys

Goal

It would be great to have "cascade delete" operation.

Expected Results

When I call Realm.removeFromRealm() function I wanna delete current Realm object and all child Realm objects.

Actual Results

At this stage if I delete Realm object all child objects stay in DB.

Code Sample

I am already made some code and happy to share it with you to make your great framework even better. May be it will be useful for someone.

Interface to make "cascade delete" optional

public interface IRealmCascade {
}

Generic "cascade delete" function

import android.util.Log;

import java.lang.reflect.Method;
import io.realm.RealmList;
import io.realm.RealmObject;
import com.company.project.models.IRealmCascade;

/**
 */

public class RealmUtils
{
public static void deleteCascade( RealmObject dataObject )
{
    if (dataObject == null)
    {
        return;
    }
    if( IRealmCascade.class.isAssignableFrom( dataObject.getClass() ) )
    {
        for( Method method : dataObject.getClass().getSuperclass().getDeclaredMethods() )
        {
            try {
                //Ignore generated methods
                if( (method.getName().contains("realmGet$")) || (method.getName().contains("access$super")) )
                {
                    continue;
                }
                Class<?> resultType = method.getReturnType();
                //Ignore non object members
                if (resultType.isPrimitive()) {
                    continue;
                }

                if (RealmObject.class.isAssignableFrom(resultType)) {
                    //Delete Realm object
                    try {
                        RealmObject childObject = (RealmObject) method.invoke(dataObject);
                        RealmUtils.deleteCascade(childObject);
                    } catch (Exception ex) {
                        Log.e("REALM", "CASCADE DELETE OBJECT: " + ex.toString());
                    }
                } else if (RealmList.class.isAssignableFrom(resultType)) {
                    //Delete RealmList items
                    try {
                        RealmList childList = (RealmList) method.invoke(dataObject);
                        while( childList.iterator().hasNext() )
                        {
                            RealmObject listItem = (RealmObject)childList.iterator().next();
                            RealmUtils.deleteCascade(listItem);
                        }
                    } catch (Exception ex) {
                        Log.e("REALM", "CASCADE DELETE LIST: " + ex.toString());
                    }
                }
            }
            catch (Exception ex)
            {
                Log.e("REALM", "CASCADE DELETE ITERATION: " + ex.toString());
            }
        }
    }
    dataObject.deleteFromRealm();
}

}

Model example

public class NodeModel extends RealmObject implements IRITSerializable, IRealmCascade {
    @PrimaryKey
    @SerializedName("id") private String objId;
    @SerializedName("parentId") private String parentId;
    @SerializedName("realmType") private RealmType realmType;
    @Required
    @SerializedName("name") private String name;

    @SerializedName("settings") private RealmList<ValueTypeModel> columns;

    public String getObjId() {
        return objId;
    }

    public void setObjId(String objId) {
        this.objId = objId;
    }

    public String getParentId() {
        return parentId;
    }

    public void setParentId(String parentId) {
        this.parentId = parentId;
    }

    public RealmType getRealmType() {
        return realmType;
    }

    public void setRealmType(RealmType realmType) {
        this.realmType = realmType;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public RealmList<ValueTypeModel> getColumns() {
        return columns;
    }

    public void setColumns(RealmList<ValueTypeModel> columns) {
        this.columns = columns;
    }
}

"How to use?"

for( NodeModel nodeItem: incomingData.getNodesList() )
{
    RealmResults<NodeModel> results = bgRealm.where(NodeModel.class).equalTo("objId", nodeItem.getObjId()).findAll();
    if (results.size() > 0)
    {
        RealmUtils.deleteCascade(results.first());
    }
    bgRealm.copyToRealm(nodeItem);
}

Version of Realm and tooling

Realm version(s): 0.89

Android Studio version: 2.1

Which Android version and device: Any

cmelchior commented 8 years ago

Hi @laindow Thank you for your suggestion. Instead of using reflection it might be easier to make a special method in your model classes that does this instead:

public class Foo extends RealmObject {
  public RealmList<Foo> list;
  public String name;

  public void cascadeDelete() {
    list.deleteAllFromRealm(); // The cascade part
    deleteFromRealm(); // delete this object
  }  
}

Your approach is more generic though, which also has a lot of merrit. This is already being tracked in #1104 though, so i'll close this as a duplicate.

laindow commented 8 years ago

Hi guys,

This is a general solution. You don't need to implement delete function for each model. It can delete your whole objects structure even if it complicated. If you add a property to the model you don't need to rewrite delete function. Sadly payment for that is performance. Reflection always more slow if compare with direct call but if you don't have huge amount of records and your models structure complicated it would be better solution.

Best regards, Konstantin.

snowpong commented 8 years ago

@cmelchior Your suggested solution won't work correctly if Foo contains any Realm Models. They'll be orphaned when their parent Foo is deleted right?

cmelchior commented 8 years ago

Yes, that is correct. You will need to manually delete all references in the cascadeDelete method.

public class Foo extends RealmObject {
  public RealmList<Foo> list;
  public Bar bar;
  public String name;

  public void cascadeDelete() {
    list.deleteAllFromRealm(); // The cascade part
    if (bar != null) {
      bar.deleteFromRealm();
    }
    deleteFromRealm(); // delete this object
  }  
}
fryossi commented 8 years ago

What about when updating an existing nested realmObject with copyToRealmOrUpdate()?

Zhuinden commented 8 years ago

@fryossi if I know correctly, copyToRealmOrUpdate() goes through all objects in the hierarchy - although technically if you modify a managed RealmObject, then you don't have to call copyToRealmOrUpdate() on it to update its fields.

snowpong commented 8 years ago

@fryossi If you call copyToRealmOrUpdate() on an existing root object, it will overwrite the child objects with their new data. Note: You'll have orphaned children if the updated root object no longer refers to them. For example Person "Bob" refers to two Dogs, "Fox" and "Rex". But then "Fox" dies and when you call copyToRealmOrUpdate() with a new version of "Bob" you'll still have "Fox" dangling in Realm but "Bob" no longer refers to it.

fryossi commented 8 years ago

You'll have orphaned children if the updated root object no longer refers to them

@Zhuinden and @snowpong - I know, that's the problem! There's a workaround for deletion but how to avoid it on updating with copyToRealmOrUpdate()?

Zhuinden commented 8 years ago

if you modify a managed RealmObject, then you don't have to call copyToRealmOrUpdate() on it to update its fields

snowpong commented 8 years ago

@fryossi assuming you have a new "Bob" gotten from a REST server for example, to update it do this in a Transaction:

  1. get the old "Bob" from realm, like: realm.where(Person.class).equalTo("id", newBob.getId()).findFirst()
  2. delete all it's children, like: oldBob.getDogs().deleteAllFromRealm()
  3. insert the new "Bob" like: realm.copyToRealmOrUpdate(newBob)

Note: This assumes the dogs can only have one owner.

fryossi commented 8 years ago

So you are saying there's no option but delete its children.

snowpong commented 8 years ago

@fryossi A more elegant solution is to remove only the children that are no longer referred to in the updated parent. You could possibly do something like:

oldBob.getDogs().where().not().in("id", arrayOfDogIdsFromNewBob).deleteFromRealm()

before calling copyToRealmOrUpdate(newBob)

fryossi commented 8 years ago

Thanks.

Miha-x64 commented 7 years ago

In a situation when many People can refer the same Dogs in their lists, removing lost Dogs may act like GC and can be quite time-consuming šŸ•” šŸ˜’

jemshit commented 7 years ago

i don't get what is the deal here. While you can add object and its sub objects without problem, why can't you delete them? You have the index of sub objects ?!

Zhuinden commented 7 years ago

I think Realm-Core does have support for cascading now.

The question here of course is that if you delete the parent object with RealmResults.deleteAllFromRealm(), then its connected objects should also be automatically deleted by Realm.

That'll happen eventually.

jemshit commented 7 years ago

Maybe you are right, have not tried on latest version, didn't work on 3.2.0 (a bit old)

Zhuinden commented 7 years ago

No it is not supported by the Java binding yet, still manual

goa commented 4 years ago

Since after so many years there is still no native approach for cascading deletes, could the Realm Java team at least please provide a temporary generic solution based on the latest version of Realm? Something like what @laindow provides in this issue's description, but updated for the latest version of the library in both Java and Kotlin?

The Realm Swift community (not the actual development team) has come up with some pretty decent solutions over the years.

Some of our projects have tens of very large model types that get updated frequently. Do we have to force our team to update both the migration code AND the cascade deletion code? It's a very error-prone procedure.

I think this problem should not be ignored any more. Most Android and iOS projects I've seen based on Realm just don't implement cascade deletes at all! Both the API and Realm's "simple to use" promotion is deceiving many developers into thinking that the current model delete methods actually clean up everything!

I personally believe that cascade deletes should have been available ages ago, but I understand that resources are limited. However, a temporary "official" generic approach should be prominently available in the documentation until the native solution becomes available... I can't believe that your best suggestion is to have us implement this manually for each one of our model types.

Zhuinden commented 4 years ago

Both the API and Realm's "simple to use" promotion is deceiving many developers into thinking that the current model delete methods actually clean up everything!

Did they ever really say that? You delete an object, nobody auto-clears the linked objects. Imagine this, if the owner of a dog is deleted, the dog lives on.

The best way to avoid hard-to-manage cascade deletion is to use as low number of links as possible. For example, forward-facing uni-directional links can be merged into the same object, inheriting the fields from the supposed linked class.

I am still not a member of Realm, but my links have never been so hard to manage. Worst thing that happened was 3 long but that was unavoidable as they were all lists from one to another.

I would think lately, using @LinkingObjects can verify that there are no linked objects pointing to you, so you can safely remove your object.

I also think that Realm-Core has supported this since 1.5 years ago, but I think Realm Sync got in the way of eventually ever adding the concept of @StrongRelation. A shame that Sync held back the development of the Database.

goa commented 4 years ago

Both the API and Realm's "simple to use" promotion is deceiving many developers into thinking that the current model delete methods actually clean up everything!

Did they ever really say that? You delete an object, nobody auto-clears the linked objects. Imagine this, if the owner of a dog is deleted, the dog lives on.

No, the Realm team didn't explicitly state that cascading deletes are handled automatically. However the easiness of creating relationships and the fact that the documentation has a whole section on them, lures most developers into thinking that everything (including cascading deletion) is handled automatically under the hood.

I've been using Realm since 2015 on some relatively demanding applications and I remember how difficult it had been to implement manual cascading deletion for a set of models with complex relations back then. In fact, I would say that the lack of this feature and the Realm threading approach have been the two biggest problems myself and most of the devs I've talked to had to handle.

Yesterday I was asked to evaluate the code of an application that uses Realm and noticed that it does not handle cascading deletion either! It's maybe the 10th time I'm seeing this on an app! The devs just did not realise that when you delete the "owner", their "dogs" stay behind. The story is always the same: Let's download some inter-related data from a REST API and save them to easy-to-use Realm. It's so easy to save them, that deleting them must be easy too.

I believe that until cascading deletes are implemented, the documentation should at least explicitly mention that they are not supported and point to an "official" temporary generic solution. It should also warn users that this temporary approach might hurt performance and offer examples of how to implement cascading deletes manually for each model type.

IMHO the current state of the documentation is severely lacking in that respect, plus I think that the best "generic" solution one can find online is the one at the beginning of this post. Java-only and implemented with Realm 0.89.

Zhuinden commented 4 years ago

While I understand the sentiment, I can't blame Realm for people misunderstanding how object deletion works.

And the "threading approach" is only a problem because devs often create this infinite nesting of thread jumps and Realm is totally legit saying "hey, we don't trust what you're trying to do here". In a way, Realm encourages code to be better, and for the developer to at least remotely care about what on earth they're doing.

Saying Realm's threading is hard is akin to saying "the way to fix NetworkOnMainThreadException is overriding Strict Mode to allow main thread network requests". No.

If people had a better understanding of concurrency, and wouldn't want to throw their code to execute on seemingly random threads, then they wouldn't find Realm's threading model difficult.

I am still not a member of Realm, and that's just my personal opinion.

However, I do admit that the addition of @StrongRelationship would have greatly improved the Realm Database, and it's regretful that it got stuck in limbo in 2018 and then never happened.

goa commented 4 years ago

I would like to thank you for replying in the first place and I agree with you that none of the issues I am raising is strictly a problem of the technology.

I am not blaming Realm for anything either. In fact I have been a very strong supporter for it since its early days. I have also made some serious architectural decisions involving Realm in multiple projects, because I do believe the technology is good and the people behind it are capable.

I just think that since this functionality is something that many developers need (and it seems some don't even know they do) and since it hasn't been implemented in a long time, an official temporary cascading delete solution and an update of the related documentation will greatly benefit both the project itself and the community.