Khi làm việc với Odoo, việc lựa chọn giữa ORM và SQL thuần là một quyết định quan trọng ảnh hưởng trực tiếp đến hiệu năng và tính ổn định của ứng dụng. Trong bài viết này, chúng ta sẽ đi sâu vào tìm hiểu về ORM Odoo, khi nào nên sử dụng ORM, khi nào nên sử dụng SQL, và cách tối ưu hiệu năng khi làm việc với Odoo ORM. Những ví dụ thực tế sẽ giúp bạn có cái nhìn rõ hơn về cách lựa chọn công cụ đúng cho từng bài toán.
1) ORM Odoo là gì?

ORM (Object-Relational Mapping) trong Odoo là một lớp trung gian giúp bạn thao tác với dữ liệu trong cơ sở dữ liệu bằng cách sử dụng đối tượng (object) trong Python, thay vì phải viết các câu lệnh SQL trực tiếp. ORM giúp đơn giản hóa các thao tác CRUD (Create, Read, Update, Delete) và giúp code dễ hiểu, dễ duy trì và ít lỗi.
Một số thao tác cơ bản của ORM Odoo:
- Model: Đối tượng đại diện cho bảng dữ liệu trong Odoo. Ví dụ, self.env[‘res.partner’] là model cho bảng res_partner.
- Recordset: Là tập hợp các record trả về từ các thao tác như search, browse, v.v.
- Domain: Điều kiện lọc dữ liệu, tương tự như câu lệnh WHERE trong SQL.
- Các phương thức chính:
- create(): Tạo mới một bản ghi.
- write(): Cập nhật dữ liệu của bản ghi.
- unlink(): Xóa bản ghi.
- search(): Tìm kiếm dữ liệu.
- read_group(): Nhóm dữ liệu và thực hiện các phép toán tổng hợp.
Ví dụ về ORM trong Odoo:
partners = self.env[‘res.partner’].search([(‘customer_rank’, ‘>’, 0)], limit=10)
for p in partners:
_logger.info(“%s – %s”, p.name, p.email)
Trong ví dụ trên, ORM sẽ tự động chuyển câu lệnh Python thành SQL phù hợp, thực hiện truy vấn và trả về các đối tượng res.partner tương ứng. ORM cũng tự động xử lý việc phân quyền và các logic nghiệp vụ.
2) SQL thuần trong Odoo

Dù ORM trong Odoo mạnh mẽ và dễ sử dụng, đôi khi bạn cần sử dụng SQL thuần để tối ưu hóa hiệu suất, đặc biệt trong những tình huống cần truy vấn dữ liệu phức tạp hoặc xử lý một lượng dữ liệu lớn. Odoo cho phép bạn chạy SQL trực tiếp thông qua cursor.
Ví dụ về SQL thuần trong Odoo:
self.env.cr.execute(“””
SELECT id, name
FROM res_partner
WHERE customer_rank > 0
LIMIT 10
“””)
rows = self.env.cr.fetchall()
SQL có thể cực kỳ mạnh mẽ khi bạn cần làm việc với:
- Join nhiều bảng.
- Aggregate dữ liệu (group by, sum, count, etc).
- Window functions và CTE để giải quyết các bài toán phức tạp.
- Batch updates khi cần xử lý một lượng dữ liệu lớn.
Tuy nhiên, sử dụng SQL thuần trong Odoo cũng có những nhược điểm:
- Bypass record rules và ACL, nghĩa là bạn sẽ phải tự chịu trách nhiệm về phân quyền và các ràng buộc nghiệp vụ.
- Không tự xử lý các hook nghiệp vụ như sequence, tracking, hoặc computed fields.
- Dễ gặp lỗi SQL injection nếu không sử dụng parameter đúng cách.
3) So sánh ORM vs SQL: Khi nào sử dụng cái nào?

3.1 Ưu tiên sử dụng ORM khi:
- Bạn cần đảm bảo đúng nghiệp vụ (constraints, computed fields, triggers, etc).
- Cần bảo mật và phân quyền theo record rules và ACL.
- Dữ liệu không quá lớn hoặc có thể tối ưu hóa bằng các phương thức như read_group, search_read.
- Các thao tác CRUD thông thường (create, write, unlink).
- Các ứng dụng có tính bảo mật cao, yêu cầu phân quyền người dùng chặt chẽ.
3.2 Cân nhắc sử dụng SQL khi:
- Report nặng: Các báo cáo có yêu cầu join nhiều bảng, aggregate dữ liệu (sum, count, etc).
- Cần window functions hoặc CTE để xử lý các bài toán phức tạp.
- Batch update một lượng dữ liệu lớn (nhưng phải rất cẩn thận vì SQL có thể bypass các nghiệp vụ của Odoo).
- Các thao tác cần tối ưu hiệu suất tối đa (chạy báo cáo phức tạp với hàng triệu bản ghi).
Lưu ý: Đừng dùng SQL để thay thế cho các thao tác nghiệp vụ của Odoo. Việc INSERT vào sale_order hay account_move bằng SQL sẽ bỏ qua các logic nghiệp vụ như tạo sequence, cập nhật computed fields, và tracking.
4) Những lỗi performance hay gặp khi dùng ORM (và cách tối ưu)
Lỗi #1: N+1 query (search trong vòng lặp)
Khi sử dụng search() trong vòng lặp, bạn sẽ gửi nhiều query tới cơ sở dữ liệu, điều này cực kỳ tốn kém khi dữ liệu của bạn lớn.
Ví dụ xấu:
partners = self.env[‘res.partner’].search([(‘customer_rank’, ‘>’, 0)])
for p in partners:
cnt = self.env[‘sale.order’].search_count([(‘partner_id’, ‘=’, p.id)])
p.x_order_count = cnt
Khi có hàng nghìn partner, bạn sẽ gửi hàng nghìn query để đếm số đơn của mỗi khách hàng.
Cách tối ưu:
data = self.env[‘sale.order’].read_group(
domain=[(‘partner_id’, ‘in’, partners.ids)],
fields=[‘partner_id’],
groupby=[‘partner_id’]
)
count_map = {d[‘partner_id’][0]: d[‘partner_id_count’] for d in data}
for p in partners:
p.x_order_count = count_map.get(p.id, 0)
Bằng cách sử dụng read_group, bạn chỉ cần một query duy nhất để lấy tổng số đơn hàng của tất cả khách hàng.
Lỗi #2: Dùng filtered() / sorted() trên recordset quá lớn
filtered() và sorted() là các phương thức của recordset, nhưng chúng sẽ hoạt động trên dữ liệu đã được tải vào bộ nhớ, điều này có thể gây chậm nếu recordset của bạn quá lớn.
Ví dụ xấu:
orders = self.env[‘sale.order’].search([])
big_orders = orders.filtered(lambda o: o.amount_total > 10000000)
Cách tối ưu:
big_orders = self.env[‘sale.order’].search([(‘amount_total’, ‘>’, 10000000)])
Hãy luôn đẩy các điều kiện lọc vào câu lệnh SQL thay vì xử lý trong Python để giảm tải bộ nhớ và tăng tốc độ.
Lỗi #3: Đọc field quan hệ trong vòng lặp (N+1 tiềm ẩn)
Nếu bạn đọc các trường liên kết trong vòng lặp mà không prefetch chúng, bạn có thể sẽ tạo ra rất nhiều query không cần thiết.
Ví dụ xấu:
orders = self.env[‘sale.order’].search([(‘state’, ‘=’, ‘sale’)])
for o in orders:
_logger.info(o.partner_id.country_id.name)
Cách tối ưu:
orders = self.env[‘sale.order’].search_read(
[(‘state’, ‘=’, ‘sale’)],
[‘name’, ‘partner_id’, ‘amount_total’],
limit=2000
)
Sử dụng search_read thay vì search để lấy tất cả các field bạn cần trong một lần truy vấn.
Lỗi #4: write() trong vòng lặp
Khi gọi write() trong vòng lặp, bạn đang thực hiện nhiều lần ghi vào cơ sở dữ liệu, điều này rất tốn kém về mặt hiệu suất.
Ví dụ xấu:
partners = self.env[‘res.partner’].search([(‘active’, ‘=’, True)])
for p in partners:
p.write({‘x_flag’: True})
Cách tối ưu:
partners.write({‘x_flag’: True})
Thay vì gọi write() từng bản ghi một, bạn có thể gọi một lần duy nhất với tất cả các bản ghi cần cập nhật.
5) Case study thực tế: Đếm đơn hàng trong 90 ngày gần nhất
Bài toán:
Hiển thị khách hàng và số đơn đã xác nhận trong 90 ngày gần nhất.
Cách làm sai (N+1):
from datetime import date, timedelta
cutoff = date.today() – timedelta(days=90)
partners = self.env[‘res.partner’].search([(‘customer_rank’, ‘>’, 0)])
for p in partners:
cnt = self.env[‘sale.order’].search_count([
(‘partner_id’, ‘=’, p.id),
(‘state’, ‘in’, [‘sale’, ‘done’]),
(‘date_order’, ‘>=’, cutoff),
])
p.x_order_90d = cnt
Với cách làm này, mỗi lần lặp qua partner sẽ gửi một query riêng để đếm số đơn hàng. Điều này cực kỳ tốn kém khi số lượng partner lớn.
Cách tối ưu bằng read_group:
from datetime import date, timedelta
cutoff = date.today() – timedelta(days=90)
data = self.env[‘sale.order’].read_group(
domain=[(‘state’, ‘in’, [‘sale’, ‘done’]), (‘date_order’, ‘>=’, cutoff)],
fields=[‘partner_id’],
groupby=[‘partner_id’]
)
count_map = {d[‘partner_id’][0]: d[‘partner_id_count’] for d in data}
partners = self.env[‘res.partner’].search([(‘customer_rank’, ‘>’, 0)])
for p in partners:
p.x_order_90d = count_map.get(p.id, 0)
Khi dữ liệu quá lớn, sử dụng SQL để aggregate:
from datetime import date, timedelta
cutoff = date.today() – timedelta(days=90)
self.env.cr.execute(“””
SELECT partner_id, COUNT(*)
FROM sale_order
WHERE state IN (‘sale’,’done’)
AND date_order >= %s
AND partner_id IS NOT NULL
GROUP BY partner_id
“””, (cutoff,))
count_map = dict(self.env.cr.fetchall())
partners = self.env[‘res.partner’].search([(‘customer_rank’, ‘>’, 0)])
for p in partners:
p.x_order_90d = count_map.get(p.id, 0)
Trong trường hợp này, việc sử dụng SQL giúp tối ưu hóa báo cáo khi dữ liệu lớn và cần tính toán nhanh.
6) Kết luận
ORM trong Odoo là công cụ mạnh mẽ giúp bạn thao tác với cơ sở dữ liệu một cách dễ dàng và an toàn. Tuy nhiên, như với bất kỳ công cụ nào, khi sử dụng ORM, bạn cần phải chú ý đến hiệu suất, đặc biệt khi làm việc với lượng dữ liệu lớn. N+1 query, lặp lại query trong vòng lặp, và dùng write() trong loop là những lỗi phổ biến khiến ứng dụng chậm lại. Khi gặp phải các tình huống phức tạp hơn như report lớn, join nhiều bảng hoặc batch update dữ liệu, bạn cần cân nhắc sử dụng SQL thuần để tối ưu hiệu suất.
Tuy nhiên, SQL không phải là “bùa phép” mà là một công cụ mạnh, nên chỉ sử dụng khi cần thiết và phải rất cẩn trọng với bảo mật, phân quyền, và logic nghiệp vụ.
Chọn ORM khi bạn muốn làm việc với Odoo theo cách “an toàn”, dễ bảo trì và bảo mật. SQL là lựa chọn đúng khi bạn cần tối ưu hóa hiệu suất với dữ liệu lớn hoặc các bài toán phức tạp. Khi viết code, hãy luôn đặt hiệu suất và tính an toàn lên hàng đầu, và đừng quên đo đạc và tối ưu thường xuyên.
Vui lòng đăng nhập để bình luận.