Odoo 開發(fā)手冊連載六 模型 - 結(jié)構(gòu)化應(yīng)用數(shù)據(jù)

本文為最好用的免費ERP系統(tǒng)Odoo 12開發(fā)手冊系列文章第六篇监徘。

在本系列文章第三篇Odoo 12 開發(fā)之創(chuàng)建第一個 Odoo 應(yīng)用中晋修,我們概覽了創(chuàng)建 Odoo 應(yīng)用所需的所有組件。本文及接下來的一篇我們將深入到組成應(yīng)用的每一層:模型層凰盔、視圖層和業(yè)務(wù)邏輯層墓卦。

本文中我們將深入學(xué)習(xí)模型層,以及學(xué)習(xí)如何使用模型來設(shè)計應(yīng)用所需的數(shù)據(jù)結(jié)構(gòu)户敬。我們會探索模型和字段的各項作用落剪,包括定義模型關(guān)系睁本、添加計算字段、創(chuàng)建數(shù)據(jù)約束忠怖。

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

  • 學(xué)習(xí)項目 - 優(yōu)化圖書館應(yīng)用
  • 創(chuàng)建模型
  • 創(chuàng)建字段
  • 模型間的關(guān)系
  • 計算字段
  • 模型約束
  • 了解 Odoo的 base 模型

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

本文代碼基于第三章 Odoo 12 開發(fā)之創(chuàng)建第一個 Odoo 應(yīng)用中所創(chuàng)建的代碼呢堰。相關(guān)代碼參見 GitHub 倉庫,本文學(xué)習(xí)完成項目請參見GitHub 倉庫脑又。相關(guān)代碼需放在一個 addons 路徑中暮胧,然后在 Odoo中安裝了 library_app 模型,本文中例子將會對該模塊修改和新增代碼问麸。

學(xué)習(xí)項目 - 優(yōu)化圖書應(yīng)用

在第三章 Odoo 12 開發(fā)之創(chuàng)建第一個 Odoo 應(yīng)用中往衷,我們創(chuàng)建了一個library_app插件模塊,實現(xiàn)了一個簡單的library.book模型用于展示圖書目錄严卖。本文中席舍,我們將回到該模塊來豐富圖書數(shù)據(jù)。我們將添加一個分類層級哮笆,添加如下用作圖書分類:

  • Name:分類標(biāo)題
  • Parent:所屬父級分類
  • Subcategories:將此作為父級分類的子分類
  • Featured book或author: 此分類中所選圖書或作者

圖書模型中已有一些基本信息字段来颤,我們會添加一些字段來展示 Odoo中的數(shù)據(jù)類型。我們還會為圖書模型添加一些約束:

  • 標(biāo)題和出版日期應(yīng)唯一
  • 輸入的ISBN應(yīng)為有效

創(chuàng)建模型

模型是 Odoo 框架的核心稠肘,它們描述應(yīng)用的數(shù)據(jù)結(jié)構(gòu)福铅,是應(yīng)用服務(wù)和數(shù)據(jù)庫存儲之間的橋梁∠钜酰可圍繞模型實現(xiàn)業(yè)務(wù)邏輯來為應(yīng)用添加功能滑黔,用戶界面也建立在模型之上。下面我們將學(xué)習(xí)模型的通用屬性环揽,用于影響行為略荡,以及幾種模型類型:普通(regular)、臨時(transient)和抽象(abstract)類型歉胶。

模型屬性

模型類可以使用控制其部分行為的額外屬性汛兜,以下是最常用的屬性:

  • _name 是我們創(chuàng)建的 Odoo 模型的內(nèi)部標(biāo)識符,在創(chuàng)建新模型時為必填通今。
  • _description是對用戶友好的模塊記錄標(biāo)題粥谬,在用戶界面中查看模型時顯示”杷可選但推薦添加帝嗡。
  • _order設(shè)置瀏覽模型記錄時或列表視圖的默認排序。其值為 SQL 語句中 order by 使用的字符串璃氢,所以可以傳入符合 SQL 語法的任意值哟玷,它有智能模式并支持可翻譯及many-to-one字段名。

我們的圖書模型中已使用了_name 和_description屬性,可以添加一個_order屬性來默認以圖書名排序巢寡,然后按出版日期倒序排(新出版在前)喉脖。

class Book(models.Model):
    _name = 'library.book'
    _description = 'Book'
    _order = 'name, date_published desc'

在高級用例中還會用到如下屬性:

  • _rec_name在從關(guān)聯(lián)字段(如many-to-one關(guān)聯(lián))中引用時作為記錄描述。默認使用模型中常用的 name字段抑月,但可以指定任意其它字段树叽。
  • _table是模型對應(yīng)的數(shù)據(jù)表名。默認表名由 ORM 通過替換模塊名中的點為下劃線來自動定義谦絮,但是可通過該屬性指定表名题诵。
  • _log_access=False用于設(shè)置不自動創(chuàng)建審計追蹤字段:create_uid, create_date, write_uid和write_date。
  • _auto=False 用于設(shè)置不自動創(chuàng)建模型對應(yīng)的數(shù)據(jù)表层皱。如有需要性锭,可通過重載init()方法來創(chuàng)建數(shù)據(jù)庫對象:數(shù)據(jù)表或視圖。

還有用于繼承模塊的_inherit和_inherits屬性叫胖,在本文后續(xù)會深入學(xué)習(xí)草冈。

模型和 Python 類

Odoo 模型以 Python 類的形式展現(xiàn),在前面的代碼中瓮增,有一個繼承了 models.Model類的 Python 類:Book怎棱,創(chuàng)建了新 Odoo 模型:library.book。Odoo的模型保存在中央注冊表(central registry)中绷跑,可通過 env 環(huán)境對象(老 API 中稱為 pool)獲取拳恋。 它是一個數(shù)據(jù)庫保存所有可用模型類引用的字典,其中的詞條可通過模型名引用 砸捏。具體來說谬运,模型方法中的代碼可使用self.env['library.book']來獲取表示 library.book模型的模型類。

可以看出模型名非常重要带膜,因為它是訪問該注冊表的關(guān)鍵。模型名的規(guī)則是以點號連接的小寫單詞鸳谜,如library.book或library.book.category膝藕。內(nèi)核模塊中的其它示例有project.project, project.task和project.task.type。模型名應(yīng)使用單數(shù)咐扭,如library.book而非library.books芭挽。

??由于歷史原因,有些內(nèi)核模型沒有遵循這一規(guī)則蝗肪,如res.users袜爪。

模型名必須全局唯一,因此第一個單詞應(yīng)使用模塊關(guān)聯(lián)的主應(yīng)用對應(yīng)薛闪,以圖書應(yīng)用而言辛馆,模型名前綴使用 library。其它示例如內(nèi)核模塊的project, crm和sale。另一方面 Python 類僅為所聲明文件本地內(nèi)容昙篙,名稱僅需在代碼文件中唯一即可腊状。因為類名不會與其它模塊中的類產(chǎn)生沖突,也就不需為其添加主應(yīng)用相關(guān)的前綴苔可。

類的命名規(guī)范是使用駝峰命名法(CamelCase)缴挖,這與 Python 標(biāo)準(zhǔn)的 PEP8編碼規(guī)范一致。

臨時(Transient)模型和抽象模型

在前述代碼中以及在大多數(shù)據(jù) Odoo 模型中的類會繼承models.Model類焚辅。這類模型在數(shù)據(jù)庫中持久化存儲:會為模型創(chuàng)建數(shù)據(jù)表并存儲記錄直至刪除映屋。但 Odoo 中還有另外兩種模型類型:臨時模型和抽象模型。

臨時模型繼承models.TransientModel類同蜻,用于向?qū)降挠脩艚换ヅ锏恪_@類數(shù)據(jù)會存儲在數(shù)據(jù)庫中,但僅是臨時性的埃仪。會定時運行清空 job 來清除這些表中的老數(shù)據(jù)乙濒。比如Settings > Translations菜單下的Load a Language對話窗口,就使用了臨時模型來存儲用戶選擇并實現(xiàn)向?qū)н壿嬄羊取T诘诎苏?Odoo 12開發(fā)之業(yè)務(wù)邏輯 - 業(yè)務(wù)流程的支持中會有討論臨時模型的示例颁股。

抽象模型繼承models.AbstractModel類,它不帶有數(shù)據(jù)存儲傻丝。抽象模型用作可復(fù)用的功能集甘有,與使用 Odoo 繼承功能的其它模型配合使用。例如mail.thread是 Discuss 應(yīng)用中的一個抽象模型葡缰,用于為其它模型添加消息和follower 功能亏掀。

檢查已有模型

通過 Python 類創(chuàng)建的模型和字段在用戶界面中有自己的元標(biāo)簽。啟動開發(fā)者模式泛释,訪問菜單Settings > Technical > Database Structure > Models滤愕,這里有數(shù)據(jù)庫中的所有模型。點擊列表中的模型會打開詳情表單:

Odoo 12圖書模型

這是一個檢查模型結(jié)構(gòu)很好的工具怜校,因為在這里可以看到不同模塊所有自定義結(jié)果间影。上圖中在右上角 In Apps字段中可以看到library.book模型的定義來自library_app和library_member兩個模塊。下方區(qū)域中還有幾個包含附加信息的標(biāo)簽:

  • Fields可快速查看模型字段
  • Access Rights是授予不同權(quán)限組的訪問控制規(guī)則
  • Views顯示模型所帶的視圖列表

我們可以通過開發(fā)者菜單下的View Metadata選項查看模型的外部標(biāo)識符茄茁。模型的外部標(biāo)識符或XML ID由 ORM 自動生成魂贬,但根據(jù)規(guī)則可預(yù)知,如library.book模型的外部標(biāo)識符為model_library_book裙顽。在定義安全訪問控制列表經(jīng)常在 CSV 文件中使用到這些XML ID付燥。

小貼士:如第一章 使用開發(fā)者模式快速入門 Odoo 12中所見,模型表單是可編輯的愈犹。通過這里是可以創(chuàng)建并修改模型键科、字段和視圖的。可在此處創(chuàng)建原型然后在插件模塊中實現(xiàn)萝嘁。

創(chuàng)建字段

創(chuàng)建新模型后的第一步是添加字段梆掸。Odoo 支持我們能想到的所有基本數(shù)據(jù)類型,如文本字符串牙言、整型酸钦、浮點型、布爾型咱枉、日期卑硫、日期時間以及圖片或二進制數(shù)據(jù)。下面就來看看 Odoo 中一些可用的字段類型吧蚕断。

基本字段類型

我們將為圖書模型添加幾種可用的字段類型欢伏,編輯library_app/models/library_book.py文件后 Book 類會長這樣:

class Book(models.Model):
...
    # String fields
    name = fields.Char('Title', required=True)
    isbn = fields.Char('ISBN')
    book_type = fields.Selection(
        [('paper', 'Paperback'),
        ('hard', 'Hardcover'),
        ('electronic', 'Electronic'),
        ('other', 'Other')],
        'Type')
    notes = fields.Text('Internal Notes')
    descr = fields.Html('Description')

    # Numeric fields:
    copies = fields.Integer(default=1)
    avg_rating = fields.Float('Average Rating', (3,2))
    price = fields.Monetary('Price', 'currency_id')
    currency_id = fields.Many2one('res.currency') # price helper

    # Date and time fields
    date_published = fields.Date()
    last_borrow_date = fields.Datetime(
        'Last Borrowed On',
        default=lambda self: fields.Datetime.now())

    # Other fields
    active = fields.Boolean('Active?', default=True)
    image = fields.Binary('Cover')

    # Relational Fields
...

此處是 Odoo 中所帶的非關(guān)聯(lián)字段示例,每個字段都帶有所需的位置參數(shù)亿乳。

??Python 中有兩類參數(shù):位置參數(shù)和關(guān)鍵字參數(shù)硝拧。位置參數(shù)需按指定順序使用。例如葛假,f(x, y)應(yīng)以f(1, 2)方式調(diào)用障陶。關(guān)鍵字參數(shù)通過參數(shù)名傳遞。如同一個例子聊训,可使用f(x=1, y=2)甚至是f(1, y=2)兩種傳參方式混用抱究。更多有關(guān)關(guān)鍵字參數(shù)知識參見 Python 官方文檔

對于大多數(shù)非關(guān)聯(lián)字段带斑,第一個參數(shù)是字段標(biāo)題鼓寺,與字符串字段參數(shù)相對應(yīng)。它用作用戶界面標(biāo)簽的默認文本勋磕。這個參數(shù)是可選的妈候,如未傳入,會根據(jù)字段名將下劃線替換為空格并將單詞首字母大寫來自動生成挂滓。以下為可用的非關(guān)聯(lián)字段類型以及其對應(yīng)的位置參數(shù):

  • Char(string)是一個單行文本苦银,唯一位置參數(shù)是string字段標(biāo)簽。
  • Text(string)是一個多行文本杂彭,唯一位置參數(shù)是string字段標(biāo)簽墓毒。
  • Selection(selection, string)是一個下拉選擇列表吓揪。選項位置參數(shù)是一個[('value', 'Title'),]元組列表亲怠。元組第一個元素是存儲在數(shù)據(jù)庫中的值,第二個元素是展示在用戶界面中的描述柠辞。該列表可由其它模塊使用selection_add關(guān)鍵字參數(shù)擴展团秽。
  • Html(string)存儲為文本字段,但有針對用戶界面 HTML 內(nèi)容展示的特殊處理。出于安全考慮习勤,該字段會被清洗踪栋,但清洗行為可被重載。
  • Integer(string)僅需字段標(biāo)題字符串參數(shù)图毕。
  • Float(string, digits)帶有第二個可選參數(shù)digits夷都,該字段是一個指定字段精度的(x,y)元組,x 是數(shù)字總長予颤,y 是小數(shù)位囤官。
  • Monetary(string, currency_field)與浮點字段類似,但帶有貨幣的特殊處理蛤虐。第二個參數(shù)currency_field用于存儲所使用貨幣党饮,默認應(yīng)傳入currency_id字段。
  • Date(string)和Datetime(string)字段只需一個字符串文本位置參數(shù)。
  • Boolean(string)的值為True 或False,可傳入一個字符串文本位置參數(shù)筛欢。
  • Binary(string)存儲文件類二進制文件马靠,只需一個字符串文本位置參數(shù)。它可由Python使用 base64編碼字符串進行處理深胳。

??Odoo 12中的修改
Date和Datetime 字段現(xiàn)在 ORM 中作為日期對象處理。此前的版本中作為文本字符串處理,進行操作時需與 Python 日期對象間進行轉(zhuǎn)換贯城。

文本字符串:Char, Text和Html有一些特有屬性:

  • size (Char)設(shè)置最大允許尺寸。無特殊原因建議不要使用霹娄,例如可用于帶有最大允許長度的社保賬號能犯。
  • translate使用得字段內(nèi)容可翻譯,帶有針對不同語言的不同值犬耻。
  • trim默認值為 True踩晶,啟動在網(wǎng)絡(luò)客戶端中自動去除周圍的空格≌泶牛可通過設(shè)置trim=false來取消渡蜻。

??Odoo 12中的修改
trim字段屬性在 Odoo 12中引入,此前版本中文本字段保存前后的空格计济。

除這些以外茸苇,還有在后面會介紹到的關(guān)聯(lián)字段。不過沦寂, 我們還要先了解下有關(guān)字段屬性的其它知識学密。

常用字段屬性

字段還有一些其它屬性供我們定義其行為。以下是常用的屬性传藏,通常都作為關(guān)鍵字參數(shù):

  • string是字段的默認標(biāo)簽腻暮,在用戶界面中使用彤守。除Selection和關(guān)聯(lián)字段外,它都是第一個位置參數(shù)哭靖,所以大多數(shù)情況下它用作關(guān)鍵字參數(shù)具垫。如未傳入,將由字段名自動生成试幽。
  • default設(shè)置字段默認值筝蚕。可以是具體值(如 active字段中的default=True)铺坞,或是可調(diào)用引用燎含,有名函數(shù)或匿名函數(shù)均可橘忱。
  • help提供 UI 中鼠標(biāo)懸停字段向用戶顯示的提示文本凝颇。
  • readonly=True會使用戶界面中的字段默認不可編輯芦岂。在 API 層面并沒有強制川无,模型方法中的代碼仍然可以向其寫入决左。僅針對用戶界面設(shè)置继找。
  • required=True使得用戶界面中字段默認必填边臼。這通過在數(shù)據(jù)庫層面為列添加NOT NULL 約束來實現(xiàn)。
  • index=True為字段添加數(shù)據(jù)庫索引,讓搜索更快速,但同時也會部分降低寫操作速度。
  • copy=False讓字段在使用 ORM copy()方法復(fù)制字段時忽略該字段。除 to-many 關(guān)聯(lián)字段外,其它字段值默認會被復(fù)制沃呢。
  • groups可限制字段僅對一些組可訪問并可見纸兔。值為逗號分隔的安全組XML ID列表洲拇,如groups='base.group_user,base.group_system'。
  • states傳入依賴 state字段值的 UI 屬性的字典映射值为严。可用屬性有readonly, required和invisible第股,例如states={'done':[('readonly',True)]}应民。

??注意states 字段等價于視圖中的 attrs 屬性。同時注意視圖也支持 states 屬性夕吻,但用途不同诲锹,傳入逗號分隔的狀態(tài)列表來控制元素什么時候可見。

以下為字段屬性關(guān)鍵字參數(shù)的使用示例:

   name = fields.Char(
        'Title',
        default=None,
        index=True,
        help='Book cover title',
        readonly=False,
        required=True,
        translate=False,
    )

如前所述涉馅,default 屬性可帶有固定值归园,或引用函數(shù)來自動計算默認值。對于簡單運算稚矿,可使用 lambda 函數(shù)來避免過重的有名函數(shù)或方法的創(chuàng)建庸诱。以下是一個計算當(dāng)前日期和時間默認值的常用示例:

   last_borrow_date = fields.Datetime(
        'Last Borrowed On',
        default=lambda self: fields.Datetime.now(),
    )

默認值也可以是一個函數(shù)引用,或待定義函數(shù)名字符串:

   last_borrow_date = fields.Datetime(
        'Last Borrowed On',
        default='_default_last_borrow_date',
    )

    def _default_last_borrow_date(self):
        return fields.Datetime.now()

當(dāng)模塊數(shù)據(jù)結(jié)構(gòu)在不同版本中變更時以下兩個屬性非常有用:

  • deprecated=True在字段被使用時記錄一條 warning 日志
  • oldname='field'是在新版本中重命名字段時使用晤揣,可在升級模塊時將老字段中的數(shù)據(jù)自動拷貝到新字段中

特殊字段名

一些字段名很特別桥爽,可能是因為它們出于特殊目的作為 ORM 保留字,或者是由于內(nèi)置功能使用了一些默認字段名昧识。id 字段保留以用作標(biāo)識每條記錄的自增數(shù)字以及數(shù)據(jù)庫主鍵钠四,每個模型都會自動添加。

以下字段只要模型中沒設(shè)置_log_access=False都會在新模型中自動創(chuàng)建:

  • create_uid為創(chuàng)建記錄的用戶
  • create_date是記錄創(chuàng)建的日期和時間
  • write_uid是最后寫入記錄的用戶
  • write_date是最后修改記錄的日期和時間

每條記錄的這些字段信息都可通過開發(fā)者菜單下的View Metadata進行查看跪楞。一些內(nèi)置 API 功能默認需要一些指定字段名缀去。避免在不必要的場合使用這些字段名會讓開發(fā)更輕松侣灶。其中有些字段名被保留并且不能在其它地方使用:

  • name (通常為 Char)默認作為記錄的顯示名稱。通過是一個 Char缕碎,但也可以是 Text 或Many2one字段類型褥影。用作顯示名的字段可修改為_rec_name模型屬性。
  • active (Boolean型)允許我們關(guān)閉記錄阎曹。帶有active=False的記錄會自動從查詢中排除掉∩烽荩可在當(dāng)前上下文中添加{'active_test': False} 來關(guān)閉這一自動過濾处嫌。可用作記錄存檔或假刪除(soft delete)斟湃。
  • state (Selection類型) 表示記錄生命周期的基本狀態(tài)熏迹。它允許使用states字段屬性來根據(jù)記錄狀態(tài)以具備不同的 UI 行為。動態(tài)修改視圖:字段可在特定記錄狀態(tài)下變?yōu)閞eadonly, required或invisible凝赛。
  • parent_id和parent_path Integer和Char型)對于父子層級關(guān)系具有特殊意義注暗。本文后續(xù)會進行討論。

??Odoo 12中的修改
層級關(guān)聯(lián)現(xiàn)在使用parent_path字段墓猎,它替代了老版本中已淘汰的parent_left和 parent_right字段(整型)捆昏。

到目前為止我們討論的都是非關(guān)聯(lián)字段。但應(yīng)用數(shù)據(jù)結(jié)構(gòu)中很大一部分是描述實體間關(guān)聯(lián)的毙沾。下面就一起來學(xué)習(xí)骗卜。

模型間的關(guān)系

中、大型業(yè)務(wù)應(yīng)用有一個結(jié)構(gòu)數(shù)據(jù)模型左胞,需要關(guān)聯(lián)所涉及到的不同實體間的數(shù)據(jù)寇仓。要實現(xiàn)這點,需要使用關(guān)聯(lián)字段烤宙。再來看看我們的圖書應(yīng)用遍烦,圖書模型中有如下關(guān)系:

  • 每本書有一個出版商。這是一個many-to-one 關(guān)聯(lián)躺枕,在數(shù)據(jù)庫引擎中通過外鍵實現(xiàn)服猪。反過來則是one-to-many關(guān)聯(lián),表示一個出版商可出版多本書拐云。
  • 每本書可以有多名作者蔓姚。這是一個many-to-many關(guān)聯(lián),反過來還是many-to-many關(guān)聯(lián)慨丐,因為一個作者也可以有多本書坡脐。

下面我們就會分別討論這些關(guān)聯(lián)。具體的用例就是層級關(guān)聯(lián)房揭,即一個模型中的記錄與同模型中的其它記錄關(guān)聯(lián)备闲。我們將引入一個圖書分類模型解釋這一情況晌端。最后,Odoo 框架還支持彈性關(guān)系恬砂,即一個字段可指向其它表中的字段咧纠,這稱為引用字段。

Many-to-one關(guān)聯(lián)

many-to-one關(guān)聯(lián)是對其它模型中記錄的引用泻骤,例如在圖書模型中漆羔,publisher_id表示圖書出版商,是對partner記錄的一個引用:

   publisher_id = fields.Many2one(
        'res.partner', string='Publisher')

與所有關(guān)聯(lián)字段一樣狱掂,Many2one字段的第一個位置參數(shù)是關(guān)聯(lián)模型(comodel關(guān)鍵字參數(shù))演痒。第二位置參數(shù)是字段標(biāo)簽(string關(guān)鍵字參數(shù)),但這和其它關(guān)聯(lián)字段不同趋惨,所以推薦使用像以上代碼一樣一直使用string關(guān)鍵字參數(shù)鸟顺。

many-to-one模型字段在數(shù)據(jù)表中創(chuàng)建一個字段,并帶有指向關(guān)聯(lián)表的外鍵器虾,其中為關(guān)聯(lián)記錄的數(shù)據(jù)庫 ID讯嫂。以下是many-to-one字段可用的關(guān)鍵字參數(shù):

  • ondelete定義關(guān)聯(lián)記錄刪除時執(zhí)行的操作:
    • set null (默認值): 關(guān)聯(lián)字段刪除時會置為空值
    • restricted:拋出錯誤阻止刪除
    • cascade:在關(guān)聯(lián)記錄刪除時同時刪除當(dāng)前記錄
  • context是一個數(shù)據(jù)字典,可在瀏覽關(guān)聯(lián)時為網(wǎng)頁客戶端傳遞信息兆沙,比如設(shè)置默認值欧芽。第八章 Odoo 12開發(fā)之業(yè)務(wù)邏輯 - 業(yè)務(wù)流程的支持中會做深入說明。
  • domain是一個域表達式:使用一個元組列表過濾記錄來作為關(guān)聯(lián)記錄選項葛圃,第八章 Odoo 12開發(fā)之業(yè)務(wù)邏輯 - 業(yè)務(wù)流程的支持中會詳細說明渐裸。
  • auto_join=True允許ORM在使用關(guān)聯(lián)進行搜索時使用SQL連接。使用時會跳過訪問安全規(guī)則装悲,用戶可以訪問安全規(guī)則不允許其訪問的關(guān)聯(lián)記錄昏鹃,但這樣 SQL 的查詢會更有效率且更快。
  • delegate=True 創(chuàng)建一個關(guān)聯(lián)記錄的代理繼承诀诊。使用時必須設(shè)置required=True和ondelete='cascade'洞渤。代理繼承更多知識參見第四章 Odoo 12 開發(fā)之模塊繼承

One-to-many反向關(guān)聯(lián)

one-to-many關(guān)聯(lián)是many-to-one的反向關(guān)聯(lián)属瓣。它列出引用該記錄的關(guān)聯(lián)模型記錄载迄。比如在圖書模型中,publisher_id與 parnter 模型是一個many-to-one關(guān)聯(lián)抡蛙。這說明partner與圖書模型可以有一個one-to-many的反向關(guān)聯(lián)护昧,列出每個出版商出版的圖書。

要讓關(guān)聯(lián)可用粗截,我們可在 partner 模型中添加它惋耙,在library_app/models/res_partner.py文件中添加如下代碼:

from odoo import fields, models

class Partner(models.Model):
    _inherit = 'res.partner'
    published_book_ids = fields.One2many(
        'library.book', # related model
        'publisher_id', # fields for "this" on related model
        string='Published Books')

我們向模塊添加了新文件,所以不要忘記在library_app/models/init.py中導(dǎo)入該文件:

from . import library_book
from . import res_partner

One2many字段接收三個位置參數(shù):

  • 關(guān)聯(lián)模型 (comodel_name關(guān)鍵字參數(shù))
  • 引用該記錄的模型字段 (inverse_name關(guān)鍵字參數(shù))
  • 字段標(biāo)簽 (string關(guān)鍵字參數(shù))

其它可用的關(guān)鍵字參數(shù)與many-to-one字段相同:context, domain和ondelete(此處作用于關(guān)聯(lián)中的 many 這一方)。

Many-to-many關(guān)聯(lián)

在兩端都存在to-many關(guān)聯(lián)時使用many-to-many關(guān)聯(lián)绽榛。還是以我們的圖書應(yīng)用為例湿酸,書和作者之間是many-to-many關(guān)聯(lián):一本書可以有多個作者,一個作者可以有多本書灭美。圖書端有一個library.book模型:

class Book(models.Model):
    _name = 'library.book'
...
    author_ids = fields.Many2many(
        'res.partner', string='Authors')

在作者端推溃,我們也可以為res.partner添加一個反向關(guān)聯(lián):

class Partner(models.Model):
    _inherit = 'res.partner'
    book_ids = fields.Many2many(
        'library.book', string='Authored Books')

Many2many最少要包含一個關(guān)聯(lián)模型位置參數(shù)(comodel_name關(guān)鍵字參數(shù)),推薦為字段標(biāo)簽提供一個string參數(shù)届腐。

在數(shù)據(jù)庫層面上铁坎,many-to-many關(guān)聯(lián)不會在已有表中添加任何列。而是自動創(chuàng)建一個關(guān)聯(lián)表來存儲記錄間的關(guān)聯(lián)犁苏,該表僅有兩個 ID 字段硬萍,為兩張關(guān)聯(lián)表的外鍵。默認關(guān)聯(lián)表名由兩個表名中間加下劃線并在最后加上_rel 來組成傀顾。我們圖書和作者關(guān)聯(lián)襟铭,表名應(yīng)為library_book_res_partner_rel碌奉。

有時我們可能需要重寫這種自動生成的默認值短曾。一種情況是關(guān)聯(lián)模型名稱過長,導(dǎo)致關(guān)聯(lián)表名的長度超出PostgreSQL數(shù)據(jù)庫63個字符的上限赐劣。這時就需要手動選擇一個關(guān)聯(lián)表名來符合字符數(shù)據(jù)要求嫉拐。另一種情況是我們需要在相同模型間建立第二張many-to-many關(guān)聯(lián)表。這時也需要手動提供一個關(guān)聯(lián)表名來避免與已存在的第一張表名沖突魁兼。

有兩種方案來重寫關(guān)聯(lián)表名:位置參數(shù)或關(guān)鍵字參數(shù)婉徘。通過字段位置參數(shù)定義示例如下:

Authors關(guān)聯(lián)(使用位置參數(shù))
author_ids = fields.Many2many(
    'res.partner', # 關(guān)聯(lián)模型(尾款)
    'library_book_res_partner_rel', # 要使用的關(guān)聯(lián)表名
    'a_id', # 本記錄關(guān)聯(lián)表字段
    'p_id', # 關(guān)聯(lián)記錄關(guān)聯(lián)表字段
    'Authors') # string標(biāo)簽文本

要使可讀性更強,也可使用關(guān)鍵字參數(shù):

Authors關(guān)聯(lián)(使用關(guān)鍵字參數(shù))
author_ids = fields.Many2many(
    comodel_name='res.partner', # 關(guān)聯(lián)模型(必填)
    relation='library_book_res_partner_rel', # 關(guān)聯(lián)表名
    column1='a_id', # 本記錄關(guān)聯(lián)表字段
    column2='p_id', # 關(guān)聯(lián)記錄關(guān)聯(lián)表字段
    string='Authors') # string標(biāo)簽文本

與one-to-many relational字段相似咐汞,many-to-many 字段還可以使用context, domain和auto_join這些關(guān)鍵字參數(shù)盖呼。

??在創(chuàng)建抽象模型時,many-to-many中不要使用column1和column2屬性化撕。在 ORM 設(shè)計中對抽象模型有一個限制几晤,如果指定關(guān)聯(lián)表列名,就無法再被正常繼承植阴。

層級關(guān)聯(lián)

父子樹狀關(guān)聯(lián)使用同一模型中many-to-one關(guān)聯(lián)表示蟹瘾,來將每條記錄引用其父級。反向的one-to-many關(guān)聯(lián)對應(yīng)記錄的子級掠手。Odoo 通過域表達式附加的child_of和parent_of操作符改良了對這些層級數(shù)據(jù)結(jié)構(gòu)的支持憾朴。只要這些模型有parent_id字段(或_parent_name有效模型定義)就可以使用這些操作符。

通過設(shè)置_parent_store=True和添加parent_path幫助字段可加快層級樹的查詢速度喷鸽。該字段存儲用于加速查詢速度的層級樹結(jié)構(gòu)信息众雷。

??Odoo 12中的修改
parent_path幫助字段在 Odoo 12中引入。此前版本中使用parent_left和parent_right整型字段來實現(xiàn)相同功能,但在 Odoo 12中淘汰了這些字段报腔。

注意這些附加操作會帶來存儲和執(zhí)行速度的開銷株搔,所以最好是用到讀的頻率大于寫的情況下,比如本例中的分類樹纯蛾。僅在優(yōu)化多節(jié)點深度層級時才需要使用纤房,對于小層級或淺層級的可能會被誤用。

為演示層級結(jié)構(gòu)翻诉,我們將為圖書應(yīng)用添加一個分類樹炮姨,用于為圖書分類。在library_app/models/library_book_category.py文件中添加如下代碼:

from odoo import api, fields, models

class BookCategory(models.Model):
    _name = 'library.book.category'
    _description = 'Book Category'
    _parent_store = True

    name = fields.Char(translate=True, required=True)
    # Hierarchy fields
    parent_id = fields.Many2one(
        'library.book.category',
        'Parent Category',
        ondelete='restrict')
    parent_path = fields.Char(index=True)

    # Optional but good to have:
    child_ids = fields.One2many(
        'library.book.category',
        'parent_id',
        'Subcategories')

這里定義了一個基本模型碰煌,包含引用父級記錄的parent_id字段舒岸。為啟用層級索引來加快樹級搜索,添加了一個_parent_store=True 模型屬性芦圾。使用該屬性必須要添加且必須要索引parent_path字段蛾派。引用父級的字段名應(yīng)為parent_id,但如果聲明了可選的_parent_name模型屬性个少,則可以使用任意其它字段名洪乍。

添加字段列出直接的子級非常方便,即為上述代碼中的one-to-many反向關(guān)聯(lián)夜焦。還有不要忘記在library_app/models/init.py文件中添加對以上代碼的引用:

from . import library_book
from . import res_partner
from . import library_book_category

使用引用字段的彈性關(guān)聯(lián)

普通關(guān)聯(lián)字段指定固定的引用co-model模型壳澳,但Reference字段類型不受這一限制,它支持彈性關(guān)聯(lián)茫经,因此相同字段不用限制只指向相同的目標(biāo)模型巷波。作為示例,我們使用圖書分類模型來添加引用重點圖書或作者卸伞。因此該字段可引用圖書或 partner:

class BookCategory(models.Model):
...   
    highlighted_id = fields.Reference(
        [('library.book', 'Book'), ('res.partner', 'Author')],
        'Category Highlight'
    )

該字段定義與 selection 字段相似抹镊,但這里選擇項為該字段中可以使用的模型。在用戶界面中荤傲,用戶會先選擇列表中的模型垮耳,然后選擇模型中的指定記錄。

??Odoo 12中的修改
刪除了可引用模型配置表弃酌。在此前版本中氨菇,可用于配置在 Reference 字段中可用的模型。通過菜單Settings > Technical > Database Structure可進行查看妓湘。這些配置可在 Reference 字段中使用odoo.addons.res.res_request.referenceable_models函數(shù)來替代模型選擇列表查蓉。

以下為有關(guān)引用字段的一些其它有用技術(shù)細節(jié):

  • 引用字段在數(shù)據(jù)庫中以model,id字符串形式存儲
  • read()方法供外部應(yīng)用使用,以格式化的('model_name', id)元組返回榜贴,而不是常用的many-to-one字段的(id, 'display_name')形式

計算字段

字段值除普通的讀取數(shù)據(jù)庫中存儲值外豌研,還可自動由函數(shù)計算妹田。計算字段的聲明和普通字段相似,但有一個額外的compute參數(shù)來定義用于計算的函數(shù)鹃共。大多數(shù)情況下鬼佣,計算字段包含書寫業(yè)務(wù)邏輯。因此要完全使用這一功能霜浴,還要學(xué)習(xí)第八章 Odoo 12開發(fā)之業(yè)務(wù)邏輯 - 業(yè)務(wù)流程的支持晶衷。此處我們將解釋計算字段用法,但會使用簡單的業(yè)務(wù)邏輯阴孟。

圖書有出版商晌纫,我們的例子是在圖書表單中添加出版商的國別。實現(xiàn)該功能永丝,我們會使用基于publisher_id的計算字段锹漱,將會從出版商的country_id字段中獲取值。

編輯library_app/models/library_book.py文件中的圖書模型慕嚷,代碼如下:

class Book(models.Model):
...
    publisher_country_id = fields.Many2one(
        'res.country', string='Publisher Country',
        compute='_compute_publisher_country'
    )

    @api.depends('publisher_id.country_id')
    def _compute_publisher_country(self):
        for book in self:
            book.publisher_country_id = book.publisher_id.country_id

以上代碼添加了一個publisher_country_id字段哥牍,和一個計算其值的_compute_publisher_country方法。方法名作為字符串參數(shù)傳入字段中喝检,但也可以傳遞一個可調(diào)用引用(方法標(biāo)識符嗅辣,不帶引號)。但這時需確定Python 文件中方法在字段之前定義蛇耀。

計算如果依賴其它字段的話就需要使用@api.depends裝飾器辩诞,通常都會依賴其它字段坎弯。它告訴服務(wù)器何時重新計算或緩存值纺涤。參數(shù)可接受一個或多個字段名,點號標(biāo)記可用于了解字段關(guān)聯(lián)抠忘。本例中撩炊,只要圖書publisher_id的country_id變更了就會重新進行計算。

和平常一樣崎脉,self 參數(shù)是要操作的字符集對象拧咳。我們需要對其遍歷來作用于每條記錄。計算值通過常用(寫)操作來設(shè)置囚灼,本例中計算相當(dāng)簡單骆膝,我們?yōu)槠浞峙洚?dāng)前圖書的publisher_id.country_id值。

同樣的計算方法可用于一個以上字段灶体。這時同一方法在多個compute 字段參數(shù)中使用阅签,計算方法將為所有計算字段分配值。

小貼士:計算函數(shù)必須為一個或多個字段分配值用于計算蝎抽。如果計算方法有 if 條件分支政钟,確保每個分支中為計算字段分配了值。否則在未分配置值的分支中將會報錯。

現(xiàn)在我們還不會修改該模塊的視圖养交,但可通過在圖書表單視圖中點擊開發(fā)者菜單中的Edit View選項精算,直接在表單 XML 中添加該字段查看效果。不必擔(dān)心出問題碎连,在下次模塊升級時會進行覆蓋灰羽。

Odoo 12圖書項目出版商國家

搜索和寫入計算字段

我們剛剛創(chuàng)建的計算字段可讀取但不可搜索或?qū)懭搿DJ情況下計算字段是實時計算鱼辙,而不存儲在數(shù)據(jù)庫中谦趣。這也是無法像普通字段那樣進行搜索的原因。

我們可通過實現(xiàn)特殊方法來開啟搜索和寫入操作座每。計算字段可與 compute 方法一起設(shè)置實現(xiàn)搜索邏輯的 search 方法前鹅,以及實現(xiàn)寫入邏輯的 inverse 方法。使用這些方法峭梳,計算字段可修改如下:

class Book(models.Model):
...
    publisher_country_id = fields.Many2one(
        'res.country', string='Publisher Country',
        compute='_compute_publisher_country'舰绘,
        # store = False, # 默認不在數(shù)據(jù)庫中存儲
        inverse='_inverse_publisher_country',
        search='_search_publisher_country',
    )

計算字段中的寫入是計算的反向(inverse)邏輯。因此處理寫入操作的方法稱為 inverse葱椭,本例中 inverse 方法很簡單捂寿。計算將book.publisher_id.country_id 的值復(fù)制給book.publisher_country_id,反向操作是將寫入book.publisher_country_id的值拷貝給book.publisher_id.country_id field字段:

   def _inverse_publisher_country(self):
        for book in self:
            book.publisher_id.country_id = book.publisher_country_id

注意這會修改出版商partner記錄數(shù)據(jù)孵运,因此也會修改相同出版商圖書的相關(guān)字段秦陋。常規(guī)權(quán)限控制對這類寫操作有效,因此僅有對 partner 模型有寫權(quán)限的當(dāng)前用戶才能成功執(zhí)行操作治笨。

要為計算字段開啟搜索操作驳概,需要實現(xiàn)search 方法。為此我們需要能夠?qū)⒂嬎阕侄蔚乃阉鬓D(zhuǎn)換為使用常規(guī)存儲字段的搜索域旷赖。本例中顺又,實際的搜索可通過關(guān)聯(lián)的publisher_id Partner 記錄的country_id來實現(xiàn):

   def _search_publisher_country(self, opearator, value):
        return [('publisher_id.country_id', operator, value)]

在模型上執(zhí)行搜索時,域表達式用作實施過濾的參數(shù)等孵。域表達式在第八章 Odoo 12開發(fā)之業(yè)務(wù)邏輯 - 業(yè)務(wù)流程的支持會做詳細講解稚照,現(xiàn)在我們應(yīng)了解它是一系列(field, operator, value)條件。

當(dāng)域表達式的條件中出現(xiàn)該計算字段時就會調(diào)用這個搜索方法俯萌。它接收搜索的操作符和值果录,并將原搜索元素轉(zhuǎn)換為一個域搜索表達式。country_id字段存儲在關(guān)聯(lián)的partner模型中咐熙,因此我們的搜索實現(xiàn)僅需修改原搜索表達式來使用publisher_id.country_id字段弱恒。

存儲計算字段

通過在定義時設(shè)置store = True還可以將計算字段值保存到數(shù)據(jù)庫中。在任意依賴變更時值就會重新計算糖声。因為值已被存儲斤彼,所以可以像普通字段一樣被搜索分瘦,也就不需要使用 search 方法了。

關(guān)聯(lián)字段

前面我們實現(xiàn)的計算字段僅僅是從關(guān)聯(lián)記錄中將值拷貝到模型自己的字段中琉苇。這種常用情況可以由 Odoo 使用關(guān)聯(lián)字段功能自動處理嘲玫。關(guān)聯(lián)字段通過關(guān)聯(lián)模型的字段可在模型中直接可用,并且可通過點號標(biāo)記法直接訪問并扇。這樣在點號標(biāo)記法不可用時(如 UI 表單視圖)也可以使用該字段去团。

要創(chuàng)建關(guān)聯(lián)字段,我們像普通計算字段那樣聲明一個所需類型的字段穷蛹,但使用的不是 compute 屬性土陪,而是 related屬性,設(shè)置用點號標(biāo)記鏈來使用所需字段肴熏。我們可以使用引用字段來獲取與上例publisher_country_id計算字段相同的效果:

   publisher_country_id = fields.Many2one(
        'res.country', string='Publisher Country',
        related='publisher_id.country_id',
    )

本質(zhì)上關(guān)聯(lián)字段僅僅是快捷實現(xiàn) search 和 inverse 方法的計算字段鬼雀。也就是說可以直接對其進行搜索和寫入,而無需書寫額外的代碼蛙吏。默認關(guān)聯(lián)字段是只讀的源哩,因inverse寫操作不可用,可通過readonly=False字段屬性來開啟寫操作鸦做。

??Odoo 12中的修改
現(xiàn)在關(guān)聯(lián)字段默認為只讀:readonly=True励烦。此前版本中它默認可寫,但事實證明這是一個默認值泼诱,因為它可能會允許修改配置或主數(shù)據(jù)這些不應(yīng)被修改的數(shù)據(jù)坛掠。

還應(yīng)指出這些關(guān)聯(lián)字段和計算字段一樣可使用store=True來在數(shù)據(jù)庫中存儲。

模型約束

通常應(yīng)用需保證數(shù)據(jù)完整性治筒,并執(zhí)行一些驗證來保證數(shù)據(jù)是完整和正確的屉栓。PostgreSQL數(shù)據(jù)庫管理器支持很多可用驗證:如避免重復(fù),或檢查值以符合某些簡單條件矢炼。模型為此可聲明并使用 PostgreSQL約束系瓢。一些檢查要求更復(fù)雜的邏輯阿纤,最好是使用 Python 代碼來實現(xiàn)句灌。對這些情況,我們可使用特定的模型方法來實現(xiàn) Python 約束邏輯欠拾。

SQL模型約束

SQL約束加在數(shù)據(jù)表定義中胰锌,并由PostgreSQL直接執(zhí)行。它由_sql_constraints類屬性來定義藐窄。這是一個元組組成的列表资昧,并且每個元組的格式為(name, code, error):

  • name是約束標(biāo)識名
  • code是約束的PostgreSQL語法
  • error是在約束驗證未通過時向用戶顯示的錯誤消息

我們將向圖書模型添加兩個SQL約束。一條是唯一性約束荆忍,用于通過標(biāo)題和出版日期是否相同來確保沒有重復(fù)的圖書格带;另一條是檢查出版日期是否為未出版:

class Book(models.Model):
...
    _sql_constraints = [
        ('library_book_name_date_uq', # 約束唯一標(biāo)識符
        'UNIQUE (name, date_published)', # 約束 SQL 語法
        'Book title and publication date must be unique'), # 消息
        ('library_book_check_date',
        'CHECK (date_published <= current_date)',
        'Publication date must not be in the future.'),
    ]

更多有關(guān)PostgreSQL約束語法撤缴,請參見官方文檔

Python模型約束

Python 約束可使用自定義代碼來檢查條件叽唱。檢查方法應(yīng)添加@api.constrains裝飾器屈呕,并且包含要檢查的字段列表,其中任意字段被修改就會觸發(fā)驗證棺亭,并且在未滿足條件時拋出異常虎眨。就圖書應(yīng)用來說,一個明顯的示例就是防止插入不正確的 ISBN 號镶摘。我們已經(jīng)在_check_isbn()方法中書寫了 ISBN 的校驗邏輯嗽桩。可以在模型約束中使用它來防止保存錯誤數(shù)據(jù):

from odoo.exceptions import ValidationError

class Book(models.Model):
...
    @api.constrains('isbn')
    def _constrain_isbn_valid(self):
        for book in self:
            if book.isbn and not book._check_isbn():
                raise ValidationError('%s is an invalid ISBN' % book.isbn)

了解 Odoo的 base 模型

在前面文章中凄敢,我們一起創(chuàng)建了新模型碌冶,如圖書模型,但也使用了已有的模型涝缝,如 Odoo 自帶的Partner 模型种樱。下面就來介紹下這些內(nèi)置模型。Odoo 內(nèi)核中有一個base插件模塊俊卤。它提供了 Odoo 應(yīng)用所需的基本功能嫩挤。然后有一組內(nèi)置插件模塊來提供標(biāo)準(zhǔn)產(chǎn)品中的官方應(yīng)用和功能。base模塊中包含兩類模型:

  • 信息倉庫(Information Repository)消恍, ir.*模型
  • 資源(Resources)岂昭, res.*模型

信息倉庫用于存儲 Odoo 所需數(shù)據(jù),以知道如何作為應(yīng)用來運作狠怨,如菜單约啊、視圖、模型佣赖、Action 等等恰矩。Technical菜單下的數(shù)據(jù)通常都存儲在信息倉庫中。相關(guān)的例子有:

  • ir.actions.act_window用于窗口操作
  • ir.ui.menu用于菜單項
  • ir.ui.view用于視圖
  • ir.model用于模型
  • ir.model.fields用于模型字段
  • ir.model.data用于XML ID

資源包含基本數(shù)據(jù)憎蛤,基本上用于應(yīng)用外傅。以下是一些重要的資源模型:

  • res.partner用于業(yè)務(wù)伙伴个唧,如客戶岛心、供應(yīng)商和地址等等
  • res.company用于公司數(shù)據(jù)
  • res.currency用于貨幣
  • res.country用于國家
  • res.users用于應(yīng)用用戶
  • res.groups用于應(yīng)用安全組

這些應(yīng)該有助于你在未來遇到這些模型時理解它們來自何處。

總結(jié)

學(xué)習(xí)完本文妆丘,我們熟悉了模型帶給我們構(gòu)造數(shù)據(jù)模型的可能性棚辽。我們看到模型通常繼承models.Model類技竟,但還可使用models.Abstract來創(chuàng)建可復(fù)用的 mixin 模型、使用models.Transient來創(chuàng)建向?qū)Щ蚋呒売脩魧υ捛辍N覀冞€學(xué)習(xí)了常見的模型屬性榔组,如_order 用于排序熙尉,_rec_name用于記錄展示的默認值。

模型中的字段定義了所有它存儲的數(shù)據(jù)搓扯。我們了解了可用的非關(guān)聯(lián)字段類型以及它們支持的屬性骡尽。我們還學(xué)習(xí)了關(guān)聯(lián)字段的幾種類型:many-to-one, one-to-many和many-to-many,以及它們?nèi)绾味x模型間的關(guān)系擅编,包括層級父子關(guān)系攀细。

大多數(shù)字段在數(shù)據(jù)庫中存儲用戶的輸入,但字段也可以通過 Python 代碼自動計算值爱态。我們看到了如何實現(xiàn)計算字段谭贪,以及一些高級用法,如使計算字段可寫及可搜索锦担。

還有模型定義的一部分是約束俭识,保持?jǐn)?shù)據(jù)一致性和執(zhí)行驗證,可以通過PostgreSQL或Python代碼實現(xiàn)洞渔。

一旦我們創(chuàng)建了數(shù)據(jù)模型套媚,就應(yīng)該為它提供一些默認和演示數(shù)據(jù)。在下一篇文章中我們將學(xué)習(xí)如何使用數(shù)據(jù)文件在系統(tǒng)中導(dǎo)入磁椒、導(dǎo)出和加載數(shù)據(jù)堤瘤。

???第七章 Odoo 12開發(fā)之記錄集 - 使用模型數(shù)據(jù)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市浆熔,隨后出現(xiàn)的幾起案子本辐,更是在濱河造成了極大的恐慌,老刑警劉巖医增,帶你破解...
    沈念sama閱讀 221,198評論 6 514
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件慎皱,死亡現(xiàn)場離奇詭異,居然都是意外死亡叶骨,警方通過查閱死者的電腦和手機茫多,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,334評論 3 398
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來忽刽,“玉大人天揖,你說我怎么就攤上這事〉蘅遥” “怎么了宝剖?”我有些...
    開封第一講書人閱讀 167,643評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長歉甚。 經(jīng)常有香客問我,道長扑眉,這世上最難降的妖魔是什么纸泄? 我笑而不...
    開封第一講書人閱讀 59,495評論 1 296
  • 正文 為了忘掉前任赖钞,我火速辦了婚禮,結(jié)果婚禮上聘裁,老公的妹妹穿的比我還像新娘雪营。我一直安慰自己,他們只是感情好衡便,可當(dāng)我...
    茶點故事閱讀 68,502評論 6 397
  • 文/花漫 我一把揭開白布献起。 她就那樣靜靜地躺著,像睡著了一般镣陕。 火紅的嫁衣襯著肌膚如雪谴餐。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,156評論 1 308
  • 那天呆抑,我揣著相機與錄音岂嗓,去河邊找鬼。 笑死鹊碍,一個胖子當(dāng)著我的面吹牛厌殉,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播侈咕,決...
    沈念sama閱讀 40,743評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼公罕,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了耀销?” 一聲冷哼從身側(cè)響起熏兄,我...
    開封第一講書人閱讀 39,659評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎树姨,沒想到半個月后摩桶,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,200評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡帽揪,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,282評論 3 340
  • 正文 我和宋清朗相戀三年硝清,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片转晰。...
    茶點故事閱讀 40,424評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡芦拿,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出查邢,到底是詐尸還是另有隱情蔗崎,我是刑警寧澤,帶...
    沈念sama閱讀 36,107評論 5 349
  • 正文 年R本政府宣布扰藕,位于F島的核電站缓苛,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏邓深。R本人自食惡果不足惜未桥,卻給世界環(huán)境...
    茶點故事閱讀 41,789評論 3 333
  • 文/蒙蒙 一笔刹、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧冬耿,春花似錦舌菜、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,264評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至缤骨,卻和暖如春爱咬,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背荷憋。 一陣腳步聲響...
    開封第一講書人閱讀 33,390評論 1 271
  • 我被黑心中介騙來泰國打工台颠, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人勒庄。 一個月前我還...
    沈念sama閱讀 48,798評論 3 376
  • 正文 我出身青樓串前,卻偏偏與公主長得像,于是被迫代替她去往敵國和親实蔽。 傳聞我的和親對象是個殘疾皇子荡碾,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,435評論 2 359