ORM Functions in Odoo 17

October 24, 2025 by
ORM Functions in Odoo 17
Alfin Isnain Hariawan

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.

ORM Functions in Odoo 17
Alfin Isnain Hariawan October 24, 2025
Share this post
Archive