Relationships between models
Non-trivial business application have a structured data model, and need to relate the data from the different entities involved. To do this, we need to use relational fields.
Looking again at our Library app, in the book model we can see the following relationships:
- Each book can have one publisher. This is a many-to-one relationship, implemented in the database engine as a foreign key. The inverse is a one-to-many relationship, meaning that each publisher can have many books.
- Each book can have many authors. That's a many-to-many relationship. The inverse relationship is also a many-to-many, since each author can have many books.
We will explore each of these relationships in the next sections.
A particular case is hierarchical relationships, where records in a Model are related to other records in the same Model. We will introduce a book category model to explain that case.
Finally, the Odoo framework also supports flexible relationships, where a field is able to point to records in different tables. These are called
Reference fields.
A many-to-one relationship is a reference to a record in another model. For example, in the library book model, the
publisher_id field represents the book publisher, and is a reference to a record in the partner model: publisher_id = fields.Many2one(
'res.partner', string='Publisher')
The
Many2one fields, first positional argument is the related model (the comodel keyword argument), as is the case for all relational fields.
The second positional argument is the field label (the
string keyword argument), but this is not the case for the other relational fields, so the preferred option is to always use string as a keyword argument, as we did in the previous code.
A many-to-one Model field creates a field in the database table, with a foreign key to the related table, and holding the database ID of the related record.
ondeletedefines what happens when the related record is deleted:-
set null(the default): an empty value is set when the related record is deleted -
restrictedraises an error preventing the deletion -
cascadewill also delete this record when the related record is deleted
-
contextis a dictionary of data, meaningful for the web client views, to carry information when navigating through the relationship, for example, to set default values. It will be better explained in Chapter 8, Business Logic - Supporting Business Processes.domainis a domain expression: a list of tuples used to filter the records made available for selection on the relation field. See Chapter 8, Business Logic - Supporting Business Processes, for more details.auto_join=Trueallows the ORM to use SQL joins when doing searches using this relationship. If used, the access security rules will be bypassed, and the user could have access to related records the security rules wouldn't allow, but the SQL queries will be more efficient and run faster.delegate=Truecreates a delegation inheritance with the related Model. When used, you must also setrequired=Trueandondelete='cascade'. See Chapter 4, Extending Modules, for more information on delegation inheritance.
A one-to-many relationship is the inverse of the many-to-one. It lists the records of the related Model that have a reference to this record.
For example, in the library book model, the
publisher_id field is a many-to-one relationship with the partner model. This means that the partner model can have a one-to-many inverse relation with the book model, listing the books published by each partner.
To have that relationship available, we can add it in the partner model. Add the
library_app/models/res_partner.py file with this:from odoo import fields, models
class Partner(models.Model):
_inherit = 'res.partner' published_book_ids = fields.One2many(
'library.book', # related model 'publisher_id', # field for "this" on related model string='Published Books')
Since we are adding a new code file to the module, we must not forget to also import it in the
library_app/models/__init__.py file:from . import library_book
from . import res_partner
The
One2many fields accept three positional arguments:- The related model (
comodel_namekeyword argument). - The field in that model referring to this record (
inverse_namekeyword argument). - The field label (
stringkeyword argument).
A many-to-many relationship is used when we have a to-many relationship on both sides. Taking our library books example again, we can find a many-to-many relationship between books and authors: each book can have many authors, and each author can have many books.
On the books side, we have on the
library.book model:class Book(models.Model)
_name = 'library.book'
author_ids = fields.Many2many(
'res.partner', string='Authors')
On the authors side, we can add the inverse relation to the
res.partner model:class Partner(models.Model):
_inherit = 'res.partner'
book_ids = fields.Many2many(
'library.book', string='Authored Books')
The
Many2many minimal signature accepts one positional argument for the related model (the comodel_name keyword argument), and it is strongly recommended to also provide the string argument with the field label.
At the database level, many-to-many relationships don't add any columns to the existing tables. Instead, a special relationship table is automatically created, to store the relations between records. This special table has only two ID fields, with foreign keys for each of the two related tables.
By default, the relationship table name is the two table names joined with an underscore and
_rel appended at the end. In the case of our books or authors relationship, it should be named library_book_res_partner_rel.
On some occasions, we may need to override these automatic defaults. One such case is when the related models have long names, and the name for the automatically generated relationship table is too long, exceeding the 63-character PostgreSQL limit. In these cases, we need to manually choose a name for the relationship table to conform to the table name size limit.
Another case is when we need a second many-to-many relationship between the same models. In these cases, we need to manually provide a name for the relationship table so that it doesn't collide with the table name already being used for the first relationship.
There are two alternatives to manually override these values: either using positional arguments or keyword arguments.
Using positional arguments for the field definition, we have the following:
# Book <-> Authors relation (using positional args)
author_ids = fields.Many2many(
'res.partner', # related model (required) 'library_book_res_partner_rel', # relation table name to use
'a_id', # rel table field for "this" record
'p_id', # rel table field for "other" record
'Authors') # string label text
We can instead use keyword arguments, which may be preferred for readability:
# Book <-> Authors relation (using keyword args)
author_ids = fields.Many2many(
comodel_name='res.partner', # related model (required)
relation='library_book_res_partner_rel', # relation table name
column1='a_id', # rel table field for "this" record
column2='p_id', # rel table field for "other" record
string='Authors') # string label text
Parent-child tree relationships are represented using a many-to-one relationship with the same model, used for each record to references its parent. The inverse one-to-many relation corresponds to the record's direct children.
Odoo provides improved support for these hierarchical data structures, with the additional
child_of and parent_of operators available in domain expressions. These operators are available as long as the model has a parent_id field (or has a _parent_name valid Model definition).
We can enable faster querying on the hierarchy tree by setting the
_parent_store=True Model attribute and adding the parent_path helper field. This fields stores additional information about the hierarchy tree structure that is leveraged for faster queries.Note
Changed on Odoo 12 The parent_path helper field was introduced in Odoo 12. Previous versions used the parent_left and parent_right integer fields for the same purpose. These are deprecated as of Odoo 12.
Be aware that these additional operations come with storage and execution time penalties, so they are best used when you expect to read more frequently than write, such as in the case of category trees. This is only necessary when optimizing deep hierarchies with many nodes, and can be misused for small or shallow hierarchies.
To showcase hierarchical structures, we will add a Category tree to the Library app, to be used to categorize our Books. For this, we will add the
library_app/models/library_book_category.py file with this code:from odoo import api, fields, models
class BookCategory(models.Model):
_name = 'library.book.category'
_description = 'Book Category'
_parent_store = True
name = fields.Char(translate=True, required=True)
# Hierarchy fields
parent_id = fields.Many2one(
'library.book.category',
'Parent Category',
ondelete='restrict')
parent_path = fields.Char(index=True)
# Optional but good to have:
child_ids = fields.One2many(
'library.book.category',
'parent_id',
'Subcategories')
Here, we have a basic model with a
parent_id field to reference the parent record.
To enable the indexing of the hierarchy, for faster tree search, we add the
_parent_store=True model attribute. When doing so, the parent_path field must also be added, and it must be indexed. The field used to refer to the parent is expected to be named parent_id, but any other field name can be used as long as we declare that in the _parent_name optional model attribute.
It is often convenient to add a field to list the direct children. This is the one-to-many inverse relation seen in the previous code.
For the previous code to be used by our module, remember to add a reference to its file in
library_app/models/__init__.py:from . import library_book_category
from . import library_book
from . import res_partner
Regular relational fields reference one fixed co-model. The
Reference field type does not have this limitation and supports flexible relationships, so that the same field is not restricted to always be pointing to the same destination model.
As an example, we will use it in our book category model to add a reference to a highlighted book or author. So, the field could refer to either a book or a partner:
# class BookCategory(models.Model):
highlighted_id = fields.Reference(
[('library.book', 'Book'), ('res.partner', 'Author')],
'Category Highlight',
)
The field definition is similar to a
selection field, but here the selection list holds the models available to be used on the field. In the user interface, the user will first pick a model from the available list, and then pick a specific record from that model.Note
Changed in Odoo 12The referenceable models configuration table was removed. In previous Odoo versions, it could be used to configure the models that can be used in Reference fields. It was available in the Settings | Technical | Database Structure menu. These configuration could be leveraged in Reference field using the odoo.addons.res.res_request.referenceable_models function in place of the model selection list.
Here are a few additional technical details about reference fields that can be useful:
- Reference fields are stored in the database as a
model,idstring - the
read()method, meant for use from external applications, returns them formatted as a('model_name', id)tuple, instead of the usual(id, 'display_name')pair for many-to-one fields
Comments
Post a Comment