Data Model Relationships

In this chapter, you'll learn how to define relationships between data models in your module, and how to manage the related records.

Note: Relationships are between data models of the same module. To relate the data models of different modules, such as your module's data model and a product, use module links.

What is a Relationship Property?#

A relationship property of a data model defines a relation to another data model of the same module. In the database, it's a foreign key column, or a pivot table.

You create a relationship property in the schema of a data model with one of these functions of the dml package, which take the name of the related data model, and options:

| Function | Relationship | | --- | --- | | dml.HasOne and dml.BelongsTo | One-to-one. The data model with BelongsTo holds the foreign key. | | dml.HasMany and dml.BelongsTo | One-to-many. The data model with BelongsTo holds the foreign key. | | dml.ManyToMany | Many-to-many, through a pivot table. |

The first argument is the name of the related data model as MedusaGo derives it from its table name: Comment for the comment model, and BlogPost for blog_post. The options are dml.RelationshipOptions. Its MappedBy field is the name of the relationship property in the other data model.


One-to-One Relationship#

A one-to-one relationship between two data models means that a record of the first data model has one record of the second, and the other way around.

You define it with dml.HasOne in one data model and dml.BelongsTo in the other. For example, an author has one profile, and a profile belongs to one author:

src/modules/blog/relationships.go
var Author = dml.Define("author", dml.Schema{
	{Name: "id", Property: dml.Id(dml.IdOptions{Prefix: "author"}).PrimaryKey()},
	{Name: "name", Property: dml.Text()},
	{Name: "profile", Property: dml.HasOne("AuthorProfile", dml.RelationshipOptions{MappedBy: "author"})},
})

var AuthorProfile = dml.Define("author_profile", dml.Schema{
	{Name: "id", Property: dml.Id(dml.IdOptions{Prefix: "aprof"}).PrimaryKey()},
	{Name: "bio", Property: dml.Text().Nullable()},
	{Name: "author", Property: dml.BelongsTo("Author", dml.RelationshipOptions{MappedBy: "profile"})},
})

The Author data model has a profile property, which is a HasOne relationship to the AuthorProfile data model. The AuthorProfile data model has an author property, which is a BelongsTo relationship to Author. Each MappedBy is the name of the property in the other data model.

Optional Relationship#

To make the relationship optional on the HasOne or BelongsTo side, use the Nullable modifier on the property:

src/modules/blog/relationships.go
var Writer = dml.Define("writer", dml.Schema{
	{Name: "id", Property: dml.Id(dml.IdOptions{Prefix: "writer"}).PrimaryKey()},
	{Name: "profile", Property: dml.HasOne("WriterProfile", dml.RelationshipOptions{MappedBy: "writer"}).Nullable()},
})

var WriterProfile = dml.Define("writer_profile", dml.Schema{
	{Name: "id", Property: dml.Id(dml.IdOptions{Prefix: "wprof"}).PrimaryKey()},
	// a profile may belong to no writer
	{Name: "writer", Property: dml.BelongsTo("Writer", dml.RelationshipOptions{MappedBy: "profile"}).Nullable()},
})

On the BelongsTo side, this makes the foreign key column nullable.

One-to-One Relationship in the Database#

When you generate the migrations of data models that have a one-to-one relationship, the migration adds to the table of the data model that has the BelongsTo property:

  1. A column of the format <relation_name>_id that stores the ID of the related record. The author_profile table has an author_id column.
  2. A foreign key on this column to the table of the related data model.
  3. A unique index on the column, so that two profiles can't belong to the same author.

One-sided One-to-One Relationship#

You can define a one-to-one relationship on only one side, when the other data model doesn't need a property that points back.

From the HasOne side, set ForeignKey to true, so that the foreign key column is added to this data model's table, and MappedByUndefined to true, since the relationship has no other side:

src/modules/blog/relationships.go
// The foreign key column is in the user table, and Email has no property
// pointing back to User.
var User = dml.Define("user_account", dml.Schema{
	{Name: "id", Property: dml.Id(dml.IdOptions{Prefix: "usr"}).PrimaryKey()},
	{Name: "email", Property: dml.HasOne("Email", dml.RelationshipOptions{ForeignKey: true, MappedByUndefined: true})},
})

var Email = dml.Define("email", dml.Schema{
	{Name: "id", Property: dml.Id(dml.IdOptions{Prefix: "email"}).PrimaryKey()},
})

The user_account table gets an email_id column, and the Email data model has no property that points to the user.

From the BelongsTo side, set MappedByUndefined to true:

src/modules/blog/relationships.go
// The foreign key column is in the badge table, and Holder has no property
// pointing to Badge.
var Holder = dml.Define("holder", dml.Schema{
	{Name: "id", Property: dml.Id(dml.IdOptions{Prefix: "hold"}).PrimaryKey()},
})

var Badge = dml.Define("badge", dml.Schema{
	{Name: "id", Property: dml.Id(dml.IdOptions{Prefix: "badge"}).PrimaryKey()},
	{Name: "holder", Property: dml.BelongsTo("Holder", dml.RelationshipOptions{MappedByUndefined: true})},
})

The badge table gets a holder_id column, and the Holder data model has no property that points to its badge.


One-to-Many Relationship#

A one-to-many relationship between two data models means that a record of the first has many records of the second, and a record of the second belongs to one record of the first.

You define it with dml.HasMany in the first data model and dml.BelongsTo in the second. For example, a post has many comments, and a comment belongs to one post:

src/modules/blog/models.go
var Comment = dml.Define("comment", dml.Schema{
	{Name: "id", Property: dml.Id(dml.IdOptions{Prefix: "cmt"}).PrimaryKey()},
	{Name: "author_name", Property: dml.Text()},
	{Name: "body", Property: dml.Text()},
	{Name: "post", Property: dml.BelongsTo("Post", dml.RelationshipOptions{MappedBy: "comments"})},
})

The Post data model has a comments property, HasMany("Comment", ...). The Comment data model has a post property, BelongsTo("Post", ...).

Optional Relationship#

To make the relationship optional on the BelongsTo side, use the Nullable modifier on the property, so that a comment can exist without a post.

One-to-Many Relationship in the Database#

When you generate the migrations of data models that have a one-to-many relationship, the migration adds to the table of the data model that has the BelongsTo property:

  1. A column of the format <relation_name>_id, such as post_id in the comment table, that stores the ID of the related record.
  2. An index and a foreign key on this column to the table of the related data model.

Many-to-Many Relationship#

A many-to-many relationship between two data models means that a record of each can be related to many records of the other. You define it with dml.ManyToMany in both data models. For example, a post has many tags, and a tag is on many posts:

src/modules/blog/models.go
var Tag = dml.Define("tag", dml.Schema{
	{Name: "id", Property: dml.Id(dml.IdOptions{Prefix: "tag"}).PrimaryKey()},
	{Name: "value", Property: dml.Text().Unique()},
	{Name: "posts", Property: dml.ManyToMany("Post", dml.RelationshipOptions{MappedBy: "tags"})},
})

The Post data model has the tags property, and the Tag data model has the posts property. Each one's MappedBy is the name of the property in the other.

Many-to-Many Relationship in the Database#

When you generate the migrations of data models that have a many-to-many relationship, the migration creates a pivot table with:

  1. A column for the ID of each of the two data models, such as post_id and tag_id. Together they're the primary key of the pivot table.
  2. A foreign key on each column to the table of its data model, which cascades the deletion.

The pivot table is named after the two tables, in alphabetical order, with the second one in the plural, such as post_tags.

Many-To-Many with Custom Columns#

To add custom columns to the pivot table of a many-to-many relationship, define a data model that represents the pivot table, and pass its name in the PivotEntity option of the relationship on one side. The side with PivotEntity is the owner of the relationship:

src/modules/blog/relationships.go
var Order = dml.Define("order_test", dml.Schema{
	{Name: "id", Property: dml.Id(dml.IdOptions{Prefix: "ord"}).PrimaryKey()},
	{Name: "products", Property: dml.ManyToMany("ProductTest", dml.RelationshipOptions{PivotEntity: "OrdersProducts"})},
})

var ProductTest = dml.Define("product_test", dml.Schema{
	{Name: "id", Property: dml.Id(dml.IdOptions{Prefix: "prd"}).PrimaryKey()},
	{Name: "orders", Property: dml.ManyToMany("OrderTest")},
})

// OrdersProducts is the pivot table of the relationship, with an extra
// column.
var OrdersProducts = dml.Define("orders_products", dml.Schema{
	{Name: "id", Property: dml.Id(dml.IdOptions{Prefix: "op"}).PrimaryKey()},
	{Name: "order", Property: dml.BelongsTo("OrderTest", dml.RelationshipOptions{MappedBy: "products"})},
	{Name: "product", Property: dml.BelongsTo("ProductTest", dml.RelationshipOptions{MappedBy: "orders"})},
	{Name: "metadata", Property: dml.JSON().Nullable()},
})
  • OrderTest.products is a many-to-many relationship, with PivotEntity: "OrdersProducts".
  • ProductTest.orders is the other side of the relationship.
  • OrdersProducts is the data model of the pivot table. It has a BelongsTo relationship to each data model, with MappedBy set to the name of the many-to-many property. It also has a metadata column.

You can define other columns in the pivot data model, like in any data model.


Cascades#

When you perform an operation on a data model, such as deleting a record, a cascade specifies the related records that the operation also applies to. For example, when a post is deleted, its comments should be deleted too.

Use the WithCascades method of the data model to configure its cascades:

src/modules/blog/models.go
var Post = dml.Define("post", dml.Schema{
	{Name: "id", Property: dml.Id(dml.IdOptions{Prefix: "post"}).PrimaryKey()},
	{Name: "title", Property: dml.Text().Searchable()},
	{Name: "slug", Property: dml.Text().Unique()},
	{Name: "content", Property: dml.Text().Nullable()},
	{Name: "status", Property: dml.Enum("draft", "published", "archived").Default("draft")},
	{Name: "views", Property: dml.Number().Default(0)},
	{Name: "published_at", Property: dml.DateTime().Nullable()},
	{Name: "author_id", Property: dml.Text().Nullable()},
	{Name: "metadata", Property: dml.JSON().Nullable()},
	{Name: "comments", Property: dml.HasMany("Comment", dml.RelationshipOptions{MappedBy: "post"})},
	{Name: "tags", Property: dml.ManyToMany("Tag", dml.RelationshipOptions{MappedBy: "posts"})},
}).
	WithIndexes([]dml.Index{{On: []string{"status", "published_at"}}}).
	WithCascades(dml.Cascades{Delete: []string{"comments"}})

The WithCascades method takes a dml.Cascades value. Its Delete field is a list of the names of the relationship properties whose records are deleted with the record. In the example above, when a post is deleted, soft-deleted, or restored, its comments are as well.

Note: You can only cascade from the parent to the child, from the HasMany or HasOne property. WithCascades panics if you cascade a BelongsTo property.

Searchable Relationships#

Add the Searchable modifier to a relationship property to make the searchable properties of the related data model part of the search of the q filter. For example, a category whose entries are searchable is found when a search term matches the title of one of its entries:

src/modules/blog/relationships.go
var Category = dml.Define("category", dml.Schema{
	{Name: "id", Property: dml.Id(dml.IdOptions{Prefix: "cat"}).PrimaryKey()},
	{Name: "name", Property: dml.Text()},
	{Name: "entries", Property: dml.HasMany("Entry", dml.RelationshipOptions{MappedBy: "category"}).Searchable()},
})

var Entry = dml.Define("entry", dml.Schema{
	{Name: "id", Property: dml.Id(dml.IdOptions{Prefix: "entry"}).PrimaryKey()},
	{Name: "title", Property: dml.Text().Searchable()},
	{Name: "category", Property: dml.BelongsTo("Category", dml.RelationshipOptions{MappedBy: "entries"})},
})

Manage Relationships#

You manage the related records with the methods of the service factory.

When you create a record, you can create the records of its HasMany relationships in the same call, with the property of the relationship. To create a record of the BelongsTo side, set its foreign key, such as post_id:

src/modules/blog/usage.go
// createRelated creates a post with its comments, then a comment of another
// post through its foreign key.
func createRelated(ctx context.Context, s *BlogModuleService) error {
	_, err := s.Call(ctx, "createPosts", []any{
		rec(
			"title", "Hello, world",
			"slug", "hello-world",
			"comments", []any{
				rec("author_name", "Ada", "body", "Great post"),
				rec("author_name", "Grace", "body", "Thanks for sharing"),
			},
		),
	})
	if err != nil {
		return err
	}

	_, err = s.Call(ctx, "createComments", []any{
		rec("author_name", "Linus", "body", "Nice", "post_id", "post_123"),
	})
	return err
}

For a many-to-many relationship, the property of the relationship takes a list of the IDs of existing records, or of records to create and relate.

Pass the names of the relationships in the relations key of the configuration of retrieve and list, to load the related records with each record:

src/modules/blog/usage.go
// postWithRelations retrieves a post with its comments and tags.
func postWithRelations(ctx context.Context, s *BlogModuleService, id string) (*dal.Record, error) {
	post, err := s.Call(ctx, "retrievePost", []any{
		id,
		rec("relations", []any{"comments", "tags"}),
	})
	if err != nil {
		return nil, err
	}
	return post.(*dal.Record), nil
}

Replace the Records of a Many-to-Many Relationship#

To change the records related through a many-to-many relationship, use the ReplaceManyToMany method of the internal service of the data model that owns the relationship. It relates the record to the IDs you pass, and removes any other relation:

src/modules/blog/usage.go
// replaceTags relates a post to the tags with the given IDs, and removes its
// other tags.
func replaceTags(ctx context.Context, s *BlogModuleService) error {
	return s.Internal("Post").ReplaceManyToMany(ctx, "post_123", "tags", []any{"tag_1", "tag_2"})
}

The SetPostTags method of the Blog Module, shown in the Database Operations chapter, does it in a transaction.

Query retrieves a record with its relationships too, using the name of the relationship in the fields, such as comments.*.

Found a problem with this page?Report Issue
Edited Nov 14·Edit this page