Enforce Referential Integrity

I want to ensure every Job has a valid JobType.

public class JobType : RealmObject
{
    [PrimaryKey]
    public string JobTypeId { get; set; } = Guid.NewGuid().ToString();

    [Backlink(nameof(Job.JobType))]
    internal IQueryable<Job> Jobs { get; }
}

public class Job : RealmObject
{
    [PrimaryKey]
    public string JobId { get; set; } = Guid.NewGuid().ToString();
    public JobType JobType { get; set; } 
}

However this still allows a Job.JobType = null. I could initialise JobType, then use the setter to prevent it from being null. Does Realm have a mechanism to ensure referential integrity?

@Nosl We don’t support this at this time

Given Realm does not enforce referential integrity, how should we design to enforce referential integrity? I’ve tried enforcing referential integrity in the property setter like this:

internal class Customer : RealmObject
{
    [PrimaryKey]
    public string CustomerId { get; set; }

    [Backlink(nameof(Order.Customer))]
    internal IQueryable<Order> Orders { get; }
}
internal class Order : RealmObject
{
    [PrimaryKey]
    public string OrderId { get; set; }
    private Customer _Customer;
    public Customer Customer
    {
        get => _Customer;
        set => _Customer = value ?? throw new System.ArgumentNullException("Customer cannot be null for Order");
    }
}

On startup this throws

Schema validation failed due to the following errors:

  • Property ‘Order.Customer’ declared as origin of linking objects property ‘Customer.Orders’ does not exist

Using Realm Browser I am able to see that none of the Orders in the test database has a null Customer, so why do we get this error?

The actual name of the property in the Realm database is _Customer, so the backlinks property needs to point to that (Customer is unknown to Realm because it’s not an automatic property). You have two options:

  1. Update the Backlink attribute and tell it to use _Customer. Since this is a private property, you’ll need to explicitly type the string value like: Backlink("_Customer"). _Customer also needs to be a property, not a field for Realm to pick it up.

or

  1. Add a MapTo attribute on the _Customer property:
[MapTo(nameof(Customer)]
private Customer _Customer { get; set; }

Thank you @nirinchev however changing the Customer property on the Order class to this

[MapTo(nameof(Customer))]
private Customer _Customer;
public Customer Customer
{
    get => _Customer;
    set => _Customer = value ?? throw new System.ArgumentNullException("Customer cannot be null for Order");
}

gives this error
Attribute 'MapTo' is not valid on this declaration type. It is only valid on 'class, property, indexer' declarations.

Yes, as I mentioned, you need to make _Customer an automatic property to be picked up by Realm.

but how then do we do ensure the property is not null?

Well, it’s a private property, so you wouldn’t be able to set it, except when using the Customer property which does the validation.

So on the Order class we have

private Customer _Customer;
public Customer Customer
{
    get => _Customer;
    set => _Customer = value ?? throw new System.ArgumentNullException("Customer cannot be null for Order");
}

and on the Customer class

[Backlink("_Customer")]
internal IQueryable<Order> Orders { get; }

Does that look right?

Apologies @nirinchev I was being a bit obtuse there. I missed that you had indicated both public and private setters. That seems to work. Thanks for your help.

One hopefully minor point. When I decorate the Customer property of the Order class

[MapTo(nameof(Customer))]
private Customer _Customer { get; set; }
public Customer Customer
{
    get => _Customer;
    set => _Customer = value ?? throw new System.ArgumentNullException("Customer cannot be null for Order");
}

I get a successful build but a warning:

Fody/RealmWeaver: Order.Customer is not an automatic property but its type is a RealmObject which normally indicates a relationship.

Is that something to worry about?

No, that’s fine - we added this warning because people sometimes assume they can write any complex property and have it persisted into Realm. Since you’re doing that on purpose, you can ignore the warning.

This has been working well now for almost a year with the Customer property of Order set like this:

private Customer _Customer { get; set; } = RealmService.DefaultCustomer();
public Customer Customer
{
    get => _Customer;
    set => _Customer = value ?? throw new Exception("Customer cannot be null for Order");
}

and on the Customer class

[Backlink(nameof(Order.Customer))]
internal IQueryable<Order> Orders { get; }

but we recently had a user where about 100 orders out of about 2000 orders had Customer set to null.

We have over 100 users trialling the Realm version of this product. Each user has their own realm. How is it that Realm has allowed the customer to be null on this one database? Is it that the Orders backlink on the Customer class references Customer property rather than _Customer field?

Since Realm doesn’t have cascading deletes, it’s possible to set a customer to a non-null value, then delete the Customer object. This will then set the property to null on all orders that had referenced that customer.

So Realm is setting Order.Customer to null without calling the Order.Customer setter. Is that correct?

Is there anything you can suggest we do in the model to mitigate this?

That is correct - Realm doesn’t really use the setters at all - they’re only for your application. Unfortunately, there’s nothing right now that works like cascading deletes would. You could setup a server-side event handler and handle changes to the Customer collection. Then, if someone deletes a customer, you can find all their orders and delete them. If you want to keep them, regardless of the deleted customer, you can create a “deleted customer” customer object and assign it to all orders whose customer has been deleted. Alternatively, in your app, you can filter orders that have a null customer and not display/use those.

* Note: if you go for the event handler solution, the docs mention using the Realm.Server package, but that’s no longer needed - everything you need to setup an event handler exists in the Realm package (starting with 4.0.0), so you should reference only that.

It’s not really cascading deletes that I’m looking for. I know that’s hard to do. It would be nice just to be able to enforce [Required] on a reference type. Is that in the pipeline?

How would you enforce that without cascading deletes though? E.g. if I delete a User that is referenced by an Order, what do you expect to happen with that order?

Prevent the delete of the customer if it is referenced by an order. We try to do that elsewhere, but that’s not foolproof. It would be better in the model.

That’s not possible at the moment and I don’t believe we have plans to support something like that. I imagine a more generalized version of that would be to allow user-defined validators to run before commit, but even that is just an idea we have discussed internally and not something we have made any plans for.