Relate two content types
The content-type system in LocomotiveCMS allows you to define and use various types of relations between models.
Let's go through classical Author <-> Book example to learn each relation type provided by LocomotiveCMS. First we're going to have the very basic models of Authors and Books containing only name fields:
wagon generate content_type authors name:string
wagon generate content_type books name:string
Belongs To: the Book belongs to the Author
With this type of relation we can store a link to an Author along with the Book, which immediately becomes callable as a property of the book instance:
Who wrote the book "{{ book.name }}"? {{ book.author.name }} did that!
"Belongs To" is the easiest relation to set up. You just need to add the following to app/content_types/books.yml:
- author:
label: Author
type: belongs_to
class_name: authors
The key author
here is a property that we will call on a book instance. It does not have to match the name of related model. For example, we could name our relation writer
.
The class_name
parameter (vice versa) must contain the slug of the model; otherwise, we couldn't know which model exactly we want to bind here.
Also, we may want to add the required: true
option if the book without an author shouldn't exist in our library.
You could also have used the Wagon relationship generator:
bundle exec wagon generate relationship books belongs_to authors
This also generates the has_many relationship as explained in the next chapter.
Ok. Once we're done, we may want to add some records to see how it works. Let's add the following to the data/authors.yml:
- "Leo Tolstoy"
- "Anton Tchekhov"
Internally, and for SEO purposes, Locomotive will convert these keys in computer readable slugs.
Leo Tolstoy will become leo-tolstoy and Anton Tchekhov will become anton-tchekhov
Then add these records to the data/books.yml:
- "War and Peace":
author: leo-tolstoy
- "The Cherry Orchard":
author: anton-tchekhov
- "Anna Karenina":
author: leo-tolstoy
Defining a relationship between content type entries in YAML data files
Have you noticed? The relationship between the records in data/authors.yml and data/books.yml is made through the subkey "author" of the books.
And beware: the value of such subkey needs to be defined by using the slugs automatically created by Locomotive.
If you use natural human readable language, the relationship will not work. So be careful here.
Now if we add the following snippet to the index page...
{% for book in contents.books %}
Who wrote the book "{{ book.name }}"? {{ book.author.name }} did that!
{% endfor %}
...we'll see the list of the books with their authors:
Who wrote the book "War and Peace"? Leo Tolstoy did that!
Who wrote the book "The Cherry Orchard"? Anton Tchekhov did that!
Who wrote the book "Anna Karenina"? Leo Tolstoy did that!
As you may notice it's perfectly legal to create an arbitrary amount of Books pointing to one Author. And this brings us to the next type of relation.
Has Many: the Author has many Books
It is quite unusual for the Author to have only one Book published. That's why we need another type of relation to describe how Books are connected to the Author.
We did most of the work for establishing has_many
relation in the last section. From the point of view of the root object this type of relation is no more than an inverted belongs_to. This structure is quite obvious: book belongs
to author and author has many books belonging to the author. That's it! We've come full circle.
Let's add a books
property to app/content_type/authors.yml:
- books:
label: Books
type: has_many
class_name: books
inverse_of: author
ui_enabled: true
Some new parameters are introduced here. The parameter inverse_of specifies which property of the foreign model is used to establish the belongs_to relation with the current model. Its name is self-explanatory: `what property of the RELATED model would be an inversion of the property that we're going to define on THIS model?
Or what property of the related model could tell us if we're connected?
It makes perfect sense if you just imagine the Tolstoy record asking Anna Karenina: "Is that me who has created you?"
The last parameter ui_enabled allows you to select whether the related item should be editable right from the editing screen of the current model in the back-office. If you turn on this option you'll be able to conveniently edit or create related items during editing or creation of the root item.
Let's take a look at how it works. To do so, we'll take the first author which happens to be Tolstoy and ask him which books he wrote:
{% for book in contents.authors.first.books %}
Who wrote the book "{{ book.name }}"? {{ book.author.name }} did that!
{% endfor %}
The output would be the following, as expected:
Who wrote the book "War and Peace"? Leo Tolstoy did that!
Who wrote the book "Anna Karenina"? Leo Tolstoy did that!
Many to Many: the case of the Book written in collaboration
Let's imagine that Tolstoy and Tchekhov wrote a book in co-authorship which in fact never happened even though they lived in the same time. But for learning purpose it would be fun to think that their partnership brought a brilliant book called "Anna Karenina in the Cherry Orchard" to the world.
It breaks our perfect world in no time. Now one Author can have many Books, and one Book can be written by a group of Authors. This type of relation is called Many-to-Many and it is considered the most complex relation.
Let's make some changes in app/content_type/books.yml
- authors:
label: Author
type: many_to_many
class_name: authors
inverse_of: books
ui_enabled: true
...and app/content_type/authors.yml
- books:
label: Books
type: many_to_many
class_name: books
inverse_of: authors
ui_enabled: true
That's it. Now we can attach as many Books to each Author as we want, and vice versa.
The only thing left is to take a look at the seed data for this kind of relation (data/books.yml):
- "War and Peace":
authors: [leo-tolstoy]
- "The Cherry Orchard":
authors: [anton-tchekhov]
- "Anna Karenina":
authors: [leo-tolstoy]
- "Anna Karenina in the Cherry Orchard":
authors: [leo-tolstoy, anton-tchekhov]
Use the slugs!
Have you noticed? In the array making the references to the authors collection, we use the slugs of the authors.
Now if we take the last book and iterate over authors, we will see Leo Tolstoy and Anton Tchekhov
:
{% for author in contents.books.last.authors %}
{{ author.name }}
{% endfor %}
will output
Leo Tolstoy
Anton Tchekhov
Tags: special relation
Locomotive has embedded support for tags. To use them, you just have to add a field of type tags to your model. Then you'll be able to scope by tags, like this:
{% with_scope tags: params['tag'] %}
...
{% endwith_scope %}
A common mistake with relations involves mistreatment of the property. Just remember, that for any relation except the belongs_to
you have a collection even though it consists of only one element.
Updated over 4 years ago