Search...
ctrl/
Light
Dark
System
Sign in

Links

Links define a relationship between two object types in Gel.

Links in Gel are incredibly powerful and flexible. They can be used to model relationships of any cardinality, can be traversed in both directions, can be polymorphic, can have constraints, and many other things.

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;
  }
}

With exclusive on GroupChat.members, two GroupChat objects cannot link to the same Person; put differently, no Person can be a member of multiple GroupChat objects.

Links can declare a default value in the form of an EdgeQL expression, which will be executed upon insertion. In this example, 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);
  }
}

By combining 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, known as a backlink. See the select docs to learn more.

Conceptually, one-to-many and many-to-one relationships are identical; the "directionality" is 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! Without it, the relationship becomes 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. Should you model this relationship as one-to-many (with a multi link) or as many-to-one (with a single link and a backlink)? A general rule of thumb:

  • Use a multi link if the relationship is relatively stable and not updated frequently, and the set of related objects is typically small. For example, a list of postal addresses in a user profile.

  • Otherwise, prefer a single link from one object type and a computed backlink on the other. This can be more efficient and is generally recommended for 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. The exclusive constraint guarantees that a given ParkingSpace can't be assigned to multiple employees. Together, these form a one-to-one relationship.

A many-to-many relation is the least constrained kind of relationship. There is no exclusivity or cardinality constraint in either direction. As an example, consider a simple app where a User can "like" their favorite Movie:

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, creating a many-to-many relationship.

Links are always distinct. It's not possible to link the same objects twice. For example:

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

type Movie {
  required title: str;
}

In this model, a user can't watch the same movie more than once (the link from a specific user to a specific movie can exist only once). One approach is to store multiple timestamps in an array on the link property:

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

Alternatively, you might introduce a dedicated 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;
}

Remember to use single links in the join table so you don't end up with extra tables.

Links can declare their own deletion policy for when the target or source is deleted.

The clause on target delete determines the action when the target object is deleted:

  • restrict (default) — raises an exception if the target is deleted.

  • delete source — deletes the source when the target is deleted (a cascade).

  • allow — removes the target from the link if the target is deleted.

  • deferred restrict — like restrict but defers the error until the end of the transaction if the object remains linked.

Copy
type MessageThread {
  title: str;
}

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

The clause on source delete determines the action when the source is deleted:

  • allow — deletes the source, removing the link to the target.

  • delete target — unconditionally deletes the target as well.

  • delete target if orphan — deletes the target if and only if it's no longer linked by any other object via the same link.

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

type Message {
  content: str;
}

You can add if orphan if you'd like to avoid deleting a target that remains linked elsewhere via the same link name.

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 even all links from the same type. If another link by a different name or with a different on-target-delete policy points at the same object, it doesn't prevent the object from being considered "orphaned" for the link that includes if orphan.

When an inherited link is modified (by adding more constraints or changing its target type, etc.), the overloaded keyword is required. This prevents unintentional overloading due to name clashes:

Copy
abstract type Friendly {
  # this type can have "friends"
  multi friends: Friendly;
}

type User extending Friendly {
  # overload the link target to to be specifically User
  overloaded multi friends: User;

  # ... other links and properties
}

This section describes the low-level DDL commands for creating, altering, and dropping links. You typically don't need to use these commands directly, but knowing about them is useful for reviewing migrations.