在上一篇文章中,我們概覽了模型創建以及如何從模型中載入和導出數據。現在我們已有數據模型和相關數據,是時候學習如何編程與其進行交互 了。模型的 ORM(Object-Relational Mapping)提供了一些交互數據的方法,稱為 API(Application Programming Interface)。這包括基本的增刪改查(CRUD)操作,也包括一些其它操作,如數據導入導出,以及改善用戶界面和體驗的工具方法。它還包含一些我們在前面文章中所看到的裝飾器。這些都讓我們可以通過添加新的方法來調用 ORM 進行相關操作。
本文主要內容有:
- 使用 shell 命令交互式地學習 ORM API
- 理解執行環境和上下文
- 使用記錄集和作用域(domain)查詢數據
- 在記錄集中訪問數據
- 在記錄中寫入
- 編寫記錄集
- 使用底層 SQL 和數據庫事務
開發准備
本文代碼使用交互式 shell 命令行執行,無需使用前面章節的代碼。
使用 shell 命令行
Python帶有命令行界面,是研究其語法一個很好的方式。Odoo 也有類似的功能,可以交互式的測試命令的執行效果,這就是 shell 命令行。在命令行中執行以下命令並指定數據庫即可使用:
windows在pychamr中terminal中的啟用方法:
python odoo-bin shell -d odoo13
-d 數據庫
linux中的啟用方法
在odoo的目錄下可以直接用以下命令執行,odoo12是指數據庫名
python odoo-bin shell -d odoo12
此時在終端上可以看到正常的服務啟動信息,等到出現>>>Python提示符時即為完成,可以輸入命令了。
ℹ️Odoo 9中的修改
shell 功能在9.0中才添加。Odoo 8.0可使用社區模塊來添加這一功能。只需下載並放入 addons 路徑即可使用,下載請見應用市場。
此處 self 表示管理員用戶的記錄,可通過如下命令進行確認:
在以上 shell 會話中,我們檢查了自己的環境:
- self命令表示res.users記錄集,僅包含一條 id 為1的記錄
- 查看self._name獲得記錄集模型名,你可能猜到了,是’res.users’
- 記錄的 name 值為OdooBot
- 記錄的 login 字段值為__system__
ℹ️Odoo 12中的修改
id 號為1的超級用戶由原來的 admin 變成無法直接登錄的內部系統用戶。現在 admin 的 id 號為 2並且不是超級用戶,但默認各應用會將其加入所有安全組。主要原因是避免用戶使用超級用戶賬號來執行日常操作。這樣的風險是該用戶會跳過權限規則並導致數據的不一致,比如跨公司(cross-company)關聯。現在超級用戶僅用於檢測問題或具體的跨公司操作。
和 Python 一樣,可通過 Ctrl + D退出該命令行。此時會結束服務並返回到系統shell 命令行。

執行環境
Odoo shell 中包含一個 self 引用,類似於在res.users模型的方法中看到的那樣。如我們所見,self 是一個記錄集。記錄集自帶環境信息,包括瀏覽信息的用戶以及其它上下文信息,如語言和時區。下面我們會學習執行環境中可用的屬性、環境上下文的用處以及如何修改該上下文。
環境屬性
我們可通過如下代碼查看當前環境:
self.env 中的執行環境中有以下屬性:
- env.cr是正在使用的數據庫游標(cursor)
- env.user是當前用戶的記錄
- env.uid是會話用戶 id,與env.user.id相同
- env.context是會話上下文的不可變字典
環境還提供對帶有所有已安裝模型注冊表的訪問,如self.env[‘res.partner’]返回一條對 partner 模型的引用。然后我們還可以對其使用search()或browse()方法來獲取記錄集:
上例中返回的res.partner模型記錄集包含三條記錄,id 分別為10, 35和3。記錄集並沒有按 id 排序,因為使用了相應模型的默認排序。就 partner 模型而言,默認的_order為display_name。
環境上下文
環境上下文是一個帶有會話數據的字典,可用於客戶端用戶界面以及服務端 ORM 和業務邏輯中。在客戶端中,它可以把信息從一個視圖帶到另一個視圖中,比如前一個視圖中活躍的記錄 id,通過點擊鏈接或按鈕,可將默認值帶入到下一個視圖中。在服務端中,一些記錄集的值會依賴於上下文提供的本地化設置。具體的例子有lang鍵影響可翻譯字段的值。上下文還可為服務端代碼提供信號。比如active_test鍵在設為 False 時,會改變ORM中search()方法的行為,它會忽略記錄中的active標記,inactive(假刪除)的記錄也會被返回。
客戶端的初始上下文長這樣:
補充:服務端查看上下文命令為self.context_get()或self.env.context
其中 lang 鍵為用戶語言,tz 為時區信息,uid 為當前用戶 id。記錄中的內容隨當前依賴的上下文可能會不同:
- translated字段根據活躍的 lang 語言不同值也會不同
- datetimep字段根據活躍的的 tz 時區不同時間會不同
在上一個視圖中點擊鏈接或按鈕打開表單時,一個active_id鍵會被加入上下文,它帶有原表單我們所在位置記錄的 id。以列表視圖為例,active_ids上下文鍵中包含上一個列表中所選擇的記錄 id 列表。
在客戶端中,上下文可用於使用default_或default_search_前綴在目錄視圖上設置默認值或啟動默認過濾器。舉例如下:
- 設置當前用戶為user_id字段默認值,使用{‘default_user_id’: uid}
- 在目標視圖上默認啟動filter_my_books過濾器,使用{‘default_search_filter_my_tasks’: 1}
修改記錄集執行環境
記錄集執行環境是不可變的,因此不能被修改,但我們可以創建一個變更環境並使用它來執行操作。我們通過如下方法來實現:
- env.sudo(user)中傳入一條用戶記錄並返回該用戶的環境。如未傳入用戶,則使用__system__超級用戶root,這時可繞過安全規則執行指定操作。
- env.with_context(<dictionary>) 替換原上下文為新的上下文
- env.with_context(key=value,…)修改當前上下文,為一些鍵設置值
此外還有一個env.ref()函數,傳入一個外部標識符字符串並返回它的記錄,請參見:
使用記錄集和作用域(domain)查詢數據
在方法或 shell 會話中,self表示當前模型,並且我們僅能訪問該模型的記錄。要訪問其它模型就需要使用self.env。例如self.env[‘res.partner’]返回一條對 Partner 模型的引用(也是一個空記錄集)。我們可以使用search()或browse()來獲取記錄集,其中search()方法使用域表達式來定義記錄選擇范圍。
創建記錄集
search()方法接收一個域表達式並返回符合條件記錄的記錄集。空域[] 將返回所有記錄。
ℹ️如果模型有特殊字段 active,默認只有active=True的記錄才在選擇范圍內
還可以使用以下關鍵字參數:
- order是一個數據庫查詢語句中ORDER BY使用的字符串,通常是一個逗號分隔的字段名列表。每個字段都可接DESC關鍵字,用於表示倒序排列。
- limit設置獲取記錄的最大條數
- offset忽略前 n 前記錄,可配合limit使用來一次查詢指定范圍記錄
有時我們只要知道滿足某一條件的記錄條數,這時可使用search_count()來返回記錄條數而非記錄集。這節約了先獲取記錄列表再記數的開銷,在還沒有獲取記錄集且僅想知道記錄條數時這樣會更高效。
browse()方法接收一個 ID 列表或單個ID並返回這些記錄的記錄集。在我們知道 ID 並想要獲取記錄時這就非常方便了。
一些使用示例如下:
域表達式
域(domain)用於過濾數據記錄。它使用一個特殊語法來供 Odoo ORM解析,生成數據庫查詢中的 WHERE 表達式。域表達式是一組條件組成的列表,每個條件都是一個(‘字段名’, ‘運算符’, ‘值’)組成的元組,例如,[(‘is_done’,’=’,False)]是僅帶有一個條件的有效域表達式。以下是對各個元素的說明:
- 字段名:是一個待過濾字段,可使用點號標記來表示關聯模型中的字段
- 值:在 Python 表達式中運行。可使用字面值,如數字、布爾值、字符串和列表,也可使用運行上下文中的字段和標識符。針對域其實有兩種運行上下文:
- 在窗口操作或字段屬性等客戶端中使用時,可使用原生字段值來渲染當前可用視圖,但不能對其使用點標記符
- 在服務端使用時,如安全記錄規則或服務端 Python 代碼中,可以對字段使用點標記符,因為當前記錄是一個對象
- 運算符:可以是以下中的一個
- 常用比較運算符有<, >, <= , >=, =和!=。
- ‘=like’和’=ilike’匹配某一模式,這里下划線_匹配單個字符,百分號%匹配任意一組字符。
- ‘like’匹配’%value%’模式,’ilike’與其相似但忽略大小寫。還可以使用’not like’和’not ilike’運算符。
- ‘child of’在配置支持層級關聯的模型中查找層級關系中的子級值。
- ‘in’ 和’not in’用於查看給定列表的包含,所以其值為一個列表。用於to-many關聯字段時,in運算符和contains運算符一樣。
- ‘not in’是in的反向運算,用於查看不在列表中的值。
域表達式是一個列表並且包含多個條件元組。默認這些條件使用AND邏輯運算符連接,也就是說它僅返回滿足所有條件的記錄。也可以使用顯式邏輯運算符 – ‘&‘符號表示 AND 運算符(默認值),管道運算符’|‘表示OR運算符。這兩個運算符會作用於接下來的兩項,遞歸執行。后面我們會一起來詳細了解。
ℹ️域表達式使用了更為正式的定義方式:前綴標記法,也稱波蘭表達式(Polish notation):運算符放在運算項之前。AND和OR是二元運算符,而NOT是一元運算符。
感嘆號’!’表示NOT運算符,可用於下一項的運算,因此要放執行的否定項之前。例如[‘!’, (‘is_done’,’=’,True)]將過濾出所有未完成(not-don e)的記錄。
下一項本身也可以是一個作用其后續項的運算符,形成一個嵌套條件。下例可以有助於我們進行理解。在服務端記錄規則中,可以找到類似下面這樣的域表達式:
這個域過濾出當前用戶在follower列表中並且是負責人用戶,或者沒有負責人用戶的用戶集。第一個’|’或運算符作用於 follower 條件以及下一個條件的結果。下一個條件是后面兩個條件的並集:用戶ID是當前會話用戶或未進行設置。下圖是上例域表達式的抽象語法樹表示:

在記錄集中訪問數據
一旦獲取了數據集,就可以查看其中包含的數據了。下面的幾個部分中我們就來看看如何訪問記錄集中的數據。我們可以獲取單條記錄的字段值,稱為單例(singleton)。關聯字段帶有特殊屬性,我們可通過點號標記來查看關聯記錄。最后我們一起思考處理日期和時間記錄並進行格式轉換。
訪問記錄中數據
記錄集的一個特例是僅有一條記錄,稱為單例。單例仍是記錄集,在需要記錄集的地方均可使用。與多元素記錄集不同,單例可使用點號標記訪問它的字段,如:
下個例子中我們看看同一個 self 單例和記錄集相同的行為,我們可對其進行遍歷。它只有一條記錄,所以只會打印出一個名稱:
嘗試訪問有多條記錄的記錄集字段值會產生錯誤,所以在不確定操作的是否為單例數據集時就會產生問題。對於設計僅操作單例的方法,可在開頭處使用self.ensure_one(),如果 self 不是單例時將拋出錯誤。
ℹ️空記錄也是單例。這樣很方便,因為訪問字段會返回 None 而非拋出錯誤。對於關聯字段同樣如此,使用點號標記訪問關聯記錄也不會拋出錯誤。
訪問關聯字段
如前面所見,模型可包含關聯字段:many-to-one, one-to-many和many-to-many。這些字段類型的值為記錄集。
對於many-to-one,其值可以是單例或空記錄集。兩種情況下都可以直接訪問字段值。如下例中的命令是正確並安全的:
為避免麻煩,空記錄可像單例一樣操作,訪問其字段值不會返回錯誤而是返回 False。所以我們可以使用點號標記來遍歷字段,而無需擔心因其值為空而報錯,如:
訪問時間和日期值
在記錄集中,日期和日期時間值以原生 Python 對象展示,例如,在查詢上次 admin 用戶登錄日期時:
因為日期和日期時間是 Python 對象,它們可使用這些對象的所有功能。
ℹ️Odoo 12中的修改
date和datetime字段值以 Python 對象表示,而此前 Odoo 版本中它們以文本字符串表示。這些字段類型值仍可像此前 Odoo 版本中那樣使用文本表示。
日期和時間在數據庫中以原生的世界標准時間(UTC) 格式存儲,不受時區影響。 在記錄集中看到的datetime值也是 UTC格式,在客戶端中向用戶展示時,datetime值會根據當前會話的時間設置來轉換成用戶的時區。這一設置存儲在上下文的tz鍵中,如{‘tz’: ‘Europe/Brussels’}。這一轉換由客戶端負責,而不是由服務端完成。
例如在布魯塞爾(UTC+1)的用戶輸入12:00 AM數據庫中會存儲為10:00 AM UTC,而在紐約(UTC-4) 的用戶查看時則為06:00 AM。
補充:請不要懷疑作者的數學是不是體育老師教的😂,布魯塞爾為東一區,紐約為西五區,但冬令時和夏令時讓這個問題變復雜了。將12:00修改為11:00應該就正確了。
ℹ️Odoo 服務日志消息時間戳使用UTC時間而非本地服務器時間
相反的轉換,由會話時區轉換為UTC,也需由客戶端在將用戶輸入的datetime傳回服務器時完成。日期對象可進行比較和相減來獲取兩個日期的時間差,時間差是一個timedelta對象。timedelta可通過date運算對date和datetime對象進行加減。這些對象由 Python 標准庫datetime模塊提供,以下是使用它進行的基本運算示例:
對於date, datetime和timedelta數據類型的完整參考請見Python 官方文檔。Odoo 還在odoo.tools.date_utils模塊中提供了一些額外的便利函數,這些函數有:
- start_of(value, granularity)是某個特定刻度時間區間的開始時間,這些刻度有year, quarter, month, week, day或hour
- end_of(value, granularity)是某個特定刻度時間區間的結束時間
- add(value, **kwargs)為指定值加上一個時間間隔。**kwargs參數由一個relativedelta對象來定義時間間隔。這些參數可以是years, months, weeks, days, hours, minutes等等
- subtract(value, **kwargs)為指定值減去一個時間間隔
relativedelta對象來自dateutil庫,可使用months或years執行date運算(Python的timedelta標准庫僅支持days)。更多內容請見相關文檔。以下為上述函數的一些使用示例:
這些工具方法在odoo.fields.Date和the odoo.fields.Datetime對象中也可使用,如:
- fields.Date.today()返回服務器所需格式的當前日期,它使用UTC作為一個引用。這足以計算默認值,這種情況下只需使用函數名無需添加括號。
- fields.Datetime.now() 返回服務器所需格式的當前datetime,它使用UTC作為一個引用。這足以計算默認值,
- fields.Date.context_today(record, timestamp=None)在會話上下文中返回帶有當前日期的字符串。時間從記錄上下文中獲取。可選項timestamp參數是一個datetime對象,如果傳入將不使用當前時間,而使用傳入值。
- fields.Datetime.context_timestamp(record, timestamp)將原生的datetime值(無時區)轉換為具體時區的datetime。時區從記錄上下文中提取,因此使了前述函數名。
轉換文本形式的日期和時間
在Odoo 12以前,在進行運算前我們需要對文本形式的date和datetime進行轉換。有些工作可幫助我們完成文本和原生數據類型的相互轉換。這在此前的 Odoo 版本中都非常有用並且在 Odoo 12中也仍然相關:我們要將給到的日期格式化為文本。為便於格式之間的轉換,fields.Date和fields.Datetime都提供了如下函數:
- to_date將字符串轉換為date對象
- to_datetime(value)將字符串轉換為datetime對象
- to_string(value)將date或datetime對象轉換為 Odoo 11及之前版本Odoo服務所需的字符串格式
函數所需的文本格式由 Odoo 通過如下方式默認預置:
- odoo.tools.DEFAULT_SERVER_DATE_FORMAT
- odoo.tools.DEFAULT_SERVER_DATETIME_FORMAT
它們分別與%Y-%m-%d和%Y-%m-%d %H:%M:%S相對應。from_string用法示例如下:
對於其它的日期和時間格式,可使用datetime對象中的strptime方法:
在記錄中寫入
有兩種寫入記錄的方式:使用對象形式直接分配和使用write() 方法。第一種很簡單但一次只能操作一條記錄,效率較低。因為每次分配都執行一次寫操作,會產生冗余的重復計算。第二種要求寫入關聯字段時使用特殊語法,但每條命令可寫入多個字段和記錄,記錄計算更為高效。
使用對象形式分配值寫入
記錄集實施活躍記錄模式。也就是說我們可以為其分配值,並且會將這些修改在數據庫中持久化存儲。這是一種操作數據的易於理解和便捷的方式,但一次只能操作一個字段和一條記錄。如:
雖然使用的是活躍記錄模式,也可以通過分配記錄值來設置關聯字段。對於many-to-one字段,分配的值必須是單條記錄(單例)。對於to-many字段,也可以通過一條記錄集分配,來替換關聯記錄列表為新列表(如果有的話),這里允許任何大小的記錄集。
通過 write()方法寫入
我們還可以使用write()方法來同時更新多條記錄中的多個字段,僅需一條數據庫命令。所以在重視效果時就應優先考慮這一方式。write() 接收一個字典來進行字段和值的映射。這會更新記錄集中的所有記錄並且沒有返回值,如:
與對象形式的分配不同,使用write() 方法時我們不能直接為關聯字段分配記錄集對象。取而代之的是,我們需要使用所需的記錄ID來從記錄集中進行提取。在寫入many-to-one字段時,寫入的值必須是關聯記錄的ID。例如,我們不用self.write({‘user_id’: self.env.user}),而應使用self.write({‘user_id’: self.env.user.id})。
在寫入to-many字段時,寫入的值必須使用和 XML 數據文件相同的特殊語法,這在第五章 Odoo 12開發之導入、導出以及模塊數據中有介紹。比如,我們設置圖書作者列表為author1和author2,這是兩條 Partner 記錄。| 管道運算符可拼接記錄來創建一個記錄集,因此使用對象形式的分配可以這么寫:
使用write()方法,同樣的操作如下:
回顧第五章 Odoo 12開發之導入、導出以及模塊數據的寫入語法,最常用的命令如下:
- (4, id, _)添加一條記錄
- (6, _, [ids])替換關聯記錄列表為所傳入的列表
寫入日期和時間值
從 Odoo 12開始,不論是直接分配還是使用 write()方法,日期和時間字段都可以 Python 原生數據類型寫入。我們仍可以使用文本形式值寫入日期和時間:
創建和刪除記錄
write()方法用於向已有記錄寫入日期,但我們還需要創建和刪除記錄。這通過create()和unlink()模型方法實現。create()接收所需創建記錄字段和值組成的字典,語法與 write()一致。沒錯,默認值會被自動應用,如下所示:
unlink()方法會刪除記錄集中的記錄,如下所示:
以上我們看到日志中幾條其它記錄被刪除的消息,這些是所刪除 partner 關聯字段的串聯刪除。
還有copy()模型方法可用於復制已有記錄,它接收一個可選參數來在新記錄中修改值,如復制demo 用戶創建一個新用戶:
帶有copy=False屬性的字段不會被自動拷貝。to-many關聯字段帶有該標記時默認被禁用,因此也不可拷貝。
重構記錄集
記錄集還支持一些其它運算。我們可查看一條記錄是否在記錄集中。如果x是一個單例,並且my_recordset是一個包含多條記錄的記錄集,可使用如下代碼:
- x in my_recordset
- x not in my_recordset
還能使用如下運算:
- recordset.ids 返回記錄集元素的ID列表
- recordset.ensure_one()檢查是否為單條記錄(單例);若不是,則拋出ValueError異常
- recordset.filtered(func)返回一個過濾了的記錄集,func可以是一個函數或一個點號分隔的表達式來表示字段路徑,可參見下面的示例。
- recordset.mapped(func)返回一個映射值列表。除函數外,還可使用文本字符串作為映射的字段名。
- recordset.sorted(func)返回一個排好序的記錄值。除函數外,文本字符串可用作排序的字段名。reverse=True是其可選參數。
以下是這些函數的使用示例:
我們勢必會對這些關聯字段中的元素進行添加、刪除或替換的操作,那么就帶來了一個問題:如何操作這些記錄集呢?
記錄集是不可變的,也就是說不能直接修改其值。那么修改記錄集就意味着在原有的基礎上創建一個新的記錄集。一種方式是使用所支持的集合運算:
- rs1 | rs2是一個集合的並運算,會生成一個包含兩個記錄集所有元素的記錄集
- rs1 + rs2是集合加法運算,會將兩個記錄集拼接為一個記錄集,這可能會帶來集合中有重復記錄
- rs1 & rs2是集合的交集運算,會生成一個僅在兩個記錄集中同時出現元素組成的數據集
- rs1 – rs2是集合的差集運算,會生成在rs1中有但rs2中沒有的元素組成的數據集
還可以使用分片標記,例如:
- rs[0]和rs[-1]分別返回第一個和最后一個元素
- rs[1:]返回除第一元素外的記錄集拷貝。其結果和rs – rs[0]相同,但保留了排序
ℹ️Odoo 10中的修改
從Odoo 10開始,記錄集操作保留了排序。此前的 Odoo 版本中,記錄集操作不一定會保留排序,雖然加運算和切片已知是保留排序的。
我們可以用如下運算通過刪除或添加元素來修改記錄集:
- self.author_ids |= author1:如果不存在author1,它會將author1加入記錄集
- self.author_ids -= author1:如果author1存在於記錄集中,會進行刪除
- self.author_ids = self.author_ids[:-1]刪除最后一條記錄
關聯字段包含記錄集值。many-to-one 可包含單例記錄集,to-many字段包含任意數量記錄的記錄集。
使用底層 SQL 和數據庫事務
數據庫引入運算在一個數據庫事務上下文中執行。通常我們無需擔心這點,因為服務器在運行模型方法時會進行處理。但有些情況下,可能需要對事務進行更精細控制。這可通過數據庫游標self.env.cr來實現,如下所示:
- self.env.cr.commit()執行事務緩沖的寫運算
- self.env.cr.rollback()取消上次 commit之后的寫運算,如果尚未 commit,則回滾所有操作
小貼士:在shell會話中,直到執行self.env.cr.commit()時數據操作才會在數據庫中生效
通過游標execute() 方法,我們可以直接在數據庫中運行 SQL 語句。它接收一個要運行的SQL 語句,以及第二個可選參數:一個用作 SQL 參數值的元組或列表。這些值會用在%s占位符之處。
- ℹ️注意:
在cr.execute() 中我們不應直接編寫拼接參數的SQL查詢。眾所周知這樣做會帶來SQL注入攻擊的安全風險。保持使用%s占位符並通過第二個參數來傳值。
如果使用SELECT查詢,會獲取到記錄。fetchall() 函數以元組列表的形式獲取所有行,dictfetchall()則以字典列表的形式獲取,示例如下:
還可以使用數據操縱語言(DML) 來運行指令,如UPDATE和INSERT。因為服務器保留數據緩存,這可能導致與數據庫中實際數據的不一致。出於這個原因,在使用原生DML后,應使用self.env.cache.invalidate()清除緩存。
ℹ️注意:
直接在數據庫中執行SQL語句可能會導致數據不一致,請僅在確定時進行該操作。
總結
在本文中,我們學習了如何操作模型數據以及執行 CRUD 運算:創建、讀取、更新和刪除數據。這是實現我們的業務邏輯和自動化的基石。
對於ORM API的測試,我們使用了Odoo交互式 shell 命令行。我們通過self.env環境運行了命令,該環境可訪問模型注冊表並提供命令運行相關信息的上下文,如當前語言 lang 和時區 tz。
記錄集使用search(<domain>)或browse([<ids>])ORM 方法創建。之后可對其進行遍歷訪問每個單例(一條獨立的記錄)。我們還可以使用對象樣式的點號標記在單例中獲取和設置記錄值。
除直接為單例分配值外,我們還可以使用write(<dict>)來通過單條命令更新記錄集中的所有元素。create(), copy()和unlink()命令用於創建、拷貝和刪除記錄。
記錄集可被檢查和操作,檢查運算符包含in和not in。重構運算符包含並集的|,交集的&以及切片:。可用的轉換包含提取 ID 列表的.ids、.mapped(<field>)、.filtered(<func>) 或.sorted(<func>)。
最后,通過self.env.cr中暴露的游標對象可控制底層 SQL 運行和事務控制。
在下一篇文章中,我們將為模型添加業務邏輯層,實現通過ORM API來自動化操作的模型方法。
