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
Interacting with related records
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">
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 targetted 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">
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 aMarten::DB::Errors::ProtectedRecord
error.:set_null
: This strategy will set the reference column tonull
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
A one-to-one field is really similar to a many-to-one field, but with an additional unicity constraint.
Interacting with related records
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">
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 targetted 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>
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 aMarten::DB::Errors::ProtectedRecord
error.:set_null
: This strategy will set the reference column tonull
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
Interacting with related records
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 targetted 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">]
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.