How to Create a Many-to-Many Relationship in Realm JS React Native

How to Create a Many-to-Many Relationship in Realm JS React Native

While building my app ThinkBack I decided to use Realm as my underlying data-store. ThinkBack allows users capture notes and categorize them using hashtags. A note can have many hashtags and a hashtag can have many notes. This is the basis of a many-to-many relationship.

Realm’s documentation on model relationships shows an example using a car and manufacturer model. In their example a car has a relationship with a manufacturer, and a manufacturer has a one-to-many relationship with a car. This example creates a “to-many” relationship from manufacturer to car simply by adding a property to the manufacturer model that references a collection of cars.

By default a relationship is one-sided, for example a manufacturer has a link to cars, but cars don’t link back to the manufacturer. In order for a car to know its manufacturer Realm leverages the concept of an inverse relationship. An inverse relationship is simply a link from one object back to another, and is defined by setting the “type” of the linking property as a linkingObjects. This relationship must be explicitly defined otherwise a car will be ignorant to the fact it belongs to a manufacturer. The downside to this is that if you want to load the manufacturer associated with a car you need to issue a separate query in Realm rather than simply dot-notationining into it (e.g. myCar.manufacturer.name)

Below is the example from the documentation. Line 11 defines the “to-many” relationship from manufacturer to car, and line 27-31 define the inverse relationship from car back to manufacturer.

class ManufacturerInverse extends Realm.Object {
  _id!: BSON.ObjectId;
  name!: string;
  cars!: Realm.List<CarInverse>;
  static schema: Realm.ObjectSchema = {
    name: 'ManufacturerInverse',
    properties: {
      _id: 'objectId',
      name: 'string',
      // A manufacturer's related CarInverse objects
      cars: 'CarInverse[]',
    },
  };
}
class CarInverse extends Realm.Object {
  _id!: BSON.ObjectId;
  model!: string;
  manufacturer!: Realm.List<ManufacturerInverse>;
  miles?: number;
  static schema: Realm.ObjectSchema = {
    name: 'CarInverse',
    properties: {
      _id: 'objectId',
      model: 'string',
      miles: 'int?',
      // A car's related ManufacturerInverse objects
      manufacturer: {
        type: 'linkingObjects',
        objectType: 'ManufacturerInverse',
        property: 'cars',
      },
    },
  };
}

Creating a many-to-many relationship between Hashtag and Note

Creating a many-to-many relationship between Hashtags and Notes is exactly the same as the relationship between Cars and Manufacturers.

Note Model

Line 30 defines a “to-many” relationship between a Note and Hashtag. This is equivalent to the manufacturer-to-car relationship in the example above. When a note is queried in Realm the hashtags object will point to N number of hashtags that reference this note. This is handy if you want to load all hashtags for a given note without issuing a separate query (e.g. myNote.hashtags.forEach(...)).

import uuid from 'react-native-uuid';
import Hashtag from './Hashtag';

export interface NoteProps {
  id?: string;
  content: string;
  hashtags: Hashtag[];
}

export default class Note {
  public id: string;
  public content: string;
  public hashtags: Hashtag[];

  constructor({
    id = uuid.v4().toString(),
    content,
    hashtags,
  }: NoteProps) {
    this.id = id;
    this.content = content;
    this.hashtags = hashtags;
  }

  static schema: Realm.ObjectSchema = {
    name: 'Note',
    properties: {
      id: 'string',
      content: 'string',
      hashtags: `Hashtag[]`,
    },
    primaryKey: 'id',
  };
}

Hashtag Model

Line 26 defines a linking object property notes which links back to the hashtags property of the Notes model. This inverse relationship effectively creates a collection of notes that have a reference to this particular hashtag. When a hashtag is queried in Realm the notes object will point to N number of notes that reference this hashtag. This is handy if you want to load all notes for a given hashtag without issuing a separate query (e.g. myHashtag.notes.forEach(...)).

import uuid from 'react-native-uuid';
import Note from './Note';

interface HashtagProps {
  id?: string;
  name: string;
}

export default class Hashtag {
  public id: string;
  public name: string;
  public notes: Note[];

  constructor({ id = uuid.v4().toString(), name }: HashtagProps) {
    this.id = id;
    this.name = name;
    this.notes = [];
  }

  static schema: Realm.ObjectSchema = {
    name: 'Hashtag',
    properties: {
      id: 'string',
      name: 'string',
      // all notes who have this hashtag added in the "hashtags" property.
      notes: { type: 'linkingObjects', objectType: 'Note', property: 'hashtags' },
    },
    primaryKey: 'id',
  };
}

With this relationship in place you can now load a note or a hashtag and have a direct reference to the other side of the relationship:

const note = realm.objectForPrimaryKey<Note>('Note', id);

// load all hashtags associated with the given note and extract their names.
const hashtagNames = note.hashtags.map(h => h.name)

I'd love to hear your thoughts