Tutorial & Best Practices
ORM SQL Queries Odoo empower developers to balance the ease of object-relational mapping with the power of raw SQL. In this tutorial, you will learn how to use ORM SQL Queries Odoo effectively, and you will see clear code examples, performance tips, and advanced patterns. Moreover, you will find outgoing links to official documentation for deeper dives.
What Are ORM SQL Queries in Odoo?
First, It combine two database interaction methods: the Odoo ORM and raw SQL queries. Therefore, you can choose the right tool for each task. Furthermore, you maintain code consistency and performance by balancing these approaches.
Odoo ORM vs Raw SQL Queries
- Odoo ORM uses Python methods like
search()
,create()
, andwrite()
. It prevents SQL injection and caches results. - Raw SQL Queries run directly via
env.cr.execute()
. They suit complex reports and bulk operations where speed matters.
When to Use ORM SQL Queries in Odoo
- Use ORM for most CRUD work and business logic. It ensures security and readability.
- Use Raw SQL for heavy data analysis, complex joins, or when you need database views. It delivers maximum speed.
Setting Up Your Odoo Development Environment
To apply effectively, first set up your environment properly.
Installing Odoo 18
- Clone the Odoo repository
git clone https://github.com/odoo/odoo.git --depth 1 --branch 18.0
cd odoo
- Install dependencies
python3 -m pip install -r requirements.txt
- Configure PostgreSQL
sudo -u postgres createuser --createdb --login odoo18
sudo -u postgres createdb --owner=odoo18 odoo18db
- Run Odoo
./odoo-bin -c ./debian/odoo.conf
Next, enable developer mode from the Settings app to access advanced features.
Creating Your First Custom Module
Then, scaffold a simple module named example_orm_sql
.
cd addons
mkdir example_orm_sql
cd example_orm_sql
mkdir models views
touch __init__.py __manifest__.py models/__init__.py views/__init__.py
Edit __manifest__.py
:
{
'name': 'Example ORM SQL Tutorial',
'version': '1.0',
'category': 'Tools',
'depends': ['base', 'sale', 'point_of_sale'],
'data': ['views/example_views.xml'],
'installable': True,
}
Working with Odoo ORM SQL Queries in Odoo
After setup, start using it in your models and controllers.
Basic ORM Operations
First, open models/example_models.py
:
from odoo import models, fields
class ExampleOrder(models.Model):
_name = 'example.order'
_description = 'Example Order'
name = fields.Char('Reference', required=True)
partner_id = fields.Many2one('res.partner', string='Customer')
order_date = fields.Date('Order Date')
amount_total = fields.Monetary('Total Amount', currency_field='currency_id')
currency_id = fields.Many2one('res.currency', default=lambda self: self.env.company.currency_id.id)
Then, perform CRUD with ORM:
# Create a record
order = env['example.order'].create({
'name': 'EX-2025-001',
'partner_id': partner.id,
'order_date': fields.Date.today(),
'amount_total': 1200.00,
})
# Read records
orders = env['example.order'].search([('amount_total', '>', 1000)])
for o in orders:
print(o.name, o.amount_total)
# Update a record
order.write({'amount_total': 1300.00})
# Delete a record
order.unlink()
Filtering with Domains
Second, apply domains for complex filters:
# Find orders this month with total > 500
from datetime import date
start = date.today().replace(day=1)
orders_month = env['example.order'].search([
('order_date', '>=', start),
('amount_total', '>', 500),
])
Moreover, you can combine OR & AND:
orders_special = env['example.order'].search([
'|',
('amount_total', '>', 1000),
('partner_id.country_id.code', '=', 'US'),
])
Writing Raw SQL Queries in Odoo
Sometimes, you need raw power. Thus, turn to raw SQL for performance.
Using env.cr
to Execute SQL
First, get the cursor and run SQL:
class ExampleReport(models.Model):
_name = 'example.report'
_auto = False
def _select(self):
return """
SELECT
row_number() OVER () AS id,
so.id AS sale_order_id,
so.date_order AS date,
rp.name AS customer,
SUM(sol.price_total) AS total
"""
def _from(self):
return """
FROM sale_order_line sol
JOIN sale_order so ON so.id = sol.order_id
JOIN res_partner rp ON rp.id = so.partner_id
"""
def init(self):
tools.drop_view_if_exists(self.env.cr, self._table)
self.env.cr.execute(f"""
CREATE OR REPLACE VIEW {self._table} AS (
{self._select()}
{self._from()}
GROUP BY so.id, so.date_order, rp.name
)
""")
Then, reading raw SQL results:
self.env.cr.execute("SELECT country_id, COUNT(*) FROM res_partner GROUP BY country_id")
for country_id, count in self.env.cr.fetchall():
print(country_id, count)
Creating Database Views
Second, map a view to an Odoo model by setting _auto=False
:
class PartnerStats(models.Model):
_name = 'partner.stats'
_description = 'Partner Statistics'
_auto = False
country_id = fields.Many2one('res.country', string='Country')
total_partners = fields.Integer('Total Partners')
def init(self):
tools.drop_view_if_exists(self.env.cr, self._table)
self.env.cr.execute(f"""
CREATE VIEW {self._table} AS
SELECT
row_number() OVER () AS id,
p.country_id,
COUNT(*) AS total_partners
FROM res_partner p
GROUP BY p.country_id
""")
Combining ORM and Raw SQL in Odoo
Balance the strengths of each method by combining them.
Unified Sales & PoS View Example
Next, you build a unified view of Sale Orders and PoS Orders. Place this in models/pos_sale_order.py
:
from odoo import models, fields, tools
class PosSaleOrder(models.Model):
_name = 'pos.sale.order'
_auto = False
_description = 'Pos & Sale Order'
product_id = fields.Many2one('product.product', string='Product')
partner_id = fields.Many2one('res.partner', string='Customer')
quantity = fields.Float( string='Quantity')
price_unit = fields.Float( string='Price Unit')
date = fields.Date( string='Date')
type = fields.Selection([('sale','Sale'),('pos','Point of Sale')], string='Type')
def init(self):
tools.drop_view_if_exists(self.env.cr, self._table)
self.env.cr.execute(f"""
CREATE VIEW {self._table} AS
SELECT
row_number() OVER () AS id,
sol.product_id,
so.partner_id,
sol.product_uom_qty AS quantity,
sol.price_unit,
so.date_order AS date,
'sale' AS type
FROM sale_order_line sol
JOIN sale_order so ON so.id = sol.order_id
WHERE so.state IN ('sale','done')
UNION ALL
SELECT
pol.product_id,
po.partner_id,
pol.qty AS quantity,
pol.price_unit,
po.date_order AS date,
'pos' AS type
FROM pos_order_line pol
JOIN pos_order po ON po.id = pol.order_id
""")
Then define the view XML in views/pos_sale_order_views.xml
:
<odoo>
<data>
<record id="view_pos_sale_order_tree" model="ir.ui.view">
<field name="name">pos.sale.order.tree</field>
<field name="model">pos.sale.order</field>
<field name="arch" type="xml">
<tree string="Combined Sales & PoS">
<field name="type"/>
<field name="date"/>
<field name="product_id"/>
<field name="partner_id"/>
<field name="quantity"/>
<field name="price_unit"/>
</tree>
</field>
</record>
<record id="action_pos_sale_order" model="ir.actions.act_window">
<field name="name">Combined Sales & PoS</field>
<field name="res_model">pos.sale.order</field>
<field name="view_mode">tree</field>
</record>
<menuitem id="menu_pos_sale_order"
name="Sales & PoS"
parent="sale.sale_menu_root"
action="action_pos_sale_order"/>
</data>
</odoo>
As a result, users can view both transaction types in one place.
Best Practices for ORM SQL Queries in Odoo
Ensuring Security
First, avoid SQL injection by never concatenating untrusted input into SQL strings. Instead, use parameterized queries:
self.env.cr.execute(
"SELECT * FROM sale_order WHERE partner_id = %s", (partner_id,)
)
Next, always validate user input and escape parameters properly.
Optimizing Performance
- Use Domains instead of
search()
for simple filters. - Batch Writes: call
write()
on recordsets rather than in loops. - Leverage SQL Views for reporting to offload heavy aggregation to the database.
Moreover, add database indexes via _sql_constraints
or raw SQL migrations:
_sql_constraints = [
('sale_order_date_idx', 'INDEX(date_order)', 'Date index for orders')
]
Maintaining Code Quality
- Keep model definitions clear by grouping related fields.
- Document your raw SQL with comments.
- Write tests for both ORM methods and raw SQL functions.
- Use consistent naming conventions in
_name
and table names.
Advanced Topics
Indexing and EXPLAIN Plans
First, analyze slow queries with PostgreSQL’s EXPLAIN ANALYZE
. Then, add indexes:
CREATE INDEX idx_sale_order_partner ON sale_order(partner_id);
Next, revisit ORM search patterns to leverage indexed fields.
Batch Operations and Caching
Moreover, use Odoo’s read_group()
for group-by operations via ORM:
data = env['sale.order'].read_group(
domain=[('date_order', '>=', start_date)],
fields=['partner_id', 'amount_total:sum'],
groupby=['partner_id']
)
Additionally, implement caching for repeated queries:
from odoo.tools import cache
@cache('my_key_prefix', keys=['user.id'])
def expensive_report(self):
# heavy computation
return result
Resources & Further Reading
- Official Odoo ORM Reference:
https://www.odoo.com/documentation/18.0/developer/reference/addons/orm.html - PostgreSQL Documentation on Indexes:
https://www.postgresql.org/docs/current/indexes.html - Odoo SQL Practices:
https://www.odoo.com/documentation/18.0/developer/reference/addons/orm.html#sql-views
Conclusion
In this tutorial, you mastered this by exploring both ORM methods and raw SQL. Moreover, you learned when to apply each technique, saw clear code samples, and discovered best practices for security, performance, and maintainability. Finally, you built a unified sales and PoS view. Now, you can leverage techinque with confidence in your next project!