Bạn viết một module và cần tham chiếu tới nhóm quyền "Nhân viên nội bộ". Cách tệ nhất là gõ id số: env['res.groups'].browse(1). Vì id số không ổn định — cài trên máy khác, phục hồi từ backup khác, cùng một nhóm có thể mang id khác. Cách đúng của Odoo là gọi nó bằng tên bất biến: env.ref('base.group_user'). Bài này mổ xẻ env.ref và cái tên đó — external id — thật ra là gì.

External id là gì

Mỗi bản ghi được tạo từ file dữ liệu XML/CSV của một module đều có thể mang một external id (còn gọi là XML id) dạng module.tên_kỹ_thuật. Đó là một bí danh do con người đặt trỏ tới một bản ghi cụ thể. env.ref(xmlid) tra bí danh đó và trả về recordset tương ứng:

admin = env.ref('base.user_admin')    # res.users(2,)
nhom  = env.ref('base.group_user')    # res.groups(1,)

Ảnh chụp đoạn mã Python nền tối minh hoạ env chấm ref, hai dòng đầu lấy base chấm user_admin và base chấm group_user, một dòng ref tới id không tồn tại ném ValueError, một dòng dùng raise_if_not_found bằng False để nhận None, một hàm mo_danh_sach_the lấy action quan_ca_phe chấm action_the_thanh_vien bằng external id, và dòng cuối get_external_id trả về xml id của bản ghi

Hình 1: Các cách dùng env.ref. Truyền external id dạng module.tên, nhận về recordset. Không tìm thấy thì mặc định ném ValueError; thêm raise_if_not_found=False để nhận None. Trong module, ta tham chiếu chính action của mình bằng external id thay vì id số.

Chạy thật trên hệ thống:

Ảnh chụp phiên odoo shell blog19, env.ref base chấm user_admin trả về res.users chứa id 2 tên Administrator, env.ref base chấm group_user trả về res.groups chứa id 1, một dòng env.ref với id không tồn tại và raise_if_not_found bằng False trả về None còn không có tham số thì ném ValueError External ID not found, một dòng ir.model.data cho thấy module base name group_user model res.groups res_id 1, hai external id của module quan_ca_phe là action_the_thanh_vien và model_quan_the_thanh_vien, và get_external_id trả về từ điển ánh xạ id 2 sang base chấm user_admin

Hình 2: env.ref biến external id thành record. base.user_admin ra res.users(2,) (Administrator), base.group_user ra res.groups(1,). Để ý: cùng là "admin" nhưng đây là user id 2, còn SUPERUSER_ID là user id 1 (OdooBot) — một lý do nữa để không đoán id số mà dùng external id.

Đằng sau nó: bảng ir.model.data

env.ref không có phép thuật gì. Mỗi external id là một dòng trong bảng ir.model.data, lưu bốn thứ: module, name, model, res_id. Khi bạn gọi env.ref('base.group_user'), Odoo tách chuỗi thành module='base', name='group_user', tra bảng đó ra model='res.groups' và res_id=1, rồi trả về res.groups(1,).

env['ir.model.data'].search([('module','=','base'), ('name','=','group_user')])
# module=base  name=group_user  model=res.groups  res_id=1

Hiểu điều này giải thích luôn vì sao external id ổn định qua các lần cài: khi module cài lại hay nâng cấp, Odoo dùng chính bảng này để biết "bản ghi tên group_user của module base là dòng nào" và cập nhật đúng dòng đó thay vì tạo trùng. Cái tên là mỏ neo; id số bên dưới có thể khác nhau giữa các hệ thống.

Module của bạn cũng sinh external id. Module demo quan_ca_phe có 63 external id — cho model, action, view, menu... Ví dụ quan_ca_phe.action_the_thanh_vien trỏ tới một ir.actions.act_window (res_id 918). Nhờ vậy trong code hay trong file XML khác, bạn tham chiếu tới action đó bằng tên, không bao giờ bằng số.

Khi external id không tồn tại

Mặc định, env.ref ném ValueError nếu không tìm thấy:

env.ref('base.khong_co_dau')   # ValueError: External ID not found in the system: base.khong_co_dau

Đây thường là hành vi bạn muốn: nếu code phụ thuộc một bản ghi mà nó biến mất, hỏng to ngay còn hơn chạy tiếp với None rồi nổ ở chỗ khác khó lần. Nhưng khi external id có thể vắng mặt một cách hợp lệ (ví dụ nó thuộc một module tuỳ chọn có thể chưa cài), truyền raise_if_not_found=False để nhận None:

rec = env.ref('mo_dun_tuy_chon.co_the_thieu', raise_if_not_found=False)
if rec:
    ...   # chỉ dùng khi thật sự có

Chiều ngược: từ record ra external id

Có sẵn recordset, muốn biết external id của nó (hữu ích khi viết migration, hay debug), dùng get_external_id():

admin.get_external_id()   # {2: 'base.user_admin'}

Nó trả về một dict {id: 'module.name'}. Bản ghi không có external id nào (ví dụ dữ liệu người dùng tạo tay) sẽ cho chuỗi rỗng ở id đó.

Dùng env.ref cho đúng

Vài lưu ý rút ra từ cơ chế trên:

def mo_danh_sach_the(self):
    action = self.env.ref('quan_ca_phe.action_the_thanh_vien')
    return action._get_action_dict()
  • Dùng external id thay cho id số ở mọi tham chiếu cứng. Nhóm quyền, action, view, sequence, template email — tất cả nên gọi bằng env.ref('module.name'), không browse(số).
  • Đặt module prefix cho đúng. env.ref('group_user') thiếu prefix sẽ lỗi; luôn ghi base.group_user. Prefix cho biết bản ghi thuộc module nào.
  • env.ref trả về đúng một record (không phải nhiều). Nếu cần một tập, dùng search; env.ref là cho những bản ghi định danh duy nhất.
  • Cẩn thận vòng phụ thuộc: gọi env.ref tới external id của module khác nghĩa là module bạn phụ thuộc module đó — phải khai trong depends của manifest, nếu không thứ tự cài có thể khiến ref chưa tồn tại.

Ba ý mang về

  1. External id (module.name) là tên bất biến của một bản ghi, ổn định qua các lần cài/nâng cấp — dùng nó thay cho id số vốn khác nhau giữa các hệ thống. env.ref(xmlid) biến tên đó thành recordset.
  2. Mỗi external id là một dòng trong ir.model.data (module, name, model, res_id); đó là cách Odoo neo tên vào bản ghi và cập nhật đúng chỗ khi cài lại.
  3. env.ref mặc định ném ValueError khi không thấy; truyền raise_if_not_found=False để nhận None khi external id có thể vắng mặt hợp lệ. Chiều ngược dùng get_external_id().

Lần sau ta bước vào một chủ đề hiệu năng ít người để ý nhưng gây bug rất kỳ quặc: Phần sau nói về cache của ORM — Odoo nhớ giá trị field trong bộ đệm ra sao, và khi nào bạn phải chủ động làm mới nó.