Skip to main content

Relationships between models



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.

Many-to-one relationships

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.
The following keyword arguments specific to many-to-one fields can also be used:
  • ondelete defines what happens when the related record is deleted:
    •  set null  (the default): an empty value is set when the related record is deleted
    •  restricted raises an error preventing the deletion
    •  cascade  will also delete this record when the related record is deleted
  • context is 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.
  • domain is 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=True allows 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=True creates a delegation inheritance with the related Model. When used, you must also set required=True and ondelete='cascade'. See Chapter 4, Extending Modules, for more information on delegation inheritance.

One-to-many inverse relationships

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_name  keyword argument).
  • The field in that model referring to this record (inverse_name keyword argument).
  • The field label (string keyword argument).
The additional keyword arguments available are the same as for many-to-one fields:  context, domain, and ondelete (here acting on the many side of the relationship).

Many-to-many relationships

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
Similarly to one-to-many relational fields, many-to-many fields can also use the keyword arguments context, domain, and auto_join.

Note

When creating abstract models, don't use the many-to-many field's  column1 and column2 attributes . There is a limitation in the ORM design regarding abstract models, and when you force the names of the relationship columns, they cannot be cleanly inherited anymore.

Hierarchical relationships

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



Flexible relationships using Reference fields

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,id string
  • 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

Popular posts from this blog

The message and activity features

The message and activity features Odoo has available global  messaging  and activity planning features, provided by the  Discuss  application, with technical name mail. The mail module provides the  mail.thread  abstract class that makes it simple to add the messaging features to any model, and the  mail.activity.mixin  that adds planned activity features. This was done in  Chapter 4 ,  Extending Modules , to explain how to inherit features from mixin abstract classes. To add these features, we need to add the mail dependency to the add-on module,  library_checkout , and then have the library checkout model class inherit from the abstract classes providing the following features. Edit the  'depends'  key in the  library_checkout/__manifest__.py   file, to add the mail module, shown as follows: Copy 'depends': ['library_member' , 'mail' ], And edit the  library_checkout/m...

Setting up an nginx reverse proxy

Setting up an nginx reverse proxy While Odoo itself can serve web pages, it's strongly recommended that there is a  reverse  proxy positioned in front of it. A reverse proxy acts as an intermediary that manages the traffic between clients sending requests and the Odoo servers responding to them. Using a reverse proxy has several benefits. On the security side, it can do the following: Handle (and enforce) HTTPS protocols to encrypt traffic Hide the internal network characteristics Act as an application firewall, limiting the URLs accepted for processing Also, on the performance side, it can provide the following significant improvements: Cached static content, hence reducing the load on the Odoo servers Compressed content to speed up loading time Act as a load balancer, distributing load between several servers Apache is a popular choice when considering a reverse proxy, although  nginx  is a recent alternative with good technical argumen...

The QWeb template language

The QWeb template language The  QWeb  parser looks for special directives in the templates and replaces them with dynamically generated HTML. These directives are XML element attributes and can be used in any valid tag or element, such as  <div> ,  <span> , or  <field> . Sometimes, we may want to use a QWeb directive but we don't want to place it in any of the XML elements in our template. For those cases, we have a  <t>  special element that can have QWeb directives, such as  t-if  or  t-foreach , but is silent and won't have any output on the final XML/HTML produced. The QWeb directives will frequently make use of evaluated expressions to produce different results, depending on the current record values. There are two different QWeb implementations: client-side JavaScript and server-side Python. The reports and website pages use the server-side Python implementation of QWeb. Kanban views us...