Odoo 開(kāi)發(fā)手冊(cè)連載七 記錄集 – 使用模型數(shù)據(jù)

本文為最好用的免費(fèi)ERP系統(tǒng)Odoo 12開(kāi)發(fā)手冊(cè)系列文章第七篇。

在上一篇文章中,我們概覽了模型創(chuàng)建以及如何從模型中載入和導(dǎo)出數(shù)據(jù)。現(xiàn)在我們已有數(shù)據(jù)模型和相關(guān)數(shù)據(jù),是時(shí)候?qū)W習(xí)如何編程與其進(jìn)行交互 了。模型的 ORM(Object-Relational Mapping)提供了一些交互數(shù)據(jù)的方法,稱(chēng)為 API(Application Programming Interface)。這包括基本的增刪改查(CRUD)操作,也包括一些其它操作,如數(shù)據(jù)導(dǎo)入導(dǎo)出,以及改善用戶(hù)界面和體驗(yàn)的工具方法。它還包含一些我們?cè)谇懊嫖恼轮兴吹降难b飾器。這些都讓我們可以通過(guò)添加新的方法來(lái)調(diào)用 ORM 進(jìn)行相關(guān)操作。

本文主要內(nèi)容有:

  • 使用 shell 命令交互式地學(xué)習(xí) ORM API
  • 理解執(zhí)行環(huán)境和上下文
  • 使用記錄集和作用域(domain)查詢(xún)數(shù)據(jù)
  • 在記錄集中訪問(wèn)數(shù)據(jù)
  • 在記錄中寫(xiě)入
  • 編寫(xiě)記錄集
  • 使用底層 SQL 和數(shù)據(jù)庫(kù)事務(wù)

開(kāi)發(fā)準(zhǔn)備

本文代碼使用交互式 shell 命令行執(zhí)行,無(wú)需使用前面章節(jié)的代碼。

使用 shell 命令行

Python帶有命令行界面,是研究其語(yǔ)法一個(gè)很好的方式。Odoo 也有類(lèi)似的功能,可以交互式的測(cè)試命令的執(zhí)行效果,這就是 shell 命令行。在命令行中執(zhí)行以下命令并指定數(shù)據(jù)庫(kù)即可使用:

~/odoo-dev/odoo/odoo-bin shell -d dev12

此時(shí)在終端上可以看到正常的服務(wù)啟動(dòng)信息,等到出現(xiàn)>>>Python提示符時(shí)即為完成,可以輸入命令了。

??Odoo 9中的修改
shell 功能在9.0中才添加。Odoo 8.0可使用社區(qū)模塊來(lái)添加這一功能。只需下載并放入 addons 路徑即可使用,下載請(qǐng)見(jiàn)應(yīng)用市場(chǎng)。

此處 self 表示管理員用戶(hù)的記錄,可通過(guò)如下命令進(jìn)行確認(rèn):

 self
res.users(1,)
>>> self._name
'res.users'
>>> self.login
'__system__'

在以上 shell 會(huì)話(huà)中,我們檢查了自己的環(huán)境:

  • self命令表示res.users記錄集,僅包含一條 id 為1的記錄
  • 查看self._name獲得記錄集模型名,你可能猜到了,是'res.users'
  • 記錄的 name 值為OdooBot
  • 記錄的 login 字段值為system

??Odoo 12中的修改
id 號(hào)為1的超級(jí)用戶(hù)由原來(lái)的 admin 變成無(wú)法直接登錄的內(nèi)部系統(tǒng)用戶(hù)?,F(xiàn)在 admin 的 id 號(hào)為 2并且不是超級(jí)用戶(hù),但默認(rèn)各應(yīng)用會(huì)將其加入所有安全組。主要原因是避免用戶(hù)使用超級(jí)用戶(hù)賬號(hào)來(lái)執(zhí)行日常操作。這樣的風(fēng)險(xiǎn)是該用戶(hù)會(huì)跳過(guò)權(quán)限規(guī)則并導(dǎo)致數(shù)據(jù)的不一致,比如跨公司(cross-company)關(guān)聯(lián)?,F(xiàn)在超級(jí)用戶(hù)僅用于檢測(cè)問(wèn)題或具體的跨公司操作。

和 Python 一樣,可通過(guò) Ctrl + D退出該命令行。此時(shí)會(huì)結(jié)束服務(wù)并返回到系統(tǒng)shell 命令行。

Odoo 12 shell 命令行

執(zhí)行環(huán)境

Odoo shell 中包含一個(gè) self 引用,類(lèi)似于在res.users模型的方法中看到的那樣。如我們所見(jiàn),self 是一個(gè)記錄集。記錄集自帶環(huán)境信息,包括瀏覽信息的用戶(hù)以及其它上下文信息,如語(yǔ)言和時(shí)區(qū)。下面我們會(huì)學(xué)習(xí)執(zhí)行環(huán)境中可用的屬性、環(huán)境上下文的用處以及如何修改該上下文。

環(huán)境屬性

我們可通過(guò)如下代碼查看當(dāng)前環(huán)境:

 self.env
<odoo.api.Environment object at 0x7f78a26026a0>

self.env 中的執(zhí)行環(huán)境中有以下屬性:

  • env.cr是正在使用的數(shù)據(jù)庫(kù)游標(biāo)(cursor)
  • env.user是當(dāng)前用戶(hù)的記錄
  • env.uid是會(huì)話(huà)用戶(hù) id,與env.user.id相同
  • env.context是會(huì)話(huà)上下文的不可變字典

環(huán)境還提供對(duì)帶有所有已安裝模型注冊(cè)表的訪問(wèn),如self.env['res.partner']返回一條對(duì) partner 模型的引用。然后我們還可以對(duì)其使用search()或browse()方法來(lái)獲取記錄集:

 self.env['res.partner'].search([('name', 'like', 'Ad')])
res.partner(10, 35, 3)

上例中返回的res.partner模型記錄集包含三條記錄,id 分別為10, 35和3。記錄集并沒(méi)有按 id 排序,因?yàn)槭褂昧讼鄳?yīng)模型的默認(rèn)排序。就 partner 模型而言,默認(rèn)的_order為display_name。

環(huán)境上下文

環(huán)境上下文是一個(gè)帶有會(huì)話(huà)數(shù)據(jù)的字典,可用于客戶(hù)端用戶(hù)界面以及服務(wù)端 ORM 和業(yè)務(wù)邏輯中。在客戶(hù)端中,它可以把信息從一個(gè)視圖帶到另一個(gè)視圖中,比如前一個(gè)視圖中活躍的記錄 id,通過(guò)點(diǎn)擊鏈接或按鈕,可將默認(rèn)值帶入到下一個(gè)視圖中。在服務(wù)端中,一些記錄集的值會(huì)依賴(lài)于上下文提供的本地化設(shè)置。具體的例子有l(wèi)ang鍵影響可翻譯字段的值。上下文還可為服務(wù)端代碼提供信號(hào)。比如active_test鍵在設(shè)為 False 時(shí),會(huì)改變ORM中search()方法的行為,它會(huì)忽略記錄中的active標(biāo)記,inactive(假刪除)的記錄也會(huì)被返回。

客戶(hù)端的初始上下文長(zhǎng)這樣:

{'lang': 'en_US', 'tz': 'Europe/Brussels', 'uid': 2}

補(bǔ)充:服務(wù)端查看上下文命令為self.context_get()或self.env.context

其中 lang 鍵為用戶(hù)語(yǔ)言,tz 為時(shí)區(qū)信息,uid 為當(dāng)前用戶(hù) id。記錄中的內(nèi)容隨當(dāng)前依賴(lài)的上下文可能會(huì)不同:

  • translated字段根據(jù)活躍的 lang 語(yǔ)言不同值也會(huì)不同
  • datetimep字段根據(jù)活躍的的 tz 時(shí)區(qū)不同時(shí)間會(huì)不同

在上一個(gè)視圖中點(diǎn)擊鏈接或按鈕打開(kāi)表單時(shí),一個(gè)active_id鍵會(huì)被加入上下文,它帶有原表單我們所在位置記錄的 id。以列表視圖為例,active_ids上下文鍵中包含上一個(gè)列表中所選擇的記錄 id 列表。

在客戶(hù)端中,上下文可用于使用default_或default_search_前綴在目錄視圖上設(shè)置默認(rèn)值或啟動(dòng)默認(rèn)過(guò)濾器。舉例如下:

  • 設(shè)置當(dāng)前用戶(hù)為user_id字段默認(rèn)值,使用{'default_user_id': uid}
  • 在目標(biāo)視圖上默認(rèn)啟動(dòng)filter_my_books過(guò)濾器,使用{'default_search_filter_my_tasks': 1}

修改記錄集執(zhí)行環(huán)境

記錄集執(zhí)行環(huán)境是不可變的,因此不能被修改,但我們可以創(chuàng)建一個(gè)變更環(huán)境并使用它來(lái)執(zhí)行操作。我們通過(guò)如下方法來(lái)實(shí)現(xiàn):

  • env.sudo(user)中傳入一條用戶(hù)記錄并返回該用戶(hù)的環(huán)境。如未傳入用戶(hù),則使用system超級(jí)用戶(hù)root,這時(shí)可繞過(guò)安全規(guī)則執(zhí)行指定操作。
  • env.with_context(<dictionary>) 替換原上下文為新的上下文
  • env.with_context(key=value,...)修改當(dāng)前上下文,為一些鍵設(shè)置值

此外還有一個(gè)env.ref()函數(shù),傳入一個(gè)外部標(biāo)識(shí)符字符串并返回它的記錄,請(qǐng)參見(jiàn):

 self.env.ref('base.user_root')
res.users(1,)

使用記錄集和作用域(domain)查詢(xún)數(shù)據(jù)

在方法或 shell 會(huì)話(huà)中,self表示當(dāng)前模型,并且我們僅能訪問(wèn)該模型的記錄。要訪問(wèn)其它模型就需要使用self.env。例如self.env['res.partner']返回一條對(duì) Partner 模型的引用(也是一個(gè)空記錄集)。我們可以使用search()或browse()來(lái)獲取記錄集,其中search()方法使用域表達(dá)式來(lái)定義記錄選擇范圍。

創(chuàng)建記錄集

search()方法接收一個(gè)域表達(dá)式并返回符合條件記錄的記錄集??沼騕] 將返回所有記錄。

??如果模型有特殊字段 active,默認(rèn)只有active=True的記錄才在選擇范圍內(nèi)

還可以使用以下關(guān)鍵字參數(shù):

  • order是一個(gè)數(shù)據(jù)庫(kù)查詢(xún)語(yǔ)句中ORDER BY使用的字符串,通常是一個(gè)逗號(hào)分隔的字段名列表。每個(gè)字段都可接DESC關(guān)鍵字,用于表示倒序排列。
  • limit設(shè)置獲取記錄的最大條數(shù)
  • offset忽略前 n 前記錄,可配合limit使用來(lái)一次查詢(xún)指定范圍記錄

有時(shí)我們只要知道滿(mǎn)足某一條件的記錄條數(shù),這時(shí)可使用search_count()來(lái)返回記錄條數(shù)而非記錄集。這節(jié)約了先獲取記錄列表再記數(shù)的開(kāi)銷(xiāo),在還沒(méi)有獲取記錄集且僅想知道記錄條數(shù)時(shí)這樣會(huì)更高效。

browse()方法接收一個(gè) ID 列表或單個(gè)ID并返回這些記錄的記錄集。在我們知道 ID 并想要獲取記錄時(shí)這就非常方便了。

一些使用示例如下:

 self.env['res.partner'].search([('name', 'like', 'Pac')])
res.partner(42, 62)
>>> self.env['res.partner'].browse([42, 62])
res.partner(42, 62)

域表達(dá)式

域(domain)用于過(guò)濾數(shù)據(jù)記錄。它使用一個(gè)特殊語(yǔ)法來(lái)供 Odoo ORM解析,生成數(shù)據(jù)庫(kù)查詢(xún)中的 WHERE 表達(dá)式。域表達(dá)式是一組條件組成的列表,每個(gè)條件都是一個(gè)('字段名', '運(yùn)算符', '值')組成的元組,例如,[('is_done','=',False)]是僅帶有一個(gè)條件的有效域表達(dá)式。以下是對(duì)各個(gè)元素的說(shuō)明:

  • 字段名:是一個(gè)待過(guò)濾字段,可使用點(diǎn)號(hào)標(biāo)記來(lái)表示關(guān)聯(lián)模型中的字段
  • 值:在 Python 表達(dá)式中運(yùn)行??墒褂米置嬷?,如數(shù)字、布爾值、字符串和列表,也可使用運(yùn)行上下文中的字段和標(biāo)識(shí)符。針對(duì)域其實(shí)有兩種運(yùn)行上下文:
    • 在窗口操作或字段屬性等客戶(hù)端中使用時(shí),可使用原生字段值來(lái)渲染當(dāng)前可用視圖,但不能對(duì)其使用點(diǎn)標(biāo)記符
    • 在服務(wù)端使用時(shí),如安全記錄規(guī)則或服務(wù)端 Python 代碼中,可以對(duì)字段使用點(diǎn)標(biāo)記符,因?yàn)楫?dāng)前記錄是一個(gè)對(duì)象
  • 運(yùn)算符:可以是以下中的一個(gè)
    • 常用比較運(yùn)算符有<, >, <= , >=, =和!=。
    • '=like'和'=ilike'匹配某一模式,這里下劃線_匹配單個(gè)字符,百分號(hào)%匹配任意一組字符。
    • 'like'匹配'%value%'模式,'ilike'與其相似但忽略大小寫(xiě)。還可以使用'not like'和'not ilike'運(yùn)算符。
    • 'child of'在配置支持層級(jí)關(guān)聯(lián)的模型中查找層級(jí)關(guān)系中的子級(jí)值。
    • 'in' 和'not in'用于查看給定列表的包含,所以其值為一個(gè)列表。用于to-many關(guān)聯(lián)字段時(shí),in運(yùn)算符和contains運(yùn)算符一樣。
    • 'not in'是in的反向運(yùn)算,用于查看不在列表中的值。

域表達(dá)式是一個(gè)列表并且包含多個(gè)條件元組。默認(rèn)這些條件使用AND邏輯運(yùn)算符連接,也就是說(shuō)它僅返回滿(mǎn)足所有條件的記錄。也可以使用顯式邏輯運(yùn)算符 - '&'符號(hào)表示 AND 運(yùn)算符(默認(rèn)值),管道運(yùn)算符'|'表示OR運(yùn)算符。這兩個(gè)運(yùn)算符會(huì)作用于接下來(lái)的兩項(xiàng),遞歸執(zhí)行。后面我們會(huì)一起來(lái)詳細(xì)了解。

??域表達(dá)式使用了更為正式的定義方式:前綴標(biāo)記法,也稱(chēng)波蘭表達(dá)式(Polish notation):運(yùn)算符放在運(yùn)算項(xiàng)之前。AND和OR是二元運(yùn)算符,而NOT是一元運(yùn)算符。

感嘆號(hào)'!'表示NOT運(yùn)算符,可用于下一項(xiàng)的運(yùn)算,因此要放執(zhí)行的否定項(xiàng)之前。例如['!', ('is_done','=',True)]將過(guò)濾出所有未完成(not-don e)的記錄。

下一項(xiàng)本身也可以是一個(gè)作用其后續(xù)項(xiàng)的運(yùn)算符,形成一個(gè)嵌套條件。下例可以有助于我們進(jìn)行理解。在服務(wù)端記錄規(guī)則中,可以找到類(lèi)似下面這樣的域表達(dá)式:

['|',
    ('message_follower_ids', 'in', [user.partner_id.id]),
    '|',
        ('user_id', '=', user.id),
        ('user_id', '=', False)
]

這個(gè)域過(guò)濾出當(dāng)前用戶(hù)在follower列表中并且是負(fù)責(zé)人用戶(hù),或者沒(méi)有負(fù)責(zé)人用戶(hù)的用戶(hù)集。第一個(gè)'|'或運(yùn)算符作用于 follower 條件以及下一個(gè)條件的結(jié)果。下一個(gè)條件是后面兩個(gè)條件的并集:用戶(hù)ID是當(dāng)前會(huì)話(huà)用戶(hù)或未進(jìn)行設(shè)置。下圖是上例域表達(dá)式的抽象語(yǔ)法樹(shù)表示:

Odoo 12域表達(dá)式抽象語(yǔ)法樹(shù)

在記錄集中訪問(wèn)數(shù)據(jù)

一旦獲取了數(shù)據(jù)集,就可以查看其中包含的數(shù)據(jù)了。下面的幾個(gè)部分中我們就來(lái)看看如何訪問(wèn)記錄集中的數(shù)據(jù)。我們可以獲取單條記錄的字段值,稱(chēng)為單例(singleton)。關(guān)聯(lián)字段帶有特殊屬性,我們可通過(guò)點(diǎn)號(hào)標(biāo)記來(lái)查看關(guān)聯(lián)記錄。最后我們一起思考處理日期和時(shí)間記錄并進(jìn)行格式轉(zhuǎn)換。

訪問(wèn)記錄中數(shù)據(jù)

記錄集的一個(gè)特例是僅有一條記錄,稱(chēng)為單例。單例仍是記錄集,在需要記錄集的地方均可使用。與多元素記錄集不同,單例可使用點(diǎn)號(hào)標(biāo)記訪問(wèn)它的字段,如:

 print(self.name)
OdooBot

下個(gè)例子中我們看看同一個(gè) self 單例和記錄集相同的行為,我們可對(duì)其進(jìn)行遍歷。它只有一條記錄,所以只會(huì)打印出一個(gè)名稱(chēng):

 for rec in self:
...     print(rec.name)
...
OdooBot

嘗試訪問(wèn)有多條記錄的記錄集字段值會(huì)產(chǎn)生錯(cuò)誤,所以在不確定操作的是否為單例數(shù)據(jù)集時(shí)就會(huì)產(chǎn)生問(wèn)題。對(duì)于設(shè)計(jì)僅操作單例的方法,可在開(kāi)頭處使用self.ensure_one(),如果 self 不是單例時(shí)將拋出錯(cuò)誤。

??空記錄也是單例。這樣很方便,因?yàn)樵L問(wèn)字段會(huì)返回 None 而非拋出錯(cuò)誤。對(duì)于關(guān)聯(lián)字段同樣如此,使用點(diǎn)號(hào)標(biāo)記訪問(wèn)關(guān)聯(lián)記錄也不會(huì)拋出錯(cuò)誤。

訪問(wèn)關(guān)聯(lián)字段

如前面所見(jiàn),模型可包含關(guān)聯(lián)字段:many-to-one, one-to-many和many-to-many。這些字段類(lèi)型的值為記錄集。

對(duì)于many-to-one,其值可以是單例或空記錄集。兩種情況下都可以直接訪問(wèn)字段值。如下例中的命令是正確并安全的:

 self.company_id
res.company(1,)
>>> self.company_id.name
'YourCompany'
>>> self.company_id.currency_id
res.currency(1,)
>>> self.company_id.currency_id.name
'EUR'

為避免麻煩,空記錄可像單例一樣操作,訪問(wèn)其字段值不會(huì)返回錯(cuò)誤而是返回 False。所以我們可以使用點(diǎn)號(hào)標(biāo)記來(lái)遍歷字段,而無(wú)需擔(dān)心因其值為空而報(bào)錯(cuò),如:

 self.company_id.parent_id
res.company()
>>> self.company_id.parent_id.name
False

訪問(wèn)時(shí)間和日期值

在記錄集中,日期和日期時(shí)間值以原生 Python 對(duì)象展示,例如,在查詢(xún)上次 admin 用戶(hù)登錄日期時(shí):

 self.browse(2).login_date
datetime.datetime(2019, 1, 8, 9, 2, 54, 45546)

因?yàn)槿掌诤腿掌跁r(shí)間是 Python 對(duì)象,它們可使用這些對(duì)象的所有功能。

??Odoo 12中的修改
date和datetime字段值以 Python 對(duì)象表示,而此前 Odoo 版本中它們以文本字符串表示。這些字段類(lèi)型值仍可像此前 Odoo 版本中那樣使用文本表示。

日期和時(shí)間在數(shù)據(jù)庫(kù)中以原生的世界標(biāo)準(zhǔn)時(shí)間(UTC) 格式存儲(chǔ),不受時(shí)區(qū)影響。 在記錄集中看到的datetime值也是 UTC格式,在客戶(hù)端中向用戶(hù)展示時(shí),datetime值會(huì)根據(jù)當(dāng)前會(huì)話(huà)的時(shí)間設(shè)置來(lái)轉(zhuǎn)換成用戶(hù)的時(shí)區(qū)。這一設(shè)置存儲(chǔ)在上下文的tz鍵中,如{'tz': 'Europe/Brussels'}。這一轉(zhuǎn)換由客戶(hù)端負(fù)責(zé),而不是由服務(wù)端完成。

例如在布魯塞爾(UTC+1)的用戶(hù)輸入12:00 AM數(shù)據(jù)庫(kù)中會(huì)存儲(chǔ)為10:00 AM UTC,而在紐約(UTC-4) 的用戶(hù)查看時(shí)則為06:00 AM。

補(bǔ)充:請(qǐng)不要懷疑作者的數(shù)學(xué)是不是體育老師教的??,布魯塞爾為東一區(qū),紐約為西五區(qū),但冬令時(shí)和夏令時(shí)讓這個(gè)問(wèn)題變復(fù)雜了。將12:00修改為11:00應(yīng)該就正確了。

??Odoo 服務(wù)日志消息時(shí)間戳使用UTC時(shí)間而非本地服務(wù)器時(shí)間

相反的轉(zhuǎn)換,由會(huì)話(huà)時(shí)區(qū)轉(zhuǎn)換為UTC,也需由客戶(hù)端在將用戶(hù)輸入的datetime傳回服務(wù)器時(shí)完成。日期對(duì)象可進(jìn)行比較和相減來(lái)獲取兩個(gè)日期的時(shí)間差,時(shí)間差是一個(gè)timedelta對(duì)象。timedelta可通過(guò)date運(yùn)算對(duì)date和datetime對(duì)象進(jìn)行加減。這些對(duì)象由 Python 標(biāo)準(zhǔn)庫(kù)datetime模塊提供,以下是使用它進(jìn)行的基本運(yùn)算示例:

 from datetime import date
>>> date.today()
datetime.date(2019, 1, 12)
>>> from datetime import timedelta
>>> timedelta(days=7)
datetime.timedelta(7)
>>> date.today() + timedelta(days=7)
datetime.date(2019, 1, 19)

對(duì)于date, datetime和timedelta數(shù)據(jù)類(lèi)型的完整參考請(qǐng)見(jiàn)Python 官方文檔。Odoo 還在odoo.tools.date_utils模塊中提供了一些額外的便利函數(shù),這些函數(shù)有:

  • start_of(value, granularity)是某個(gè)特定刻度時(shí)間區(qū)間的開(kāi)始時(shí)間,這些刻度有year, quarter, month, week, day或hour
  • end_of(value, granularity)是某個(gè)特定刻度時(shí)間區(qū)間的結(jié)束時(shí)間
  • add(value, kwargs)為指定值加上一個(gè)時(shí)間間隔。kwargs參數(shù)由一個(gè)relativedelta對(duì)象來(lái)定義時(shí)間間隔。這些參數(shù)可以是years, months, weeks, days, hours, minutes等等
  • subtract(value, **kwargs)為指定值減去一個(gè)時(shí)間間隔

relativedelta對(duì)象來(lái)自dateutil庫(kù),可使用months或years執(zhí)行date運(yùn)算(Python的timedelta標(biāo)準(zhǔn)庫(kù)僅支持days)。更多內(nèi)容請(qǐng)見(jiàn)相關(guān)文檔。以下為上述函數(shù)的一些使用示例:

 from odoo.tools import date_utils
>>> from datetime import datetime
>>> date_utils.start_of(datetime.now(), 'week')
datetime.datetime(2019, 1, 7, 0, 0)
>>> date_utils.end_of(datetime.now(), 'week')
datetime.datetime(2019, 1, 13, 23, 59, 59, 999999)
>>> from datetime import date
>>> date_utils.add(date.today(), months=2)
datetime.date(2019, 3, 12)
>>> date_utils.subtract(date.today(), months=2)
datetime.date(2018, 11, 12)

這些工具方法在odoo.fields.Date和the odoo.fields.Datetime對(duì)象中也可使用,如:

  • fields.Date.today()返回服務(wù)器所需格式的當(dāng)前日期,它使用UTC作為一個(gè)引用。這足以計(jì)算默認(rèn)值,這種情況下只需使用函數(shù)名無(wú)需添加括號(hào)。
  • fields.Datetime.now() 返回服務(wù)器所需格式的當(dāng)前datetime,它使用UTC作為一個(gè)引用。這足以計(jì)算默認(rèn)值,
  • fields.Date.context_today(record, timestamp=None)在會(huì)話(huà)上下文中返回帶有當(dāng)前日期的字符串。時(shí)間從記錄上下文中獲取??蛇x項(xiàng)timestamp參數(shù)是一個(gè)datetime對(duì)象,如果傳入將不使用當(dāng)前時(shí)間,而使用傳入值。
  • fields.Datetime.context_timestamp(record, timestamp)將原生的datetime值(無(wú)時(shí)區(qū))轉(zhuǎn)換為具體時(shí)區(qū)的datetime。時(shí)區(qū)從記錄上下文中提取,因此使了前述函數(shù)名。

轉(zhuǎn)換文本形式的日期和時(shí)間

在Odoo 12以前,在進(jìn)行運(yùn)算前我們需要對(duì)文本形式的date和datetime進(jìn)行轉(zhuǎn)換。有些工作可幫助我們完成文本和原生數(shù)據(jù)類(lèi)型的相互轉(zhuǎn)換。這在此前的 Odoo 版本中都非常有用并且在 Odoo 12中也仍然相關(guān):我們要將給到的日期格式化為文本。為便于格式之間的轉(zhuǎn)換,fields.Date和fields.Datetime都提供了如下函數(shù):

  • to_date將字符串轉(zhuǎn)換為date對(duì)象
  • to_datetime(value)將字符串轉(zhuǎn)換為datetime對(duì)象
  • to_string(value)將date或datetime對(duì)象轉(zhuǎn)換為 Odoo 11及之前版本Odoo服務(wù)所需的字符串格式

函數(shù)所需的文本格式由 Odoo 通過(guò)如下方式默認(rèn)預(yù)置:

  • odoo.tools.DEFAULT_SERVER_DATE_FORMAT
  • odoo.tools.DEFAULT_SERVER_DATETIME_FORMAT

它們分別與%Y-%m-%d和%Y-%m-%d %H:%M:%S相對(duì)應(yīng)。from_string用法示例如下:

 from odoo import fields
>>> fields.Datetime.to_datetime('2019-01-12 13:48:50')
datetime.datetime(2019, 1, 12, 13, 48, 50)

對(duì)于其它的日期和時(shí)間格式,可使用datetime對(duì)象中的strptime方法:

 from datetime import datetime
>>> datetime.strptime('1/1/2019', '%d/%m/%Y')
datetime.datetime(2019, 1, 1, 0, 0)

在記錄中寫(xiě)入

有兩種寫(xiě)入記錄的方式:使用對(duì)象形式直接分配和使用write() 方法。第一種很簡(jiǎn)單但一次只能操作一條記錄,效率較低。因?yàn)槊看畏峙涠紙?zhí)行一次寫(xiě)操作,會(huì)產(chǎn)生冗余的重復(fù)計(jì)算。第二種要求寫(xiě)入關(guān)聯(lián)字段時(shí)使用特殊語(yǔ)法,但每條命令可寫(xiě)入多個(gè)字段和記錄,記錄計(jì)算更為高效。

使用對(duì)象形式分配值寫(xiě)入

記錄集實(shí)施活躍記錄模式。也就是說(shuō)我們可以為其分配值,并且會(huì)將這些修改在數(shù)據(jù)庫(kù)中持久化存儲(chǔ)。這是一種操作數(shù)據(jù)的易于理解和便捷的方式,但一次只能操作一個(gè)字段和一條記錄。如:

 root = self.env['res.users'].browse(1)
>>> print(root.name)
OdooBot
>>> root.name = 'Superuser'
>>> print(root.name)
Superuser

雖然使用的是活躍記錄模式,也可以通過(guò)分配記錄值來(lái)設(shè)置關(guān)聯(lián)字段。對(duì)于many-to-one字段,分配的值必須是單條記錄(單例)。對(duì)于to-many字段,也可以通過(guò)一條記錄集分配,來(lái)替換關(guān)聯(lián)記錄列表為新列表(如果有的話(huà)),這里允許任何大小的記錄集。

通過(guò) write()方法寫(xiě)入

我們還可以使用write()方法來(lái)同時(shí)更新多條記錄中的多個(gè)字段,僅需一條數(shù)據(jù)庫(kù)命令。所以在重視效果時(shí)就應(yīng)優(yōu)先考慮這一方式。write() 接收一個(gè)字典來(lái)進(jìn)行字段和值的映射。這會(huì)更新記錄集中的所有記錄并且沒(méi)有返回值,如:

 Partner = self.env['res.partner']
>>> recs = Partner.search( [('name', 'ilike', 'Azure')] )
>>> recs.write({'comment': 'Hello!'})
True

與對(duì)象形式的分配不同,使用write() 方法時(shí)我們不能直接為關(guān)聯(lián)字段分配記錄集對(duì)象。取而代之的是,我們需要使用所需的記錄ID來(lái)從記錄集中進(jìn)行提取。在寫(xiě)入many-to-one字段時(shí),寫(xiě)入的值必須是關(guān)聯(lián)記錄的ID。例如,我們不用self.write({'user_id': self.env.user}),而應(yīng)使用self.write({'user_id': self.env.user.id})。

在寫(xiě)入to-many字段時(shí),寫(xiě)入的值必須使用和 XML 數(shù)據(jù)文件相同的特殊語(yǔ)法,這在第五章 Odoo 12開(kāi)發(fā)之導(dǎo)入、導(dǎo)出以及模塊數(shù)據(jù)中有介紹。比如,我們?cè)O(shè)置圖書(shū)作者列表為author1和author2,這是兩條 Partner 記錄。| 管道運(yùn)算符可拼接記錄來(lái)創(chuàng)建一個(gè)記錄集,因此使用對(duì)象形式的分配可以這么寫(xiě):

publisher.child_ids = author1 | author2

使用write()方法,同樣的操作如下:

book.write( { 'child_ids': [(6, 0, [author1.id, author2.id])] } )

回顧第五章 Odoo 12開(kāi)發(fā)之導(dǎo)入、導(dǎo)出以及模塊數(shù)據(jù)的寫(xiě)入語(yǔ)法,最常用的命令如下:

  • (4, id, _)添加一條記錄
  • (6, _, [ids])替換關(guān)聯(lián)記錄列表為所傳入的列表

寫(xiě)入日期和時(shí)間值

從 Odoo 12開(kāi)始,不論是直接分配還是使用 write()方法,日期和時(shí)間字段都可以 Python 原生數(shù)據(jù)類(lèi)型寫(xiě)入。我們?nèi)钥梢允褂梦谋拘问街祵?xiě)入日期和時(shí)間:

 demo = self.search([('login', '=', 'demo')])
>>> demo.login_date
False
>>> demo.login_date = '2019-01-01 09:00:00'
>>> demo.login_date
datetime.datetime(2019, 1, 1, 9, 0)

創(chuàng)建和刪除記錄

write()方法用于向已有記錄寫(xiě)入日期,但我們還需要?jiǎng)?chuàng)建和刪除記錄。這通過(guò)create()和unlink()模型方法實(shí)現(xiàn)。create()接收所需創(chuàng)建記錄字段和值組成的字典,語(yǔ)法與 write()一致。沒(méi)錯(cuò),默認(rèn)值會(huì)被自動(dòng)應(yīng)用,如下所示:

 Partner = self.env['res.partner']
>>> new = Partner.create({'name': 'ACME', 'is_company': True})
>>> print(new)
res.partner(64,)
>>> print(new.customer) # customer標(biāo)記默認(rèn)為 True
True

unlink()方法會(huì)刪除記錄集中的記錄,如下所示:

 rec = Partner.search([('name', '=', 'ACME')])
>>> rec.unlink()
2019-01-12 06:32:48,601 2612 INFO dev12 odoo.models.unlink: User #1 deleted mail.message records with IDs: [28]
2019-01-12 06:32:48,651 2612 INFO dev12 odoo.models.unlink: User #1 deleted ir.attachment records with IDs: [416, 415, 414]
2019-01-12 06:32:48,655 2612 INFO dev12 odoo.models.unlink: User #1 deleted res.partner records with IDs: [64]
2019-01-12 06:32:48,666 2612 INFO dev12 odoo.models.unlink: User #1 deleted mail.followers records with IDs: [7]
True

以上我們看到日志中幾條其它記錄被刪除的消息,這些是所刪除 partner 關(guān)聯(lián)字段的串聯(lián)刪除。

還有copy()模型方法可用于復(fù)制已有記錄,它接收一個(gè)可選參數(shù)來(lái)在新記錄中修改值,如復(fù)制demo 用戶(hù)創(chuàng)建一個(gè)新用戶(hù):

 demo = self.env.ref('base.user_demo')
>>> new = demo.copy({'name': 'Daniel', 'login': 'daniel', 'email': ''})

帶有copy=False屬性的字段不會(huì)被自動(dòng)拷貝。to-many關(guān)聯(lián)字段帶有該標(biāo)記時(shí)默認(rèn)被禁用,因此也不可拷貝。

重構(gòu)記錄集

記錄集還支持一些其它運(yùn)算。我們可查看一條記錄是否在記錄集中。如果x是一個(gè)單例,并且my_recordset是一個(gè)包含多條記錄的記錄集,可使用如下代碼:

  • x in my_recordset
  • x not in my_recordset

還能使用如下運(yùn)算:

  • recordset.ids 返回記錄集元素的ID列表
  • recordset.ensure_one()檢查是否為單條記錄(單例);若不是,則拋出ValueError異常
  • recordset.filtered(func)返回一個(gè)過(guò)濾了的記錄集,func可以是一個(gè)函數(shù)或一個(gè)點(diǎn)號(hào)分隔的表達(dá)式來(lái)表示字段路徑,可參見(jiàn)下面的示例。
  • recordset.mapped(func)返回一個(gè)映射值列表。除函數(shù)外,還可使用文本字符串作為映射的字段名。
  • recordset.sorted(func)返回一個(gè)排好序的記錄值。除函數(shù)外,文本字符串可用作排序的字段名。reverse=True是其可選參數(shù)。

以下是這些函數(shù)的使用示例:

 rs0 = self.env['res.partner'].search([])
>>> len(rs0)
48
>>> starts_A = lambda r: r.name.startswith('A')
>>> rs1 = rs0.filtered(starts_A)
>>> print(rs1)
res.partner(63, 59, 14, 35)
>>> rs1.sorted(key=lambda r: r.id, reverse=True)
res.partner(63, 59, 35, 14)
>>> rs2 = rs1.filtered('is_company')
>>> print(rs2)
res.partner(14,)
>>> rs2.mapped('name')
['Azure Interior']
>>> rs2.mapped(lambda r: (r.id, r.name))
[(14, 'Azure Interior')]

我們勢(shì)必會(huì)對(duì)這些關(guān)聯(lián)字段中的元素進(jìn)行添加、刪除或替換的操作,那么就帶來(lái)了一個(gè)問(wèn)題:如何操作這些記錄集呢?

記錄集是不可變的,也就是說(shuō)不能直接修改其值。那么修改記錄集就意味著在原有的基礎(chǔ)上創(chuàng)建一個(gè)新的記錄集。一種方式是使用所支持的集合運(yùn)算:

  • rs1 | rs2是一個(gè)集合的并運(yùn)算,會(huì)生成一個(gè)包含兩個(gè)記錄集所有元素的記錄集
  • rs1 + rs2是集合加法運(yùn)算,會(huì)將兩個(gè)記錄集拼接為一個(gè)記錄集,這可能會(huì)帶來(lái)集合中有重復(fù)記錄
  • rs1 & rs2是集合的交集運(yùn)算,會(huì)生成一個(gè)僅在兩個(gè)記錄集中同時(shí)出現(xiàn)元素組成的數(shù)據(jù)集
  • rs1 - rs2是集合的差集運(yùn)算,會(huì)生成在rs1中有但rs2中沒(méi)有的元素組成的數(shù)據(jù)集

還可以使用分片標(biāo)記,例如:

  • rs[0]和rs[-1]分別返回第一個(gè)和最后一個(gè)元素
  • rs[1:]返回除第一元素外的記錄集拷貝。其結(jié)果和rs - rs[0]相同,但保留了排序

??Odoo 10中的修改
從Odoo 10開(kāi)始,記錄集操作保留了排序。此前的 Odoo 版本中,記錄集操作不一定會(huì)保留排序,雖然加運(yùn)算和切片已知是保留排序的。

我們可以用如下運(yùn)算通過(guò)刪除或添加元素來(lái)修改記錄集:

  • self.author_ids |= author1:如果不存在author1,它會(huì)將author1加入記錄集
  • self.author_ids -= author1:如果author1存在于記錄集中,會(huì)進(jìn)行刪除
  • self.author_ids = self.author_ids[:-1]刪除最后一條記錄

關(guān)聯(lián)字段包含記錄集值。many-to-one 可包含單例記錄集,to-many字段包含任意數(shù)量記錄的記錄集。

使用底層 SQL 和數(shù)據(jù)庫(kù)事務(wù)

數(shù)據(jù)庫(kù)引入運(yùn)算在一個(gè)數(shù)據(jù)庫(kù)事務(wù)上下文中執(zhí)行。通常我們無(wú)需擔(dān)心這點(diǎn),因?yàn)榉?wù)器在運(yùn)行模型方法時(shí)會(huì)進(jìn)行處理。但有些情況下,可能需要對(duì)事務(wù)進(jìn)行更精細(xì)控制。這可通過(guò)數(shù)據(jù)庫(kù)游標(biāo)self.env.cr來(lái)實(shí)現(xiàn),如下所示:

  • self.env.cr.commit()執(zhí)行事務(wù)緩沖的寫(xiě)運(yùn)算
  • self.env.cr.rollback()取消上次 commit之后的寫(xiě)運(yùn)算,如果尚未 commit,則回滾所有操作

小貼士:在shell會(huì)話(huà)中,直到執(zhí)行self.env.cr.commit()時(shí)數(shù)據(jù)操作才會(huì)在數(shù)據(jù)庫(kù)中生效

通過(guò)游標(biāo)execute() 方法,我們可以直接在數(shù)據(jù)庫(kù)中運(yùn)行 SQL 語(yǔ)句。它接收一個(gè)要運(yùn)行的SQL 語(yǔ)句,以及第二個(gè)可選參數(shù):一個(gè)用作 SQL 參數(shù)值的元組或列表。這些值會(huì)用在%s占位符之處。

  • ??注意:
    在cr.execute() 中我們不應(yīng)直接編寫(xiě)拼接參數(shù)的SQL查詢(xún)。眾所周知這樣做會(huì)帶來(lái)SQL注入攻擊的安全風(fēng)險(xiǎn)。保持使用%s占位符并通過(guò)第二個(gè)參數(shù)來(lái)傳值。

如果使用SELECT查詢(xún),會(huì)獲取到記錄。fetchall() 函數(shù)以元組列表的形式獲取所有行,dictfetchall()則以字典列表的形式獲取,示例如下:

 self.env.cr.execute("SELECT id, login FROM res_users WHERE login=%s OR id=%s", ('demo',1))
>>> self.env.cr.fetchall()
[(1, '__system__'), (6, 'demo')]
>>> self.env.cr.execute("SELECT id, login FROM res_users WHERE login=%s OR id=%s", ('demo',1))
>>> self.env.cr.dictfetchall()
[{'id': 1, 'login': '__system__'}, {'id': 6, 'login': 'demo'}]

還可以使用數(shù)據(jù)操縱語(yǔ)言(DML) 來(lái)運(yùn)行指令,如UPDATE和INSERT。因?yàn)榉?wù)器保留數(shù)據(jù)緩存,這可能導(dǎo)致與數(shù)據(jù)庫(kù)中實(shí)際數(shù)據(jù)的不一致。出于這個(gè)原因,在使用原生DML后,應(yīng)使用self.env.cache.invalidate()清除緩存。

??注意:
直接在數(shù)據(jù)庫(kù)中執(zhí)行SQL語(yǔ)句可能會(huì)導(dǎo)致數(shù)據(jù)不一致,請(qǐng)僅在確定時(shí)進(jìn)行該操作。

總結(jié)

在本文中,我們學(xué)習(xí)了如何操作模型數(shù)據(jù)以及執(zhí)行 CRUD 運(yùn)算:創(chuàng)建、讀取、更新和刪除數(shù)據(jù)。這是實(shí)現(xiàn)我們的業(yè)務(wù)邏輯和自動(dòng)化的基石。

對(duì)于ORM API的測(cè)試,我們使用了Odoo交互式 shell 命令行。我們通過(guò)self.env環(huán)境運(yùn)行了命令,該環(huán)境可訪問(wèn)模型注冊(cè)表并提供命令運(yùn)行相關(guān)信息的上下文,如當(dāng)前語(yǔ)言 lang 和時(shí)區(qū) tz。

記錄集使用search(<domain>)或browse([<ids>])ORM 方法創(chuàng)建。之后可對(duì)其進(jìn)行遍歷訪問(wèn)每個(gè)單例(一條獨(dú)立的記錄)。我們還可以使用對(duì)象樣式的點(diǎn)號(hào)標(biāo)記在單例中獲取和設(shè)置記錄值。

除直接為單例分配值外,我們還可以使用write(<dict>)來(lái)通過(guò)單條命令更新記錄集中的所有元素。create(), copy()和unlink()命令用于創(chuàng)建、拷貝和刪除記錄。

記錄集可被檢查和操作,檢查運(yùn)算符包含in和not in。重構(gòu)運(yùn)算符包含并集的|,交集的&以及切片:??捎玫霓D(zhuǎn)換包含提取 ID 列表的.ids、.mapped(<field>)、.filtered(<func>) 或.sorted(<func>)。

最后,通過(guò)self.env.cr中暴露的游標(biāo)對(duì)象可控制底層 SQL 運(yùn)行和事務(wù)控制。

在下一篇文章中,我們將為模型添加業(yè)務(wù)邏輯層,實(shí)現(xiàn)通過(guò)ORM API來(lái)自動(dòng)化操作的模型方法。

???第八章 Odoo 12開(kāi)發(fā)之業(yè)務(wù)邏輯 - 業(yè)務(wù)流程的支持

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容