Odoo 17 ORM (Object Relational Mapping) is the backbone of the Odoo framework. It allows developers to manage database records in a simple, Pythonic way — no need for raw SQL queries.
Every Odoo model you define is automatically linked to a database table, and each record corresponds to a row in that table.
In this detailed guide, we’ll explore all the core ORM functions in Odoo 17, including create(), write(), unlink(), copy(), search(), browse(), read(), and more — with in-depth explanations, examples, and real-world use cases.
What Is ORM in Odoo?
ORM (Object Relational Mapping) is a layer that maps Python objects to database records.
Each Odoo model class (like res.partner, sale.order, etc.) represents a database table.
This means you can use Python objects (recordsets) to create, read, update, and delete data, instead of SQL queries.
Example:
self.env['res.partner'].create({'name': 'John Doe'})
This automatically executes an SQL statement like:
INSERT INTO res_partner (name) VALUES ('John Doe');
⚙️ ORM Basics in Odoo 17
All ORM operations are performed through recordsets.
A recordset is:
- Lazy-evaluated (no SQL executed until necessary),
- Ordered (preserves order of records),
- Iterable (you can loop through records like lists).
You get a recordset using:
self.env['model.name']
Example:
Product = self.env['product.template']
products = Product.search([('list_price', '>', 1000)])
🧩 1. create() — Adding New Records
The create() method inserts new records into the database.
✅ Syntax:
record = self.env['model.name'].create(vals)
- vals: dictionary of field names and their values.
- Returns a recordset of the newly created record(s).
✅ Single Record Example
new_partner = self.env['res.partner'].create({
'name': 'John Doe',
'email': 'john.doe@example.com',
})
✅ Multi-Record Example (Odoo 17 feature)
In Odoo 17, use @api.model_create_multi to handle multiple record creation at once efficiently:
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
if not vals.get('email'):
vals['email'] = f"{vals['name'].replace(' ', '').lower()}@example.com"
records = super().create(vals_list)
return records
Usage:
self.env['res.partner'].create([
{'name': 'Alice'},
{'name': 'Bob', 'email': 'bob@example.com'}
])
💡 Tip: create() automatically triggers any computed fields, onchange, and constraints after record insertion.
✏️ 2. write() — Updating Records
write() modifies existing records in place.
✅ Syntax:
recordset.write(vals)
- vals: dictionary of field names and updated values.
- Works on multiple records at once.
- Returns True if successful.
✅ Example
partners = self.env['res.partner'].search([('country_id.code', '=', 'ID')])
partners.write({'phone': '+62 812-3456-7890'})
✅ Override Example
You can override write() to inject logic:
def write(self, vals):
res = super().write(vals)
for rec in self:
_logger.info(f"Record {rec.display_name} updated.")
return res
💡 Tip: When updating related (relational) fields such as Many2one, Many2many, or One2many, Odoo uses Command sets in Odoo 17.
Example:
record.write({
'tag_ids': [Command.set([1, 2, 3])]
})
🗑️ 3. unlink() — Deleting Records
Deletes record(s) from the database.
✅ Syntax:
recordset.unlink()
✅ Example:
inactive_partners = self.env['res.partner'].search([('active', '=', False)])
inactive_partners.unlink()
✅ Override Example:
def unlink(self):
for record in self:
if record.state == 'done':
raise UserError("Cannot delete completed records.")
return super().unlink()
💡 Tip: If you delete a record with One2many or Many2many relations, related data is handled depending on the ondelete policy defined in your field.
🧬 4. copy() — Duplicating Records
Duplicates an existing record and returns the new one.
✅ Example:
partner = self.env['res.partner'].browse(5)
new_partner = partner.copy({'name': f'{partner.name} (Copy)'})
✅ Override Example:
def copy(self, default=None):
default = dict(default or {})
default['name'] = f"{self.name} (Duplicate)"
return super().copy(default)
🔍 5. search() — Finding Records
Searches for records matching a domain filter.
✅ Syntax:
recordset = self.env['model.name'].search(domain, limit=0, offset=0, order=None)
✅ Example:
partners = self.env['res.partner'].search([
('is_company', '=', True),
('country_id.code', '=', 'ID')
], order='create_date desc', limit=10)
💡 search() returns a recordset, not a list or dictionary.
🔢 6. browse() — Get Records by ID
Retrieves records by ID (or a list of IDs).
✅ Example:
partner = self.env['res.partner'].browse(10)
if partner.exists():
print(partner.name)
💡 .exists() in Odoo 17 filters out deleted records safely.
📖 7. read() — Reading Field Values
read() retrieves data from records as a dictionary list.
✅ Example:
partners = self.env['res.partner'].search([], limit=3)
data = partners.read(['name', 'email'])
print(data)
Output:
[{'id': 1, 'name': 'John Doe', 'email': 'john@example.com'}, ...]
🔁 8. search_read() — Search and Read Combined
This is a shortcut to combine search() and read() in one call (faster).
✅ Example:
records = self.env['res.partner'].search_read(
domain=[('is_company', '=', True)],
fields=['name', 'email'],
limit=5
)
🔍 10. name_search() — Custom Search Behavior
Defines how records are searched in dropdowns.
✅ Example:
@api.model
def name_search(self, name='', args=None, operator='ilike', limit=100):
args = args or []
domain = ['|', ('name', operator, name), ('email', operator, name)]
return self.search(domain + args, limit=limit).name_get()
⚙️ 11. Recordset Helper Methods
Method | Description | Example |
mapped() | Extract field values or apply method | emails = partners.mapped('email') |
filtered() | Filter recordsets using a lambda | active = partners.filtered(lambda p: p.active) |
ensure_one() | Raises error if recordset has ≠1 record | self.ensure_one() |
exists() | Remove deleted records from recordset | valid = self.exists() |
sudo() | Bypass access rules | self.sudo().write({'active': True}) |
🧮 12. Domain Filters and Operators
Common operators used in Odoo ORM:
Operator | Meaning | Example |
= | Equal | ('state', '=', 'draft') |
!= | Not equal | ('active', '!=', True) |
> < >= <= | Comparison | ('price', '>', 100) |
in | Value in list | ('country_id', 'in', [1, 2, 3]) |
| ilike | Case-insensitive match | ('name', 'ilike', 'john') |
| child_of | Hierarchical search | ('category_id', 'child_of', 5) |
💡 Domains can also be combined using logical operators:
['&', ('field', '=', value), ('other_field', '=', value)]
or simplified with tuples.
📋 13. Practical Example: Full CRUD Flow
class PartnerDemo(models.Model):
_name = 'demo.partner'
_description = 'Demo Partner Model'
name = fields.Char(required=True)
email = fields.Char()
active = fields.Boolean(default=True)
@api.model_create_multi
def create(self, vals_list):
records = super().create(vals_list)
for rec in records:
_logger.info(f"Created partner: {rec.name}")
return records
def write(self, vals):
_logger.info(f"Updating {len(self)} records")
return super().write(vals)
def unlink(self):
if any(not rec.active for rec in self):
raise UserError("Inactive records cannot be deleted.")
return super().unlink()
Conclusion
Odoo 17’s ORM layer is one of the most powerful features of the framework — it abstracts away SQL complexity and lets you manage data intuitively.
By mastering ORM methods such as:
- create() for adding data,
- write() for updates,
- unlink() for deletions,
- search() for filtering, and
- helpers like mapped() and filtered(),
you can build powerful, clean, and maintainable Odoo modules that fully leverage the framework’s power.
