Odoo 11 Backend

Table of Contentshtml

命令入口node

服務器python

啓動serverweb

thread 模式sql

prefork 模式shell

gevent模式數據庫

wsgi 應用json

響應 客戶端請求api

xmlrpc服務器

web

http路由處理

HTTP request

JSON request

路由註冊

註冊表

註冊表建立或更新

模塊

模塊信息

遷移 鉤子 migration hook

模塊依賴關係 圖

模塊安裝/升級/卸載

模塊登記

模型初始化

模塊數據加載

記錄集

數據 讀寫

約束

SQL 約束

Python 約束

默認值

視圖和字段

權限

視圖 之 權限處理

服務

XML RPC 服務

Web服務

   

   

命令入口

odoo-bin 腳本

支持的子命令

help

deploy

scaffold

server

shell

start

 

子命令註冊

   

command 基類, 往 commands 註冊 支持的子命令, 等定義 接口 run()…. 每一個子命令必須實現它

   

   

服務器

   

運行 子命令 server ,

   

檢查運行用戶、pg用戶、讀取配置、顯示主要配置

   

   

啓動server

   

   

加載 全局 addons… load_server_wide_modules() #L864 service\server.py

此時 initialize_sys_path() , 並 import odoo模塊, 名稱空間爲 'odoo.addons.'+module_name

# 默認全局加載的模塊爲 web

   

根據 配置 運行 具體模式server 的 run(preload, stop) 方法 ; 支持如下集中模式

  1. 多線程 --> ThreadedServer
  2. 多進程 --> PreforkServer
  3. gevent --> GeventServer

   

運行 server 時, 根據配置 肯定server ,而後將 wsgi應用 傳遞給 它, 也就是將 self.app 指定爲 wsgi 應用 odoo.service.wsgi_server.application

, 以便在啓動http server後,以此wsgi 應用來響應請求

   

thread 模式

對於 Thread 模式, 經過 run 入口 調用 http_spawn() 以線程模式啓動 wsgi服務器

   

以 werkzeug.serving.ThreadedWSGIServer 模式 啓動 wsgi application

   

prefork 模式

對於 Prefork 模式, 經過 run 入口 調用 worker_spawn() 依次調用對應的 worker,

http worker --> WorkerHTTP

cron worker --> WorkerCron

long_polling 進程

   

在 worker_spawn 初始化 對應的 worker 後 調用 run() 啓動它

   

對於 http worker ,,, 則 調用WorkerHTTP,self.workers_http), 啓動 BaseWSGIServerNoBind(self.multi.app)

使用 werkzeug 啓動 wsgi 服務器werkzeug.serving.BaseWSGIServer.__init__(self,"127.0.0.1",0,app)

   

gevent模式

對於 gevent 模式, 以 gevent.wsgi 方式啓動 服務器

   

說明: gevent模式專用於 longpooling

   

wsgi 應用

封裝了2個 wsgi 處理器

  1. xmlrpc
  2. http root # 也就是在 http.py 裏面定義的 class Root

 

   

響應客戶端請求

當對 http server發起請求時, http server將請求轉發給 wsgi應用

   

當wsgi 接收到 請求時, 嘗試調用全部支持的 wsgi 處理器[handler]對請求進行處理,

調用的前後順序爲

wsgi_xmlrpc(environ,start_response)

odoo.http.root(environ,start_response)

   

若是handler處理不了,則返回 "No handler found." 錯誤

   

xmlrpc

調用 xmlrpc 處理器 #L102 wsgi_xmlrpc wsgi_server.py

驗證xmlrpc 調用請求,取出 xmlrpc 調用的服務,方法,以及參數, 傳遞給 odoo.http.dispatch_rpc(service,method,params),返回 最終 dispath() 方法

各個服務將調用相應的模塊的dispatch() 方法來 分發 遠程調用

  1. common --> odoo.service.common
  2. db --> odoo.service.db
  3. object --> odoo.service.model

每一個服務都實現了 dispatch 方法

   

   

web

odoo web 將 http請求分爲2種

  1. jsonrequest
  2. httprequest

它們都繼承webreqeust

   

http.root # odoo/http.py#L1294

http Root() 初始初始化,並 將請求經過 dispatch() 進行分發

先判斷是否 第一次加載 addons, 如是, 則 加載 模塊 ,並使用 靜態文件 處理器的 dispatch 方法 分發請求

若是不是 第一次加載 addons, 則調用 root 的 dispatch() 方法 分發請求

   

加載 模塊,目的是 加載全部包含了 靜態文件 和 controller的 odoo addons

對於靜態文件的addons, 則將 使用 disablecachemiddleware 對 wsgi 應用 進行 處理

   

對於其餘的,則使用 root 的 dispatch 方法, 根據 請求的不一樣情形 進行分發

   

可能的情形

  • http請求不包含 db 時, 分發到建立數據庫
  • http請求包含 db 時, 檢查註冊表信號【若是註冊表尚未 ready, 則先 準備好註冊表】
    • 若是在檢查註冊表信號,或者調取ir_http 出現異常, 則 分發到建立數據庫 或則 選擇數據庫
    • 而後經過註冊表獲取 ir_http 對象,將請求交給 ir_http dispatch 進行路由選擇, 而後交給相應的 http Endpoint 進行處理

   

同時,在作實際的dispatch() 以前,先對 web request 進行識別, 判斷究竟是 jsonrequest 仍是 httpreqeust

   

根據請求數據 判別 request 類型, 而後用 對應的 request 方式進行數據處理

 

http路由處理

ir_http dispatch 經過 _find_handler() 調用 ir.http 類方法 routing_map()獲取 路由表 # L227 ir_http.py

   

根據已經 安裝的 模塊, 經由 http.routing_map() 獲得 route map.

例如,

 

根據 route_map 選擇 對應的 endpoint 處理 web 請求

   

調用 web 請求的 dispatch() 方法 對請求進行處理, 而相應的 request 最終會 調用

父類 Request 方法 _call_function() 調用 endpoint 處理 request…

   

HTTP request

若是是 http 類型, 調用 HttpRequest.dispatch() 處理

   

使用 對應的 endpoint 處理 reqeust.. 並返回結果

   

JSON request

若是是 http 類型, 調用 JsonRequest.dispatch() 處理

   

對應的endpoint 處理 請求, 返回結果 經 _json_response 處理爲 jsonrpc 返回數據規範

   

路由註冊

在加載odoo addons的時候,若是是controller 會往 controllers_per_module{} 註冊 控制器類, 註冊內容是

{ 模塊:[ (模塊名.類名, 類)] }

   

例如

生成路由表時, 從 註冊表讀出已安裝的模塊, 而後從上面數據讀出控制器類,並讀出 方法的 routing 屬性

   

routing屬性,是在往 控制器方法修飾 route 時, 注入進去的

   

註冊表

   

registry , 每一個數據庫 一個 註冊表, 在 每一個 odoo實例 的 registry 對象 的 registries 屬性記錄 所有的 註冊表

   

主要 方法

load()

加載 模型, 構建 model class

setup_models()

設置 base , 設置 字段, 設置 計算字段

init_models()

初始模型,調用 model 的 auto_init() 和 init( ) 操做數據庫, 創建 數據庫表 , 增長字段 字段 , 增長 約束 /// 在此 實現 MPTT 【 預排序遍歷樹 】 // 提示, 能夠在model 定製 init() 改變 數據庫初始化邏輯

   

   

註冊表建立或更新

wsgi 應用 Dispatch 請求時, dispatch 邏輯裏,在檢查註冊表時, 先嚐試 獲取 註冊表, 而後檢查 "信號" # odoo/odoo/http.py:1445

   

註冊表獲取 # odoo/odoo/__init__.py:76

   

   

根據db 建立 註冊表, 加載 模塊 # odoo/odoo/modules/registry.py:61

   

加載 模塊 # odoo/odoo/modules/registry.py:85

   

模塊

 

模塊信息

load_information_from_description_file()

   

   

   

   

遷移鉤子 migration hook

   

在 安裝/升級 模塊時, 執行 migrations

   

Migrations 定義:

   

This class manage the migration of modules

Migrations files must be python files containing a `migrate(cr, installed_version)`

function. Theses files must respect a directory tree structure: A 'migrations' folder

which containt a folder by version. Version can be 'module' version or 'server.module'

version (in this case, the files will only be processed by this version of the server).

Python file names must start by `pre` or `post` and will be executed, respectively,

before and after the module initialisation. `end` scripts are run after all modules have

been updated.

Example:

<moduledir>

`-- migrations === 目錄名必須

|-- 1.0 === 版本號, odoo服務版本號,或者模塊版本號

| |-- pre-update_table_x.py === 升級前執行腳本

| |-- pre-update_table_y.py

| |-- post-create_plop_records.py === 升級後執行腳本

| |-- end-cleanup.py === 最終執行腳本

| `-- README.txt # not processed

|-- 9.0.1.1 # processed only on a 9.0 server

| |-- pre-delete_table_z.py

| `-- post-clean-data.py

`-- foo.py # not processed

   

   

當 遷移腳本的 版本 處於 已安裝的版本, 和當前版本直接時, 才 會執行

if parsed_installed_version < parse_version(convert_version(version)) <= current_version:

   

   

模塊依賴關係圖

   

   

   

   

模塊安裝/升級/卸載

   

代碼 odoo/odoo/modules/loading.py

   

背景知識點: python 環境 sys.modules

   

  1. 調用 initialize_sys_path() 引入 odoo 模塊, 加入到 sys.modules
  2. 若是數據庫還沒創建,初始化數據庫, 對應的SQL 文件 odoo/odoo/addons/base/base.sql
  3. 獲取 註冊表
  4. 初始化 模塊依賴關係圖
  5. 按 模塊依賴關係圖, 運行如下邏輯 load_module_graph()
    1. 運行預遷移腳本
    2. 加載 odoo模塊,若是 模塊指定了 post_load 運行它 # load_openerp_module()
    3. 對於新安裝模塊, 運行模塊指定的 pre_init_hook
    4. 往註冊表加載 模塊
    5. 對於新安裝/升級的模塊, 經過註冊表 設置模型 setup_models(), 初始化模型 init_models()
    6. 對於新安裝/升級的模塊, 加載 數據 以及 演示數據
    7. 運行遷移後腳本
    8. 若是在config 設置了overwrite_existing_translations,則更新翻譯,
    9. 驗證 視圖
    10. 對於新安裝模塊, 運行模塊指定的 post_init_hook
  6. 計算 依賴模塊, 再次 按 模塊依賴關係圖 運行 模塊安裝/升級 邏輯
  7. 運行最終遷移腳本
  8. 完成安裝並清理
  9. 若是是 卸載模塊, 執行 卸載,並重置 註冊表 // 卸載時,從數據庫表 ir_model_data 刪除相關數據, 將模塊標記爲 uninstalled
  10. 驗證 自定義視圖
  11. 運行 模型註冊鉤子 _register_hook()

   

   

load_openerp_module 處理 odoo 名稱 空間

   

odoo.addons.[addons_name].models.[model_name]

   

別名

openerp.addons. *

   

   

# 注意

此外,在引入 odoo模塊的時候,經過 MetaModel 將 addons 登記 module_to_models, 以便 註冊表 在 load 模型時, 構建 model.

   

   

模塊登記

   

在 模塊加載 邏輯的 第二步, 更新 數據庫表 ir_module_module 往裏面 登記 須要 加載的模塊

   

 

   

模型初始化

   

往註冊表 加載 模塊時, 調用 model 的 build_model()方法 創建 模型 # L233 load() registry.py

   

def load(self, cr, module):

""" Load a given module in the registry, and return the names of the

modified models.

   

At the Python level, the modules are already loaded, but not yet on a

per-registry level. This method populates a registry with the given

modules, i.e. it instanciates all the classes of a the given module

and registers them in the registry.

   

"""

from .. import models

   

lazy_property.reset_all(self)

   

# Instantiate registered classes (via the MetaModel automatic discovery

# or via explicit constructor call), and add them to the pool.

model_names = []

for cls in models.MetaModel.module_to_models.get(module.name, []):

# models register themselves in self.models

model = cls._build_model(self, cr)

model_names.append(model._name)

   

return self.descendants(model_names, '_inherit', '_inherits')

   

   

根據 addons depends 以及 _inherit 來決定 model 繼承

   

例如 , 經過 type(env['res.users']).mro() 查看 繼承 順序, 調用 supper() 時, 調用父級的前後順序

   

env['res.users'] 爲 空記錄集

type(env['res.users']) 獲得 記錄集對應的模型, 類型 [ class ]

   

   

例如, res.users 模型

   

   

   

模塊數據加載

   

在 模塊 加載時, 經過 _load_data() 調用 tools.convert_file() 將 data file 導入到 db

   

tools.convert_file(cr, module_name, filename, idref, mode, noupdate, kind, report)

   

 

convert_file()

   

def convert_file(cr, module, filename, idref, mode='update', noupdate=False, kind=None, report=None, pathname=None):

if pathname is None:

pathname = os.path.join(module, filename)

ext = os.path.splitext(filename)[1].lower()

   

with file_open(pathname, 'rb') as fp:

if ext == '.csv':

convert_csv_import(cr, module, pathname, fp.read(), idref, mode, noupdate)

elif ext == '.sql':

convert_sql_import(cr, fp)

elif ext == '.yml':

convert_yaml_import(cr, module, fp, kind, idref, mode, noupdate, report)

elif ext == '.xml':

convert_xml_import(cr, module, fp, idref, mode, noupdate, report)

elif ext == '.js':

pass # .js files are valid but ignored here.

else:

raise ValueError("Can't load unknown file type %s.", filename)

   

   

   

   

   

記錄集

   

實例化一個 model..

records=object.__new__(cls)

   

而後 給它的 屬性 _ids 賦值

   

這樣, 就能夠 經過 __getitem__() 獲取 字段數據, __setitem__() 設置 字段數據

   

   

記錄集 操做

Recordsets are immutable, but sets of the same model can be combined using various set operations, returning new recordsets. Set operations do not preserve order.

  • record in set returns whether record (which must be a 1-element recordset) is present in set. record not in set is the inverse operation # __contains__()
  • set1 <= set2 and set1 < set2 return whether set1 is a subset of set2 (resp. strict) # __le__()
  • set1 >= set2 and set1 > set2 return whether set1 is a superset of set2 (resp. strict)# __ge__()
  • set1 | set2 returns the union of the two recordsets, a new recordset containing all records present in either source# __or__()
  • set1 & set2 returns the intersection of two recordsets, a new recordset containing only records present in both sources# __and__()
  • set1 - set2 returns a new recordset containing only records of set1 which are notin set2 # __sub__()

   

   

   

數據讀寫

   

   

_read_from_database()

   

調用 cursor 執行 數據庫 讀取, 同時 更新 record cache.

# store result in cache

for vals in result:

record = self.browse(vals.pop('id'), self._prefetch)

record._cache.update(record._convert_to_cache(vals, validate=False))

   

   

   

read()

   

   

   

create()

 

   

   

write()

 

   

   

unlink()

   

   

 

   

   

   

約束

   

模型的 _constraints 屬性和 _sql_constraints 屬性

其中 _constraints 已廢棄,改用 @api. Constraints()

   

   

   

SQL 約束

   

(name, sql_definition, message)

 

模型初始化 db 時,往 db 創建約束, _add_sql_constraints () #L2175 model.py

   

   

   

Python 約束

   

經過 _validate_fields() 驗證 #L933 model.py /// create() 和 write() 時調用。

   

經過返回 true, 不然返回異常

   

   

   

   

   

detailed,,,

   

_constraints

list of (constraint_function, message, fields) defining Python constraints. The fields list is indicative

Deprecated since version 8.0: use constrains()

   

_sql_constraints

list of (name, sql_definition, message) triples defining SQL constraints to execute when generating the backing table

   

   

   

   

   

默認值

 

模型 defaults 屬性

 

經過上下文默認值,用戶默認值, 模型默認值[ 字段默認值,父級默認值] 進行維護

   

   

上下文默認值

default_ 開頭, 加上 字段

   

   

用戶默認值

self.env['ir.default'].get_model_defaults(self._name)

   

   

模型默認值

   

字段默認值

field.default

   

父級字段默認值

if field and field.inherited:

field = field.related_field

parent_fields[field.model_name].append(field.name)

   

   

具體 邏輯

   

default_get() #L974 model.py

   

實踐用法:

改寫 default_get() 改變默認值

   

   

   

   

   

視圖和字段

   

   

Web client 經過 rpc 調用 load_views 獲得視圖定義

   

@api.model

def load_views(self, views, options=None):

""" Returns the fields_views of given views, along with the fields of

the current model, and optionally its filters for the given action.

   

:param views: list of [view_id, view_type]

:param options['toolbar']: True to include contextual actions when loading fields_views

:param options['load_filters']: True to return the model's filters

:param options['action_id']: id of the action to get the filters

:return: dictionary with fields_views, fields and optionally filters

"""

options = options or {}

result = {}

   

toolbar = options.get('toolbar')

result['fields_views'] = {

v_type: self.fields_view_get(v_id, v_type if v_type != 'list' else 'tree',

toolbar=toolbar if v_type != 'search' else False)

for [v_id, v_type] in views

}

result['fields'] = self.fields_get()

   

if options.get('load_filters'):

result['filters'] = self.env['ir.filters'].get_filters(self._name, options.get('action_id'))

   

   

return result

   

   

底層 2個 方法:

   

fields_view_get 獲取視圖定義

   

   

fields_get 獲取字段定義

   

   

   

   

   

權限

   

ACL 記錄在 ir.model.access

Record rule 記錄在 ir.rule

   

   

   

   

對於 admin ,,, user_id =1 旁路

   

if self._uid == 1:

# User root have all accesses

return True

   

   

   

   

對於 admin ,,, user_id =1 旁路

   

if self._uid == SUPERUSER_ID:

return

   

   

   

 

   

在 增create() 刪 unlink() 改 write() 查 search() 時, 調用 check_access_rights() 以及 check_access_rule() 檢查是否有權限

   

   

視圖之權限處理

   

   

調用 _apply_group() 將 無權限訪問的 node 去除

   

對於 field 設置了 權限的, 將 字段設置爲 readonly

   

   

   

   

服務

   

wsgi 應用

   

   

導出 xmlrpc 和 http 服務

 

WSGI application 被 server 使用

   

   

XML RPC 服務

   

http.py dispatch_rpc()

   

根據不一樣的名稱空間,轉發給對應的 handler

   

   

   

Web服務

   

http.py Root.dispatch()

   

# 經過 Root 類 的 __call__() 調用 dispatch()

相關文章
相關標籤/搜索