Understanding Backlinks


#1

The Realm Database docs for Xamarin use Ships and Captains and Owners and Dogs to illustrate the use of backlinks, but I’m still confused.

In my world Ships and Captains have a many to many relationship as captains rotate among ships on a roster. An owner might have many dogs, but also a single dog might have two or more owners.

It’s possible I am confused because I have no experience writing apps for ships or dogs. I do write business apps though so if you don’t mind I hope someone can help clarify how backlinks help in a common business scenario.

Take an Order class that has a collection of OrderDetail class.

public class Order : RealmObject
{
    public string OrderId { get; set; }
    public IList<OrderDetail> OrderDetails { get; }
}
public class OrderDetail : RealmObject
{
    public string OrderDetailId { get; set; }
    public Order Order { get; set; }
}

How would backlinks be used in this case and how would they improve the app?


#2

I’m sorry that the examples in the documentation aren’t clear enough. I’ll try to come up with better ones and update the docs. In any case, the idea was to show that backlinks can be used both for one-to-many and many-to-many scenarios (I assumed that dogs can only have a single owner). In the case of your order and order details, backlinks can be used to replace the list and to ensure that you don’t have duplicates or inconsistencies. For example, with the list you could end in the following erroneous states:

Order order = ...;
OrderDetail detail = ...;

order.OrderDetails.Add(detail);
detail.Order = null; // doesn't point to the parent
detail.Order = order2; // points to a different parent

order.OrderDetails.Add(detail);
order.OrderDetails.Add(detail); // duplicates in the list

detail.Order = order;
order.OrderDetails.Clear(); // parent doesn't contain it in the list

All of this can be avoided with backlinks:

public class Order : RealmObject
{
    public string OrderId { get; set; }

    [Backlink(nameof(OrderDetail.Order))]
    public IQueryable<OrderDetail> OrderDetails { get; }
}
public class OrderDetail : RealmObject
{
    public string OrderDetailId { get; set; }
    public Order Order { get; set; }
}

In this case, you don’t add details directly to the order (as IQueryable doesn’t have Add method), but instead set the Order property on the OrderDetail instance. Then, the OrderDetails collection will automatically get updated to include the new value, e.g.:

Order order = ...;
OrderDetail detail = ...;

Console.WriteLine(order.OrderDetails); // empty

realm.Write(() =>
{
    detail.Order = order;
});

Console.WriteLine(order.OrderDetails); // 1 element: detail

Hope this helps. I’ll be happy to answer any follow-up questions :slight_smile:


#3

Thank you @nirinchev for taking the trouble to give such a comprehensive answer. That clears it up nicely for me.


#4

It’s taken we a while @nirinchev but I do have a follow up:

public class Order : RealmObject
{
    public string OrderId { get; set; }

    [Backlink(nameof(OrderDetail.Order))]
    public IQueryable<OrderDetail> OrderDetails { get; }
}
public class OrderDetail : RealmObject
{
    public string OrderDetailId { get; set; }
    public Order Order { get; set; }
}

If we do this

realm.Write(() =>
{
    order.OrderId = ....;
    OrderDetail detail = new OrderDetail();
    detail.OrderDetailId = ....;
    detail.Order = order;
    realm.Add(order, update: true);
});

The order is saved but the order detail is not. Is that expected behavior?

If so, what is recommended practice for creating new objects with children? Must the parent object be saved before children can be added? Can committing parent/child collections be transactional?


#5

It is expected, yes. The backlink relationship is not “strong” relationship, meaning objects won’t be added to a Realm simply because they would be member of a backlink collection. Relationship properties, however, are, so adding a detail would add its related unmanaged objects. So you can modify your code to:

realm.Write(() =>
{
    order.OrderId = ....;
    OrderDetail detail = new OrderDetail();
    detail.OrderDetailId = ....;
    detail.Order = order;

    realm.Add(detail, update: true); // <--
});

To be on the safe side, you can always add both the order and the detail - there’s no performance overhead of doing so and may be a bit more obvious to people working with the codebase.


#6

Thank you for that @nirinchev. To be sure I understand correctly we could clarify what is happening with
realm.Write(() =>

{
    Order order = new Order()
    order.OrderId = ....;
    realm.Add(order, update true);

    OrderDetail detail = new OrderDetail();
    detail.OrderDetailId = ....;
    detail.Order = order;

    realm.Add(detail, update: true); // <--
});

The outcome would be the same as your example. Is that what you meant?


#7

Yes. To give you a bit more context here, when realm.Add is invoked, it creates the native object and starts copying its properties from the managed object. When it reaches a relationship, e.g. detail.Order, it checks if the Order is already managed by Realm. If it is, we simply continue, if it isn’t, we recursively call realm.Add for the order (and possibly its related unmanaged objects). This is why, realm.Add(detail) will add the order if necessary. This applies to IList<T> relationships as well.

The reason why it doesn’t behave the same way for backlinks is that backlinks are computed properties and don’t actually contain references to their members. Under the hood they are similar to:

public class Order : RealmObject
{
    // Backlink equivalent
    public IQueryable<OrderDetail> OrderDetails
    {
        get
        {
            return this.realm.All<OrderDetail>().Where(d => d.Order == this);
        }
    }
}

So before you actually add the detail to the Realm, order.OrderDetails will not contain it. Hope this helps.


#8

Thank you @nirinchev. I really appreciate the explanation.


#9

On a related note, when you add the items to the database, do they have to be added in the same write transaction for the relationship to work? For example, if I create an order object, write/add the order to the realm db, then create a detail object with the original order object and write/add that to the db in a second transaction, will it understand that it is the same order?

I tried doing something like this, but got an exception like “An Order object already exists with primary key property ID == 1”

Does this mean I can only add new orders when I add a new detail? Is there a way to link a new detail object with an existing order?


#10

Use realm.Add(detail, update: true); This works like an upsert.


#11

Thanks! I assume this will update both the order and the detail objects, if there are changes to either of them?


#12

If you can post your snippet I can give you more specific explanation, but generally, if you .Add an object to Realm, it will recursively add the entire graph (e.g. realm.Add(foo) will internally call realm.Add(foo.Bar)). If at that point foo.Bar is an unmanaged object that has a conflicting primary key, you’ll get that exception. But if you assign Bar to an object that has already been added, things will go without an error. For example

var order = new Order
{
    OrderId = 5
};

realm.Write(() =>
{
    realm.Add(order);
});

var detail = new Detail
{
    Order = order; // Order is managed, so this is fine
};

realm.Write(() =>
{
    realm.Add(detail);
});

var duplicateOrder = new Order
{
    OrderId = 5
};

// This will result in an error because duplciateOrder is unmanaged
// and realm.Add will try to add that as well.
var errorDetail = new Detail
{
    Order = duplicateOrder
}

realm.Write(() =>
{
    realm.Add(detail);
});

As @nosl pointed out, if you use Add(detail, update: true), this will work, but the order will be updated as well, so it may not be desired behavior.


#13

I’m a bit confused about what makes an item managed vs. unmanaged. I’ve done something like this:

(In TestData.cs)

List<Project> projects = new List<Project>
            {
                new Project{ ProjectName = "Project Name 1", ProjectIdentifier ="ABC"},
                new Project{ ProjectName = "Project Name 2", ProjectIdentifier = "XYZ" }
            };

List<Activity> activities = new List<Activity> 
            {
                new Activity { ActivityIdentifier = "a", Project = projects[0] },
                new Activity { ActivityIdentifier = "b", Project = projects[0] },
                new Activity { ActivityIdentifier = "c", Project = projects[1] },
            };

//add to realm db
ProjectController projectController = new ProjectController();
foreach (var project in projects)
{
    projectController.AddProject(project);
}

ActivityController activityController = new ActivityController();
foreach(var activity in activities)
{
    activityController.AddActivity(activity);
}

(Models)

public class Activity: RealmObject
    {
        [PrimaryKey]
        public string ActivityIdentifier { get; set; }

        public Project Project { get; set; }

        [Backlink(nameof(Result.Activity))]
        public IQueryable<Result> Results { get; }
    }

public class Project : RealmObject
    {
        [PrimaryKey]
        public string ProjectIdentifier { get; set; }

        [Backlink(nameof(Activity.Project))]
        public IQueryable<Activity> Activities { get; }
    }

(Controllers)

class ActivityController
    {
        private Realm realm;
        public ActivityController()
        {
            realm = Realm.GetInstance();
        }

        public bool AddActivity(Activity activity)
        {
            //check if the activity already exists in the realm db
            var existing = realm.All<Activity>().Any(p => p.ActivityIdentifier == activity.ActivityIdentifier);

            if (!existing)
            {
                realm.Write(() =>
                    {
                        //update: true will update both related items and the activity
                        realm.Add(activity, update: true);
                    });
                    return true;
            }
            //if not added to db
            return false;
        }
    }

public class ProjectController
    {
        private Realm realm;
        public ProjectController()
        {
            realm = Realm.GetInstance();
        }

        public bool AddProject(Project project)
        {  
            //try-catch blocks removed from code in this example to make it more succinct          
            realm.Write(() =>
                {
                    realm.Add(project);
                });
             return true; 
        }
    }

I’m pretty new to this kind of project, so I’m not sure if this MVC type of pattern makes sense for Realm/Xamarin. Maybe doing everything in a separate controller is part of the problem? It seems to work okay with the check for the existing activity, but I’m wondering why the update: true is needed here - it seems very similar to your example.

Thanks very much for your help.