Relations between models in Odoo: Many2one, One2many and Many2many without the confusion
The three relations that connect your data, what each one really stores in the database, and the commands to write them without breaking anything.
Part of our complete guide: Odoo module development.
A model on its own in Odoo isn't much: a table with its fields and little else. The power shows up when you connect them. A property belongs to a salesperson, has several offers and is tagged with several categories. Those three sentences are, exactly, Odoo's three relations: Many2one, One2many and Many2many. They sound like jargon, but the moment you understand what each one actually stores in the database, they stop being scary and you start modelling properly.
The misconception almost everyone carries is believing the three "store data" the same way. They don't. And that difference explains 90% of the performance problems and the "why won't this delete?" head-scratchers.
Many2one: the good old foreign key
It's the king of relations, the one you'll use most. "This property has one salesperson." Under the hood it's a column in your table holding the related record's id. Nothing exotic: a foreign key.
class Property(models.Model):
_name = "estate.property"
salesperson_id = fields.Many2one(
"res.users", string="Salesperson",
default=lambda self: self.env.user,
)
buyer_id = fields.Many2one("res.partner", string="Buyer")
What many people overlook is ondelete. By default, if you delete the salesperson, the field is set to empty (set null). But sometimes you want restrict (don't let me delete a salesperson who has properties) or cascade (if the parent goes, the children go too). Deciding this on purpose avoids orphan records and production scares.
One2many: a mirror, not a column
Here's the most important conceptual trap in all of Odoo. A One2many stores nothing in your table. It's the flip side of a Many2one that lives on the other model. It's, literally, a computed view: "give me every record over there that points to me".
# In estate.property
offer_ids = fields.One2many("estate.property.offer", "property_id", string="Offers")
# In estate.property.offer (this is where the real column lives!)
property_id = fields.Many2one("estate.property", required=True, ondelete="cascade")
That's why a One2many always needs its inverse Many2one: without that column on the other side, there's nothing to look at. The classic beginner mistake is defining the One2many and expecting it to work on its own. It won't, and the error message isn't always obvious.
Many2many: the join table Odoo manages for you
"A property has several tags, and a tag is on several properties." That's a Many2many, and under the hood Odoo creates an intermediate (relation) table without you seeing it.
tag_ids = fields.Many2many("estate.property.tag", string="Tags")
Convenient, but with fine print: queries over a Many2many with many records can get heavy, and sometimes what you really need is an explicit intermediate model (a One2many to a bridge table) because you want to store data about the relation —a date, a state, an amount—. When you notice your Many2many "wishes it had fields", that's the sign you need an intermediate model.
How to write relations (this is gold)
This is where the docs lose a lot of people. You don't write into a One2many or Many2many like into a normal field; you use commands. In modern Odoo there are readable helpers:
from odoo.fields import Command
property.write({
"offer_ids": [
Command.create({"price": 250000, "partner_id": partner.id}), # create and link
Command.unlink(old_offer.id), # delete the record
],
"tag_ids": [
Command.set([tag_a.id, tag_b.id]), # replace the whole set
Command.link(tag_c.id), # link an existing one
],
})
You'll also see them as cryptic tuples —(0, 0, {...}), (4, id), (6, 0, [ids])— in older code; they're the same thing. The day you understand that Command.set replaces and Command.link adds, you stop accidentally duplicating tags and deleting relations you thought you were keeping.
Welcome shortcuts: related fields
Once you have relations, you can "pull" fields from one model into another without recomputing anything, with a related field. Handy to show the parent's data on the child:
buyer_phone = fields.Char(related="buyer_id.phone", string="Buyer phone")
It's convenient, but remember what we covered about computed fields and store: if you're going to filter or group by that related field, you'll need to consider storing it. Cheap gets expensive when the list has 50,000 rows.
What separates a healthy data model from a sick one
In audits, the pattern repeats: relations that should be Many2one solved with a Char ("type the customer's name here") that destroys integrity; Many2many where an intermediate model was needed; One2many without ondelete leaving junk everywhere. None of those mistakes show up in the demo. All of them show up two years later, when the data is already dirty and migrating hurts.
Modelling relations well is, without exaggeration, the technical decision that most shapes your Odoo's life. If you're starting a custom build, this is designed before writing the first view. We cover it in the anatomy of a module and, when it's time to evolve it without breaking it, in inheritance in Odoo.
Is your data model already giving you grief, or are you about to start a new one and want to get it right the first time? Tell us in a diagnosis session or see how we approach custom development.
Comments (0)
Be the first to comment.
Sign in to leave a comment.
Sign inComments are reviewed before publishing.
Related articles
Automated actions in Odoo: let the ERP do the boring work
Server actions, automation rules and scheduled jobs: the three pieces that make Odoo react, notify and run on its own —without anyone having to remember.
PDF reports with QWeb in Odoo: from the form to the document that closes deals
QWeb is HTML that turns into PDF. We show you how to build a report, loop over records with t-foreach, and why t-field saves you a thousand headaches.
Inheritance in Odoo: the feature that decides whether your code survives upgrades
Classical extension, delegation and view inheritance. The three ways to modify Odoo without touching the core —and why inheriting instead of copying saves you every migration.