Skip to main content

Relationships

Contember knows and correctly handles all kinds of relationships - one has one, one has many and many has many.

Quick example

Let's define two entities - a Category and a Post:

export class Category {
name = def.stringColumn();
}
export class Post {
title = def.stringColumn();
content = def.stringColumn();
}

Now just add a relationship field to the Post entity definition:

export class Post {
title = def.stringColumn().notNull();
content = def.stringColumn();
category = def.manyHasOne(Category);
}

That's all. In next sections, you'll find out how to setup inverse side, not null etc.

Types of relationships

We distinguish relationship types - simply "how many entities can be referenced."

One-has-many (and many-has-one)

Owning side of this relationship references (at most) one entity, but that entity can be referenced many times.

one has many relationship

  • We define owning side of this relationship using manyHasOne method.
  • Optionally, we define an inverse side using oneHasMany method.
  • Joining column with actual relationship value is located on owning side.
  • For this relationship, you can also configure:

Use case

This is probably the most common type of relationship.

An example is a Post having a many PostComment, but the PostComment belongs to one single Post. Here, the PostComment is owning side of this relationship, because it holds a Post identifier in its joining column.

Example: Configuring only owning side

export class PostComment {
post = def.manyHasOne(Post)
}
export class Post {
}

Example: Configuring both owning and inverse side

export class PostComment {
post = def.manyHasOne(Post, 'comments')
}

export class Post {
comments = def.oneHasMany(PostComment, 'post')
}

Many-has-many

An owning entity can reference many inverse entities. Also, this inverse entity can be referenced from many owning entities.

many has many relationship

  • Relationship is realized through a joining (also called junction) table.
  • Although there is no joining column, we still recognize owning and inverse side (mainly for configuration purposes).
  • We define owning side of this relationship using manyHasMany method.
  • Optionally, we define an inverse side using manyHasManyInverse method.
  • For this relationship, you can also configure:

Use case

Useful when you need to just connect two entities without any additional metadata. E.g. a Post has many Tags, also there are many Posts of each Tag. Downside is that you cannot attach any information on the relationship between them, e.g. you can't even sort Tags of given Post. In case you need such thing, you'd better create an extra entity representing the relationship (e.g. a PostTag referencing using ManyHasOne both Post and Tag)

Example: Configuring only owning side

export class Post {
tags = def.manyHasMany(Tag)
}
export class Tag {
}

Example: Configuring both owning and inverse side

export class Post {
tags = def.manyHasMany(Tag, 'posts')
}

export class Category {
posts = def.manyHasManyInverse(Post, 'tags')
}

Example: Alternative design with intermediate entity representing the relationship

export class Post {
tags = def.oneHasMany(PostTag, 'post')
}

export class PostTag {
post = def.manyHasOne(Post, 'tags').notNull().cascadeOnDelete()
tag = def.manyHasOne(Tag, 'posts').notNull().cascadeOnDelete()
order = def.intColumn()
}

export class Tag {
posts = def.oneHasMany(PostTag, 'tag')
}

One-has-one

There is at most one entity on each side of this relationship.

one has one relationship

  • We define owning side of this relationship using oneHasOne method.
  • Optionally, we define an inverse side using oneHasOneInverse method.
  • Joining column with actual relationship value is located on owning side.
  • For this relationship, you can also configure

Use case

Not as common, but sometimes useful type of relationship. Imagine entities Post and PostContent - there is always single PostContent entity of each Post and a single Post for each PostContent. In this case, it might seem a bit pointless - all fields PostContent entity can be safely inlined into Post. Let's change it a bit - rename PostContent to Content. Now we can reference this generic Content not only from a Post, but also from e.g. a Category and use same logic for storing, managing and rendering the Content of both entities. In this example, owning side would be in Post and Category entities, optional inverse side in Content.

Example: Configuring only owning side

export class Post {
content = def.oneHasOne(Content)
}
export class Content {
}

Example: Configuring both owning and inverse side

export class Post {
content = def.oneHasOne(Content, 'post')
}

export class Content {
post = def.oneHasOneInverse(Post, 'content')
}

Relationships settings

Nullability

You can also define .notNull() constraint for "one has one" relationships and owning side of "many has one" relationship. This will ensure that there is an entity connected.

Example: making category of post not nullable

export class Post {
category = def.manyHasOne(Category).notNull();
}

On delete behavior

Using .onDelete() you can set what happens when referenced entity is deleted. E.g. you have a post, which is assigned to a category. When a category is deleted, three things can happen:

  • Restrict: this is default behavior. When you try to delete an entity, which is referenced from other entities, the delete operation will fail.
  • Set null: field, which references removed entity, is set to null. Obviously, this is possible only for nullable relationships. You can use shortcut .setNullOnDelete() to select this behavior.
  • Cascade: all entities, which references an entity which is being removed, are also removed. You can use a shortcut .cascadeOnDelete().

Pay attention when you are choosing the strategy, because choosing a wrong strategy may lead to runtime errors or deleting more content than you wanted.

note

In database, all relationships are marked as "NO ACTION" and actual strategy is executed by Contember. This is because Contember can evaluate ACL rules.

Example: setting onDelete cascade

This will delete Post entity when referenced Content is deleted.

export class Post {
content = def.oneHasOne(Content, 'post').cascadeOnDelete()
}

Example: setting onDelete cascade

This will set content relationship to null when referenced Content is deleted

export class Post {
content = def.oneHasOne(Content, 'post').setNullOnDelete()
}

Default order

You can use a method .orderBy() on "has many" relationships to set default order of this relationship. Of course, you can later override this order in a query.

Example: sorting posts in a category by title

export class Category {
title = def.stringColumn();
posts = def.oneHasMany(Post, "category").orderBy("title");
}

export class Post {
title = def.stringColumn().notNull();
category = def.manyHasOne(Category, "posts");
}
note

By calling this method multiple times, you can set subsequent order rules.

export class Category {
title = def.stringColumn();
posts = def.oneHasMany(Post, "category").orderBy("title").orderBy('lead');
}

Orphan removal

Orphan removal is a special behaviour for one-has-one relationships. When you delete an owning side of relationship (e.g. a Post of Content), the inverse side (Content) remains orphaned, meaning it is not referenced from any Post.

By enabling this option, Content will be removed once Post is removed.

Example: enabling orphan removal

export class Post {
content = def.oneHasOne(Content, 'post').removeOrphan()
}