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