Skip to main content
Version: Next

Relationships

Marten offers a powerful and intuitive solution for defining the three most common types of database relationships (many-to-one, one-to-one, and many-to-many) through the use of model fields. By leveraging these special fields, developers can enhance their application's data modeling and streamline data access.

Many-to-one relationships

Many-to-one relationships can be defined through the use of many_to_one fields. This special field type requires the utilization of the to argument, allowing to explicitly define the target model class associated with the current model.

For example, an Article model could have a many-to-one field towards an Author model. In such case, an Article record would only have one associated Author record, but every Author record could be associated with many Article records:

class Author < Marten::Model
field :id, :big_int, primary_key: true, auto: true
field :full_name, :string, max_size: 128
end

class Article < Marten::Model
field :id, :big_int, primary_key: true, auto: true
field :title, :string, max_size: 128
field :author, :many_to_one, to: Author
end

Like for any other model fields, Marten automatically generates getters and setters allowing to interact with the field's value.

With the above snippet, it would be possible to access the Author record associated with a specific Article record by leveraging the #author and #author= methods. For example:

# Create two authors
author_1 = Author.create!(full_name: "Foo Bar")
author_2 = Author.create!(full_name: "John Doe")

# Create an article
article = Article.create!(title: "First article", author: author_1)
article.author!.id # => 1
article.author # => #<Author:0x101590c40 id: 1, full_name: "Foo Bar">

# Change the article author
article.author = author_2
article.save!
article.author!.id # => 2
article.author # => #<Author:0x101590c41 id: 2, full_name: "John Doe">
tip

Note that you can also access the related record's ID directly without actually loading it by leveraging the #<field_name>_id method (which corresponds to the actual name of the column used to persist the reference to the related record's primary key in the model table).

For instance, using the model definitions provided earlier, you could perform the following operation:

author = Author.create!(full_name: "Foo Bar")
article = Article.create!(title: "First article", author: author)
article.author_id # => 1

Backward relations

By default, many_to_one fields do not establish a backward relation. This means that you cannot directly retrieve records that target a specific related record starting from the related record itself. For instance, by default, it is not possible to retrieve all the Article records associated with a specific Author record.

To enable this capability, you need to make use of the related argument when defining your many_to_one field. For instance, we could modify the previous model definitions as follows in order to define an articles backward relation and to let Author records expose their related Article records:

class Author < Marten::Model
field :id, :big_int, primary_key: true, auto: true
field :full_name, :string, max_size: 128
end

class Article < Marten::Model
field :id, :big_int, primary_key: true, auto: true
field :title, :string, max_size: 128
field :author, :many_to_one, to: Author, related: :articles
end

When the related argument is used, a method will be automatically created on the targeted model by using the chosen argument's value. For example, this means that all the Article records associated with a specific Author record will be accessible through the use of the Author#articles method:

# Create two authors
author_1 = Author.create!(full_name: "Foo Bar")
author_2 = Author.create!(full_name: "John Doe")

# Create articles
article_1 = Article.create!(title: "First article", author: author_1)
article_2 = Article.create!(title: "Second article", author: author_2)
article_3 = Article.create!(title: "Third article", author: author_1)

# List the first author's articles
author_1.articles.to_a # [#<Article:0x1036e3ee0 id: 1, title: "First article", author_id: 1>,
# #<Article:0x1036e3e70 id: 3, title: "Third article", author_id: 1>]

# Create an article associated with the first author
article_4 = author_1.articles.create!(title: "Fourth article")
article_4.author # => #<Author:0x101590c40 id: 1, full_name: "Foo Bar">
tip

The method generated for the backward relation returns a query set that you can use to further filter the list of records. For example:

author.articles.filter(title__startswith: "Top")

Deletion strategy

When defining many_to_one fields, it is highly advisable to specify a deletion strategy for the associated relation. This configuration determines the behavior of records with many-to-one fields when one of the records referred to by such fields gets deleted.

Such behavior can be configured by leveraging the on_delete argument when defining many_to_one fields. This argument allows specifying the deletion strategy to adopt when a related record (one that is targeted by the many_to_one field) is deleted. This argument accepts the following values (expressed as symbols):

  • :do_nothing: This is the default strategy. With this strategy, Marten won't do anything to ensure that records referencing the record being deleted are deleted or updated. If the database enforces referential integrity (which will be the case for foreign key fields), this means that deleting a record could result in database errors.
  • :cascade: This strategy can be used to perform cascade deletions. When deleting a record, Marten will try to first destroy the other records that reference the object being deleted.
  • :protect: This strategy allows explicitly preventing the deletion of records if they are referenced by other records. This means that attempting to delete a "protected" record will result in a Marten::DB::Errors::ProtectedRecord error.
  • :set_null: This strategy will set the reference column to null when the related record is deleted.

For example, we could modify our previous model definition so that Article records are cascade-deleted if the associated Author records are destroyed:

class Author < Marten::Model
field :id, :big_int, primary_key: true, auto: true
field :full_name, :string, max_size: 128
end

class Article < Marten::Model
field :id, :big_int, primary_key: true, auto: true
field :title, :string, max_size: 128
field :author, :many_to_one, to: Author, related: :articles, on_delete: :cascade
end

With this change, if we try to delete an Author record, we should notice that the associated Article records are deleted as well:

# Create two authors
author_1 = Author.create!(full_name: "Foo Bar")
author_2 = Author.create!(full_name: "John Doe")

# Create articles
article_1 = Article.create!(title: "First article", author: author_1)
article_2 = Article.create!(title: "Second article", author: author_2)
article_3 = Article.create!(title: "Third article", author: author_1)

# Delete the first author
author_1.delete

article_1.reload # => raises Marten::DB::Errors::RecordNotFound

One-to-one relationships

One-to-one relationships can be defined through the use of one_to_one fields. This special field type requires the utilization of the to argument, allowing to explicitly define the target model class associated with the current model.

For example, a User model could have a one-to-one field towards a Profile model. In such case, the User model could only have one associated Profile record, and the reverse would be true as well (a Profile record could only have one associated User record):

class Profile < Marten::Model
field :id, :big_int, primary_key: true, auto: true
field :full_name, :string, max_size: 128
end

class User < Marten::Model
field :id, :big_int, primary_key: true, auto: true
field :email, :email
field :profile, :one_to_one, to: Profile
end
info

A one-to-one field is really similar to a many-to-one field, but with an additional unicity constraint.

Like for any other model fields, Marten automatically generates getters and setters allowing to interact with the field's value.

With the above snippet, it would be possible to access the Profile record associated with a specific User record by leveraging the #profile and #profile= methods. For example:

# Create two users
user_1 = User.create!(email: "[email protected]", profile: Profile.create!(full_name: "Foo Bar"))
user_2 = User.create!(email: "[email protected]", profile: Profile.create!(full_name: "John Doe"))

# Access a user's profile
user_1.profile!.id # => 1
user_1.profile # => #<Profile:0x101590c40 id: 1, full_name: "Foo Bar">

# Change a user's profile
user_1.profile = Profile.create!(full_name: "New Profile")
user_1.save!
user_1.profile!.id # => 3
user_1.profile # => #<Profile:0x101590c41 id: 3, full_name: "New Profile">
tip

Like for many-to-one relationships, you can also access the related record's ID directly without actually loading it by leveraging the #<field_name>_id method (which corresponds to the actual name of the column used to persist the reference to the related record's primary key in the model table).

For instance, using the model definitions provided earlier, you could perform the following operation:

user = User.create!(email: "[email protected]", profile: Profile.create!(full_name: "Foo Bar"))
user.profile_id # => 1

Backward relations

By default, one_to_one fields do not establish a backward relation. This means that you cannot directly retrieve the record that targets a specific related record starting from the related record itself. For instance, by default, it is not possible to retrieve the User record associated with a specific Profile record.

To enable this capability, you need to make use of the related argument when defining your one_to_one field. For instance, we could modify the previous model definitions as follows in order to define a user backward relation and to let Profile records expose their related User record:

class Profile < Marten::Model
field :id, :big_int, primary_key: true, auto: true
field :full_name, :string, max_size: 128
end

class User < Marten::Model
field :id, :big_int, primary_key: true, auto: true
field :email, :email
field :profile, :one_to_one, to: Profile, related: :user
end

When the related argument is used, a method will be automatically created on the targeted model by using the chosen argument's value. For example, this means that the User record associated with a specific Profile record will be accessible through the use of the Profile#user method:

# Create two profiles
profile_1 = Profile.create!(full_name: "Foo Bar")
profile_2 = Profile.create!(full_name: "John Doe")

# Create two users
user_1 = User.create!(email: "[email protected]", profile: profile_1)
user_2 = User.create!(email: "[email protected]", profile: profile_2)

# Get the first profile's user
profile_1.user # => #<User:0x1036e3ee0 id: 1, email: "[email protected]", profile_id: 1>
tip

Note that in the previous example, #user could return nil if no User record is available for the considered profile. A nil-safe version of the related method is also automatically defined with the following name: #<related_name>!. For example:

# Create two profiles
profile_1 = Profile.create!(full_name: "Foo Bar")
profile_2 = Profile.create!(full_name: "John Doe")

# Create two users
user_1 = User.create!(email: "[email protected]", profile: profile_1)
user_2 = User.create!(email: "[email protected]", profile: profile_2)

# Delete the first user
user_1.delete

# Get the first profile's user
profile_1.user! # => raises Marten::DB::Errors::RecordNotFound

Deletion strategy

Like for many-to-one relationships, the deletion strategy to use for one_to_one fields can be configured by leveraging the on_delete argument. This argument allows specifying the deletion strategy to adopt when a related record (one that is targeted by the many_to_one field) is deleted. This argument accepts the following values (expressed as symbols):

  • :do_nothing: This is the default strategy. With this strategy, Marten won't do anything to ensure that the record referencing the record being deleted is deleted or updated. If the database enforces referential integrity (which will be the case for foreign key fields), this means that deleting a record could result in database errors.
  • :cascade: This strategy can be used to perform cascade deletions. When deleting a record, Marten will try to first destroy the other record that references the object being deleted.
  • :protect: This strategy allows explicitly preventing the deletion of the record if is is referenced by another record. This means that attempting to delete a "protected" record will result in a Marten::DB::Errors::ProtectedRecord error.
  • :set_null: This strategy will set the reference column to null when the related record is deleted.

For example, we could modify our previous model definition so that a User record is cascade-deleted if the associated Profile records is destroyed:

class Profile < Marten::Model
field :id, :big_int, primary_key: true, auto: true
field :full_name, :string, max_size: 128
end

class User < Marten::Model
field :id, :big_int, primary_key: true, auto: true
field :email, :email
field :profile, :one_to_one, to: Profile, related: :user, on_delete: :cascade
end

With this change, if we try to delete a Profile record, we should notice that the associated User records is deleted as well:

# Create two profiles
profile_1 = Profile.create!(full_name: "Foo Bar")
profile_2 = Profile.create!(full_name: "John Doe")

# Create two users
user_1 = User.create!(email: "[email protected]", profile: profile_1)
user_2 = User.create!(email: "[email protected]", profile: profile_2)

# Delete the first profile
profile_1.delete

user_1.reload # => raises Marten::DB::Errors::RecordNotFound

Many-to-many relationships

Many-to-many relationships can be defined through the use of many_to_many fields. This special field type requires the utilization of the to argument, allowing to explicitly define the target model class associated with the current model.

For example, an Article model could have a many-to-many field towards a Tag model. In such case, an Article record could have many associated Tag records, and every Tag record could be associated with many Article records as well:

class Tag < Marten::Model
field :id, :big_int, primary_key: true, auto: true
field :label, :string, max_size: 128
end

class Article < Marten::Model
field :id, :big_int, primary_key: true, auto: true
field :title, :string, max_size: 128
field :tags, :many_to_many, to: Tag
end

many_to_many fields exhibit unique characteristics compared to other relationship fields. When using many_to_many fields in Marten, the framework generates a #<field_name> getter method that returns a specialized query set that not only enables filtering of targeted records but also facilitates the dynamic addition and removal of records to/from the set.

With the above snippet, it would be possible to access the Tags records associated with a specific Article record by leveraging the #tags method. For example:

# Create three tags
tag_1 = Tag.create!(label: "Tag 1")
tag_2 = Tag.create!(label: "Tag 2")
tag_3 = Tag.create!(label: "Tag 3")

# Create one article
article = Article.create!(title: "My article")

# Add one tag to the article
article.tags.add(tag_1)
article.tags.to_a # => [#<Tag:0x1036e3ee0 id: 1, label: "Tag 1">]

# Add two tags to the article
article.tags.add(tag_2, tag_3)
article.tags.to_a # => [#<Tag:0x1036e3ee0 id: 1, label: "Tag 1">,
# #<Tag:0x1036e3ee1 id: 2, label: "Tag 2">,
# #<Tag:0x1036e3ee2 id: 3, label: "Tag 3">]

# Filter the article's tags
article.tags.filter(label: "Tag 1").to_a # => [#<Tag:0x1036e3ee0 id: 1, label: "Tag 1">]

# Remove a tag from the article's tags
article.tags.remove(tag_2)
article.tags.to_a # => [#<Tag:0x1036e3ee0 id: 1, label: "Tag 1">,
# #<Tag:0x1036e3ee2 id: 3, label: "Tag 3">]

# Clear the article's tags
article.tags.clear

Take note of the utilization of the #add and #remove methods, facilitating the addition or removal of objects from the record's many-to-many collection of associated items. These methods are callable with single or multiple records as parameters, as well as with arrays of records for streamlined addition or removal.

Backward relations

By default, many_to_many fields do not establish a backward relation. This means that you cannot directly retrieve records that target a specific related record starting from the related record itself. For instance, by default, it is not possible to retrieve all the Article records associated with a specific Tag record.

To enable this capability, you need to make use of the related argument when defining your many_to_many field. For instance, we could modify the previous model definitions as follows in order to define an articles backward relation and to let Tag records expose their related Article records:

class Tag < Marten::Model
field :id, :big_int, primary_key: true, auto: true
field :label, :string, max_size: 128
end

class Article < Marten::Model
field :id, :big_int, primary_key: true, auto: true
field :title, :string, max_size: 128
field :tags, :many_to_many, to: Tag, related: :articles
end

When the related argument is used, a method will be automatically created on the targeted model by using the chosen argument's value. For example, this means that all the Article records associated with a specific Tag record will be accessible through the use of the Tag#articles method:

# Create three tags
tag_1 = Tag.create!(label: "Tag 1")
tag_2 = Tag.create!(label: "Tag 2")
tag_3 = Tag.create!(label: "Tag 3")

# Create two articles
article_1 = Article.create!(title: "First article")
article_2 = Article.create!(title: "Second article")

# Add tags to the articles
article_1.tags.add(tag_1, tag_2)
article_2.tags.add(tag_2, tag_3)

# Retrieve the second tag's articles
tag_2.articles.to_a # => [#<Article:0x1036e3ee0 id: 1, title: "First article">,
# #<Article:0x1036e3ee2 id: 3, title: "Second article">]
tag_2.articles.filter(title: "First article").to_a # => [#<Article:0x1036e3ee0 id: 1, title: "First article">]

Polymorphic relationships

Polymorphic relationships can be defined through the use of polymorphic fields. Those are useful when you want to store a reference to a record whose model can vary among a predefined set of possible types.

This special field type requires the utilization of the to argument, allowing to explicitly define the model classes that can be related to the model where the polymorphic field is defined. For example, a Comment model could have a polymorphic field towards an Article or a Recipe model. In such case, a Comment record could be associated with an Article or a Recipe record, and each of these models could have many associated Comment records:

class Article < Marten::Model
field :id, :big_int, primary_key: true, auto: true
field :title, :string, max_size: 128
end

class Recipe < Marten::Model
field :id, :big_int, primary_key: true, auto: true
field :title, :string, max_size: 128
end

class Comment < Marten::Model
field :id, :big_int, primary_key: true, auto: true
field :target, :polymorphic, to: [Article, Recipe]
field :text, :text
end

Under the hood, the framework keeps track of both the target object's primary key and its model type, allowing it to resolve the relationship dynamically when accessed. This means that polymorphic fields contribute two columns to the model table: <field_name>_type and <field_name>_id, where field_name is the name of the polymorphic field. The _type column is used to store the type of the related record (the class name of the related record), and the _id column is used to store the ID of the related record. In the previous example, the Comment model would have two columns named target_type and target_id because of the target polymorphic field.

Marten automatically generates getters and setters for polymorphic fields, allowing to interact with the field's value. On top of that, Marten also generates a set of methods allowing to access the related record based on its type as well as numerous helper methods.

For example:

# Create an article
article = Article.create!(title: "This is an article")

# Create a recipe
recipe = Recipe.create!(title: "This is a recipe")

# Create a comment
comment = Comment.create!(text: "This is a comment", target: article)

# Regular getter methods
comment.target # => #<Article:0x1036e3ee0 id: 1, title: "This is an article">
comment.target_type # => "Article"
comment.target_id # => 1

# Type class getter method
comment.target_class # => Article (or nil if no related record is set)
comment.target_class! # => Article (or raise if no related record is set)

# Predicate helper methods
comment.article_target? # => true
comment.recipe_target? # => false

# Typed getters methods
comment.article_target # => Returns the associated Recipe record if the targeted record is indeed a Recipe record (or nil otherwise)
comment.article_target! # => Returns the associated Recipe record if the targeted record is indeed a Recipe record (or raise otherwise)

# Type-specific model scopes (generated based on the specified type classes)
Comment.with_article_target # => Returns all the comments associated with Article records
Comment.with_recipe_target # => Returns all the comments associated with Recipe records

Backward relations

By default, polymorphic fields do not establish a backward relation. This means that you cannot directly retrieve records that target a specific related record starting from the related record itself. For instance, by default, it is not possible to retrieve all the Comment records associated with a specific Article or Recipe record.

To enable this capability, you need to make use of the related argument when defining your polymorphic field. For instance, we could modify the previous model definitions as follows in order to define a comments backward relation and to let Article and Recipe records expose their related Comment records:

class Comment < Marten::Model
field :id, :big_int, primary_key: true, auto: true
field :target, :polymorphic, to: [Article, Recipe], related: :comments
field :text, :text
end

When the related argument is used, a method will be automatically created on the targeted model by using the chosen argument's value. For example, this means that all the Comment records associated with a specific Article or Recipe record will be accessible through the use of the Article#comments or Recipe#comments methods:

# Create an article
article = Article.create!(title: "This is an article")

# Create a recipe
recipe = Recipe.create!(title: "This is a recipe")

# Create comments
Comment.create!(text: "This is a comment", target: article)
Comment.create!(text: "This is a comment", target: recipe)

# Get the article's comments
article.comments.to_a # => [#<Comment:0x1036e3ee0 id: 1, text: "This is a comment">]

# Get the recipe's comments
recipe.comments.to_a # => [#<Comment:0x1036e3ee1 id: 2, text: "This is a comment">]
tip

The method generated for the backward relation returns a query set that you can use to further filter the list of records. For example:

article.comments.filter(text__startswith: "This is")

Advanced topics

Recursive relationships

All the relationship fields mentioned previously support defining recursive relations, ie. relations that target the same model as the model defining the relation field. To do so, you can define a many_to_one, one_to_one, or many_to_many field whose to argument is set to the self keyword.

For example:

class TreeNode < Marten::Model
field :id, :big_int, primary_key: true, auto: true
field :label, :string, max_size: 128
field :parent, :many_to_one, to: self
end

In the above snippet, the TreeNode model will have a relation to itself through the parent field.