---
url: https://docs.cybrosys.com/cheatsheets/orm-methods.md
description: >-
  Odoo ORM cheatsheet: search, create, write and unlink, domains, display names
  and recordset methods, with version notes.
---

# ORM Methods

Examples target Odoo 18 and later unless marked. Tabs show the Odoo 16 and 17 versions where the API differs.

Use this page as a quick reference for the Odoo ORM methods you call most: creating, searching, updating and deleting records, building domains, working with recordsets and the environment, and customising display names and name search.

## Quick CRUD

The four calls below cover the basic life cycle of a record: `create()` adds one, `search()` finds records, `write()` changes them and `unlink()` deletes them. The sections under [CRUD in detail](#crud-in-detail) cover single and multiple records for each call.

```python
# Create a record
self.env['model.name'].create({'field': value})
```

```python
# Read records
records = self.env['model.name'].search([('field', '=', value)])
```

```python
# Update records
record.write({'field': new_value})
```

```python
# Delete records
record.unlink()
```

## Advanced usage

These patterns build on the basic methods for cases where a single condition or call is not enough.

### Combine domains with OR

A domain is a list of conditions joined by AND by default. Put the `'|'` operator before two conditions to join them with OR, as in the example below, where `field1` must match and either `field2` or `field3` must match.

```python
records = self.env['model.name'].search([
    ('field1', '=', value1),
    '|', ('field2', '!=', value2), ('field3', 'in', [1, 2, 3]),
])
```

## CRUD in detail

These sections repeat the four CRUD calls from [Quick CRUD](#quick-crud) and add the single-record and multi-record forms.

### Create records

Call `create()` with a dictionary to add one record, or with a list of dictionaries to add several records in one call, which is faster than creating them in a loop.

```python
# Create a single record
self.env['model.name'].create({
    'field1': value1,
    'field2': value2,
})

# Create multiple records
self.env['model.name'].create([
    {'field1': value1, 'field2': value2},
    {'field1': value3, 'field2': value4},
])
```

### Read records

Use `search()` to find records by domain, `browse()` to load records from known IDs, `read()` to get field values as dictionaries, and attribute access to read a single field value.

```python
# Search records
records = self.env['model.name'].search([('field', '=', value)])

# Browse by ID
record = self.env['model.name'].browse(id)

# Read specific fields
values = record.read(['field1', 'field2'])

# Get field value
value = record.field_name
```

### Update records

Call `write()` on a record or a recordset with a dictionary of new values. On a recordset, one call updates every record in it.

```python
# Update single record
record.write({
    'field1': new_value1,
    'field2': new_value2,
})

# Update multiple records
records.write({
    'field1': new_value1,
    'field2': new_value2,
})
```

### Delete records

Call `unlink()` on a record or a recordset to delete it permanently. Deleting cannot be undone, so check the recordset first.

```python
# Delete single record
record.unlink()

# Delete multiple records
records.unlink()
```

## Search methods

Use `search()` with a domain, a list of conditions, to find records. The sections below cover the domain syntax and the operators you can use.

### Search domain

A search domain is a list of `(field, operator, value)` tuples passed to `search()`. Conditions are joined with AND unless you add `'|'` (OR), `'&'` (AND) or `'!'` (NOT) in front of them, which Odoo reads in prefix notation.

```python
# Basic search
records = self.env['model.name'].search([
    ('field1', '=', value1),
    ('field2', '!=', value2),
])

# Complex conditions
records = self.env['model.name'].search([
    '|',  # OR
    '&',  # AND
    ('field1', '=', value1),
    ('field2', '!=', value2),
    ('field3', 'in', [value3, value4]),
])
```

### Search operators

Use these operators in the second position of a domain tuple. Use `ilike` for case-insensitive text matching and `in` to match any value in a list.

```python
# Common operators
'='     # equals
'!='    # not equals
'>'     # greater than
'>='    # greater than or equal
'<'     # less than
'<='    # less than or equal
'like'  # pattern matching
'ilike' # case-insensitive pattern matching
'in'    # in list
'not in'  # not in list
```

## Recordset operations

A recordset is a collection of records. You can filter, map and sort it in Python without extra database queries.

### Filtering

`filtered()` keeps the records for which a Python function returns true, `filtered_domain()` keeps the records that match a domain without querying the database again, and `mapped()` returns the values of a field (or a recordset for a relational field) across all records. Use these on a recordset you already loaded instead of running a new `search()`.

```python
# Filter records
filtered_records = records.filtered(lambda r: r.field > value)

# Filter by domain
filtered_records = records.filtered_domain([('field', '=', value)])

# Map to field values
values = records.mapped('field_name')
```

### Sorting

`sorted()` returns a new recordset ordered by a field name or a key function. Pass `reverse=True` for descending order.

```python
# Sort by field
sorted_records = records.sorted('field_name')
sorted_records = records.sorted(key=lambda r: r.field_name)

# Reverse sort
reversed_records = records.sorted('field_name', reverse=True)
```

## Environment methods

The environment (`self.env`) holds the current user, company and context, and lets you run code as a different user or company.

### Context

The context is a dictionary of flags that travels with the environment. Read it with `self.env.context`, use `with_context()` to get a copy of the recordset with extra keys, and use `with_company()` to run code as a different company.

```python
# Get context
context = self.env.context

# Update context
self.with_context(key=value)
self.with_context(dict(key=value))

# Switch company
self.with_company(company_id)
```

### User

`self.env.user` is the user running the code. Use it to read the user's companies and to test group membership with `has_group()` before you allow an action.

```python
# Get current user
user = self.env.user

# Get user companies
companies = self.env.user.company_ids

# Check access rights
self.env.user.has_group('group_name')
```

## Common patterns

These are the ORM customisations you meet most often when extending a model.

### Display name

To change how a record is shown in many2one fields and breadcrumbs, set the computed `display_name` field. Odoo 17 deprecates `name_get()` in favour of `display_name`, and Odoo 18 removes `name_get()`, so override `_compute_display_name` in Odoo 17 and later and `name_get()` in Odoo 16 and earlier.

**Odoo 17 and later**

```python
@api.depends('code', 'name')
def _compute_display_name(self):
    for record in self:
        record.display_name = f"{record.code} - {record.name}"
```

**Odoo 16 and earlier (legacy)**

```python
def name_get(self):
    result = []
    for record in self:
        name = f"{record.code} - {record.name}"
        result.append((record.id, name))
    return result
```

### Name search

Name search runs when a user types in a many2one field. To search more than the name field, such as a code, override the hook for your version: `name_search` (or `_name_search`) in Odoo 16, `_name_search` in Odoo 17 and `_search_display_name` in Odoo 18 and later. Starting in Odoo 18, `_search_display_name` returns a domain and `name_search` is no longer the hook to override.
**Odoo 18 and later**

```python
@api.model
def _search_display_name(self, operator, value):
    if operator in ('ilike', 'like', '=', '=like', '=ilike') and value:
        return ['|', ('name', operator, value), ('code', operator, value)]
    return super()._search_display_name(operator, value)
```

**Odoo 17**

```python
@api.model
def _name_search(self, name, domain=None, operator='ilike', limit=None, order=None):
    domain = list(domain or [])
    if name:
        domain = ['|', ('name', operator, name), ('code', operator, name)] + domain
    return self._search(domain, limit=limit, order=order)
```

**Odoo 16 and earlier (legacy)**

```python
@api.model
def name_search(self, name='', args=None, operator='ilike', limit=100):
    args = args or []
    domain = []
    if name:
        domain = ['|', ('name', operator, name), ('code', operator, name)]
    return self.search(domain + args, limit=limit).name_get()
```

### Default get

`default_get()` returns the initial values of a new record's fields. Override it to set defaults that depend on the context or on other data, and call `super()` first to keep the standard defaults.

```python
def default_get(self, fields_list):
    defaults = super().default_get(fields_list)
    defaults['field'] = self.env.context.get('default_field', False)
    return defaults
```

## Related

* [Computed fields](/cheatsheets/computed-fields) for `@api.depends` and compute methods.
* [API decorators](/cheatsheets/api) for `@api.model`, `@api.depends` and the other decorators used above.
* [Inheritance in Odoo](/cheatsheets/inheritance) for extending models with `_inherit`.
* [Security rules](/cheatsheets/security-rules) for access rights and record rules.
* [Smart suggestions in VS Code](/vs-code-extension/features/smart-suggestions) and [in PyCharm](/pycharm-plugin/features/smart-suggestions) to complete ORM calls and field names in the editor.
* [Python snippets in Assista IDE](/assista-ide/snippets/python-snippets) to insert common ORM code.
