Search
ctrl/
Ask AI
Light
Dark
System

Links

Links define a specific relationship between two object types.

You can add an exclusive constraint to a link to guarantee that no other instances can link to the same target(s).

Copy
type Person {
  name: str;
}

type GroupChat {
  required multi members: Person {
    constraint exclusive;
  }
}

In the GroupChat example, the GroupChat.members link is now exclusive. Two GroupChat objects cannot link to the same Person; put differently, no Person can be a member of multiple GroupChat objects.

By combinining link cardinality and exclusivity constraints, we can model every kind of relationship: one-to-one, one-to-many, many-to-one, and many-to-many.

Relation type

Cardinality

Exclusive

One-to-one

single

Yes

One-to-many

multi

Yes

Many-to-one

single

No

Many-to-many

multi

No

Many-to-one relationships typically represent concepts like ownership, membership, or hierarchies. For example, Person and Shirt. One person may own many shirts, and a shirt is (usually) owned by just one person.

Copy
type Person {
  required name: str
}

type Shirt {
  required color: str;
  owner: Person;
}

Since links are single by default, each Shirt only corresponds to one Person. In the absence of any exclusivity constraints, multiple shirts can link to the same Person. Thus, we have a one-to-many relationship between Person and Shirt.

When fetching a Person, it’s possible to deeply fetch their collection of Shirts by traversing the Shirt.owner link in reverse. This is known as a backlink; read the select docs to learn more.

Conceptually, one-to-many and many-to-one relationships are identical; the “directionality” of a relation is just a matter of perspective. Here, the same “shirt owner” relationship is represented with a multi link.

Copy
type Person {
  required name: str;
  multi shirts: Shirt {
    # ensures a one-to-many relationship
    constraint exclusive;
  }
}

type Shirt {
  required color: str;
}

Don’t forget the exclusive constraint! This is required to ensure that each Shirt corresponds to a single Person. Without it, the relationship will be many-to-many.

Under the hood, a multi link is stored in an intermediate association table, whereas a single link is stored as a column in the object type where it is declared.

Choosing a link direction can be tricky when modeling these kinds of relationships. Should you model the relationship as one-to-many using a multi link, or as many-to-one using a single link with a backlink to traverse in the other direction? A general rule of thumb in this case is as follows.

Use a multi link if:

  • The relationship is relatively stable and thus not updated very frequently. For example, a list of postal addresses in a user profile.

  • The number of elements in the link tends to be small.

Otherwise, prefer a single link from one object type coupled with a computed backlink on the other. This is marginally more efficient and generally recommended when modeling 1:N relations:

Copy
type Post {
  required author: User;
}

type User {
  multi posts := (.<author[is Post])
}

Under a one-to-one relationship, the source object links to a single instance of the target type, and vice versa. As an example consider a schema to represent assigned parking spaces.

Copy
type Employee {
  required name: str;
  assigned_space: ParkingSpace {
    constraint exclusive;
  }
}

type ParkingSpace {
  required number: int64;
}

All links are single unless otherwise specified, so no Employee can have more than one assigned_space. Moreover, the exclusive constraint guarantees that a given ParkingSpace can’t be assigned to multiple employees at once. Together the single link and exclusivity constraint constitute a one-to-one relationship.

A many-to-many relation is the least constrained kind of relationship. There is no exclusivity or cardinality constraints in either direction. As an example consider a simple app where a User can “like” their favorite Movies.

Copy
type User {
  required name: str;
  multi likes: Movie;
}
type Movie {
  required title: str;
}

A user can like multiple movies. And in the absence of an exclusive constraint, each movie can be liked by multiple users. Thus this is a many-to-many relationship.

Links are always distinct. That means it’s not possible to link the same objects twice.

Copy
type User {
  required name: str;
  multi watch_history: Movie {
    seen_at: datetime;
  };
}
type Movie {
  required title: str;
}

With this model it’s not possible to watch the same movie twice. Instead, you might change your seen_at link property to an array to store multiple watch times.

Copy
type User {
  required name: str;
  multi watch_history: Movie {
    seen_at: array<datetime>;
  };
}
type Movie {
  required title: str;
}

Alternatively, the watch history could be modeled more traditionally as its own type.

Copy
type User {
  required name: str;
  multi watch_history := .<user[is WatchHistory];
}
type Movie {
  required title: str;
}
type WatchHistory {
  required user: User;
  required movie: Movie;
  seen_at: datetime;
}

Be sure to use single links in the join table instead of a multi link otherwise there will be four tables in the database.

Like properties, links can declare a default value in the form of an EdgeQL expression, which will be executed upon insertion. In the example below, new people are automatically assigned three random friends.

Copy
type Person {
  required name: str;
  multi friends: Person {
    default := (select Person order by random() limit 3);
  }
}

Links can declare their own deletion policy. There are two kinds of events that might trigger these policies: target deletion and source deletion.

Target deletion policies determine what action should be taken when the target of a given link is deleted. They are declared with the on target delete clause.

Copy
type MessageThread {
  title: str;
}

type Message {
  content: str;
  chat: MessageThread {
    on target delete delete source;
  }
}

The Message.chat link in the example uses the delete source policy. There are 4 available target deletion policies.

  • restrict (default) - Any attempt to delete the target object immediately raises an exception.

  • delete source - when the target of a link is deleted, the source is also deleted. This is useful for implementing cascading deletes.

    There is a limit to the depth of a deletion cascade due to an upstream stack size limitation.

  • allow - the target object is deleted and is removed from the set of the link targets.

  • deferred restrict - any attempt to delete the target object raises an exception at the end of the transaction, unless by that time this object is no longer in the set of link targets.

Source deletion policies determine what action should be taken when the source of a given link is deleted. They are declared with the on source delete clause.

There are 3 available source deletion policies:

  • allow - the source object is deleted and is removed from the set of the link’s source objects.

  • delete target - when the source of a link is deleted, the target is unconditionally deleted.

  • delete target if orphan - the source object is deleted and the target object is unconditionally deleted unless the target object is linked to by another source object via the same link.

Copy
type MessageThread {
  title: str;
  multi messages: Message {
    on source delete delete target;
  }
}

type Message {
  content: str;
}

Under this policy, deleting a MessageThread will unconditionally delete its messages as well.

To avoid deleting a Message that is linked to by other MessageThread objects via their message link, append if orphan to that link’s deletion policy.

Copy
type MessageThread {
  title: str;
  multi messages: Message {
    on source delete delete target;
    on source delete delete target if orphan;
  }
}

The if orphan qualifier does not apply globally across all links in the database or across any other links even if they’re from the same type. Deletion policies using if orphan will result in the target being deleted unless

  1. it is linked by another object via the same link the policy is on, or

  2. its deletion is restricted by another link’s on target delete policy (which defaults to restrict unless otherwise specified)

For example, a Message might be linked from both a MessageThread and a Channel, which is defined like this:

Copy
type Channel {
  title: str;
  multi messages: Message {
    on target delete allow;
  }
}

If the MessageThread linking to the Message is deleted, the source deletion policy would still result in the Message being deleted as long as no other MessageThread objects link to it on their messages link and the deletion isn’t otherwise restricted (e.g., the default policy of on target delete restrict has been overridden, as in the schema above). The object is deleted despite not being orphaned with respect to all links because it is orphaned with respect to the MessageThread type’s messages field, which is the link governed by the deletion policy.

If the Channel type’s messages link had the default policy, the outcome would change.

Copy
type Channel {
  title: str;
  multi messages: Message {
    on target delete allow;
  }
}

With this schema change, the Message object would not be deleted, but not because the message isn’t globally orphaned. Deletion would be prevented because of the default target deletion policy of restrict which would now be in force on the linking Channel object’s messages link.

The limited scope of if orphan holds true even when the two links to an object are from the same type. If MessageThread had two different links both linking to messages — maybe the existing messages link and another called related used to link other related Message objects that are not in the thread — if orphan on a deletion policy on message could result in linked messages being deleted even if they were also linked from another MessageThread object’s related link because they were orphaned with respect to the messages link.