• Odoo 11 Backend


    Table of Contents

    命令入口

    服务器

    启动server

    thread 模式

    prefork 模式

    gevent模式

    wsgi 应用

    响应 客户端请求

    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 serviceserver.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()

  • 相关阅读:
    【代码笔记】iOS-JQIndicatorViewDemo
    【代码笔记】iOS-plist获得城市列表
    【代码笔记】iOS-HTTPQueue下载图片
    【代码笔记】iOS-请求去掉url中的空格
    【代码笔记】iOS-collectionView实现照片删除
    【代码笔记】iOS-自定义loading(IanAlert)
    Foundation框架-NSCalendar
    iOS Xcode全面剖析
    Mac下Git的基础操作
    iOS开发UI篇—常见的项目文件介绍
  • 原文地址:https://www.cnblogs.com/odoouse/p/8401456.html
Copyright © 2020-2023  润新知