Find Jim's Dog in a one-to-one relationship


#1

In the Realm Xamarin docs for To-One Relationships we have this example for finding Rex’s owner

public class Dog : RealmObject
{
    // ... other property declarations
    public Person Owner { get; set; };
}

public class Person : RealmObject
{
    public string Name { get; set; }
}
realm.Write(() =>
{
    var jim = realm.Add(new Person { Name = "Jim" });

    var rex = realm.Add(new Dog { Owner = jim });
});

We can find Rex’s owner like this rex.Owner. How do we find Jim’s dog? Must we query the database to return a list of one as in realm.All<Dog>().Where(d => d.Owner == "Jim")?


#2

I assume it’s d.Owner.Name == "Jim".

Also, don’t forget that Person could have a backlink to their Dogs owned by them.


#3

When using the LINQ syntax, you need to obtain a reference to jim, e.g.:

var jim = realm.All<Person>().FirstOrDefault(p => p.Name == "Jim");
var jimsDogs = realm.All<Dog>().Where(d => d.Owner == jim);

// If you're certain jim only has a 0 or 1 dogs, you can use SingleOrDefault
var jimsOnlyDog = realm.All<Dog>().SingleOrDefault(d => d.Owner == jim);

You can also use the string-based query syntax that is more powerful but not as convenient as LINQ:

var jimsDogs = realm.All<Dog>().Filter("Owner.Name == 'Jim'");
var jimsOnlyDog = jimsDogs.SingleOrDefault();

#4

Thanks @nirinchev. That’s as I thought: in Realm you must choose in your one-to-one relationship design to have either Owner.Dog or Dog.Owner, you can’t have both.


#5

You could declare an Inverse Relationship property on your model, but inherently that is a collection - you can’t force it to be a single object, and the reason is that there’s nothing preventing you from assigning the same owner to two or more dogs. If you have code/logic in your app that ensures it’s a single object, you could play with the accessibility modifiers of the properties to simulate a one-to-one relationship:

public class Person : RealmObject
{
    [Backlink(nameof(Dog.Owner)]
    private IQueryable<Dog> Dogs { get; }

    public Dog Dog => Dogs.FirstOrDefault();
}

public class Dog : RealmObject
{
    public Person Owner { get; set; }
}

This way, the Person.Dogs collection contains all dogs that have this person assigned as Owner and the Dog property returns the first of those. Because the collection is private, and the property is public, the consumers of this model will assume it’s a legitimate one-to-one relationship. Be advised though, that Dog is not a persisted property on the Realm, so you won’t be able to create a query that references it, e.g. realm.All<Person>().Where(p => p.Dog == someDog) will throw a runtime exception.


#6

Is that because the only property of Dog is Person? If Dog had another property such as name:

public class Dog : RealmObject
{
    public atring Name { get; set; };
    public Person Owner { get; set; };
}

would Dog then be persisted?


#7

I meant that Person.Dog is not a persisted property - e.g. if you open the Realm in Studio, you’ll see that the Person table doesn’t have a column called Dog. This is because Person.Dog is an automatic property with no setter, so Realm ignores these.


#8
public class Person : RealmObject
{
    [Backlink(nameof(Dog.Owner)]
    private IQueryable<Dog> Dogs { get; }

    public Dog Dog => Dogs.FirstOrDefault();
}

public class Dog : RealmObject
{
    public Person Owner { get; set; }
}

Sorry but I’ve realised I’m not understanding this as well as I thought @nirinchev . We need to persist Dog as a property of Person. Person can have a maximum of 1 Dog. Every Dog must have exactly one owner.

Is that in the examples?


#9

The examples assumed you have some logic in place to ensure that the 1-1 relationship holds. In general, there’s nothing built-in in Realm to enforce such a design. E.g. what would happen if you tried to assign Person.Dog to a person that already has a dog? How do you handle the first dog? You could expose a setter on the Person.Dog property to encapsulate some of that logic, but if you’re using synchronized Realms, there’s no way to enforce that, after merging some changes, multiple dogs won’t end up with the same owner. If you’re not using sync, you could implement this as a setter logic:

public class Person : RealmObject
{
    [Backlink(nameof(Dog.Owner)]
    private IQueryable<Dog> Dogs { get; }

    public Dog Dog
    {
        get { return Dogs.FirstOrDefault(); }
        set
        {
            var existing = Dogs.FirstOrDefault();
            if (existing != null)
            {
                throw new Exception("Person already has s dog.");
            }

            // Assign yourself as owner to this Dog.
            // This will update the backlinks property which in turn
            // will update the value of this.Dog
            value.Owner = this;
        }
    }
}

#10

Thanks again @nirinchev . Your example returns an error that I am unable to resolve:

An object reference is required for the non-static field, method, or property ‘Person.Dog’


#11

Which example - I’m not using Person.Dog in the snippet I posted? The error is caused by a code that is trying to access Dog as a static property on the Person class rather than an instance of this object (something like var person = ...; person.Dog = ...).


#12

I just copied the Person and the Dog class into a new project with nothing else in it. Nothing was trying to access either.


#13

Not sure I understand - if nothing was accessing anything, where did the exception originate? Can you post your new project’s code?


#14

Simple demo app sent by PM


#15

Thanks to @nirinchev for helping resolve this. In case others wish to specify a relationship of the kind where Person can have 0 or 1 dogs and Dog must have exactly one owner (one-to-oneORnull), the approach I have taken is below

namespace Data
{
    public class Person : RealmObject
    {
        [Backlink(nameof(Data.Dog.Owner))]
        private IQueryable<Dog> Dogs { get; }

        public Dog Dog
        {
            get { return Dogs.FirstOrDefault(); }
            set
            {
                var existing = Dogs.FirstOrDefault();
                if (existing != null)
                {
                    throw new Exception("Person already has s dog.");
                }

                // Assign yourself as owner to this Dog.
                // This will update the backlinks property which in turn
                // will update the value of this.Dog
                value.Owner = this;
            }
        }
    }
    public class Dog : RealmObject
    {
        public Person Owner { get; set; }
    }
}