2023年3月

定义

代理是一个
中间者的角色
,如生活中的中介,出于种种考虑/限制,一个对象不能直接访问另一个对象,需要一个第三者(中间代理)牵线搭桥从而间接达到访问目的,这样的就是代理模式。

es6 中的代理

es6 的 proxy 就是上面说的代理模式的实现,es6 帮我们在语法层面提供了这个新的api,让我们可以很轻松的就使用了代理模式。

const p = new Proxy(target, handler)
target:要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)
handler:一个通常以函数作为属性的对象

proxy 实例

const handler = {
    get: function(obj, prop) {
        return prop in obj ? obj[prop] : 37;
    }
};

const p = new Proxy({}, handler);
p.a = 1;
p.b = undefined;

console.log(p.a, p.b);      // 1, undefined
console.log('c' in p, p.c); // false, 37

应用实践-模拟代理模式

代理模式的应用非常常见
,既可以是为了
加强控制、拓展功能、提高性能
,也可以仅仅是为了优化我们的代码结构、实现功能的解耦。无论是出于什么目的,这种模式的套路就只有一个——
A 不能直接访问 B,A 需要借助一个帮手来访问 B,这个帮手就是代理器
。需要这种代理器的就是代理模式的应用场景。

通常开发中最常见的代理类型:
事件代理、虚拟代理、缓存代理、保护代理

  • 事件代理:代理 DOM
  • 虚拟代理:代理 DOM
  • 缓存代理:代理函数
  • 保护代理:代理对象

事件代理

事件代理是代理模式最常见的一种应用方式,它的场景是一个父元素下有多个子元素

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport">
  <meta content="yes" name="apple-mobile-web-app-capable">
  <meta content="black" name="apple-mobile-web-app-status-bar-style">
  <meta content="telephone=no,email=no" name="format-detection">
  <meta name="App-Config" content="fullscreen=yes,useHistoryState=yes,transition=yes">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title></title>

</head>

<body>
  <p>图片列表--事件代理</p>
  <ul id="ul_wrapper">
    <li>
      1、<img id="img_1" src="" alt="">
    </li>
    <li>
      2、<img id="img_2" src="" alt="">
    </li>
    <li>
      3、<img id="img_3" src="" alt="">
    </li>
    <li>
      4、<img id="img_4" src="" alt="">
    </li>
    <li>
      5、<img id="img_5" src="" alt="">
    </li>
    <li>
      6、<img id="img_6" src="" alt="">
    </li>
    <li>
      7、<img id="img_7" src="" alt="">
    </li>
    <li>
      8、<img id="img_8" src="" alt="">
    </li>
  </ul>
  <script>
      // 自己找个base64,拷贝上来太长了
    let defualtSrc = ``
    let initPage = (function () {
      document.querySelectorAll('img').forEach(item => {
        item.src = defualtSrc
      })
    })();
    document.querySelector('#ul_wrapper').addEventListener('click', function (e) {
      if (e.target.nodeName === 'IMG') {
        alert('图片被点击')
      }
    }, false)
  </script>
</body>

</html>

缓存代理

缓存代理可以避免重复的计算

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport">
  <meta content="yes" name="apple-mobile-web-app-capable">
  <meta content="black" name="apple-mobile-web-app-status-bar-style">
  <meta content="telephone=no,email=no" name="format-detection">
  <meta name="App-Config" content="fullscreen=yes,useHistoryState=yes,transition=yes">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title></title>

</head>

<body>
  <p>图片列表--缓存代理</p>
  <button type="button">计算</button>
  <div>
    <label>结果:</label><input type="text">
  </div>
  <script>

    /*
    有效的减少计算;
    工具函数
    */
    const addAll = function (...args) {
      console.log('进行了一次新计算')
      let result = 0
      const len = args.length
      for (let i = 0; i < len; i++) {
        result += args[i]
      }
      return result
    }

    let proxyAddAll = (function () {
      const resultCache = {}
      return function (fn, ...args) {
        const key = args.join('')
        if (resultCache[key]) {
          return resultCache[key]
        }
        return resultCache[key] = fn.apply(this, args)
      }
    })()

    // 123456 参数相同,只是第一次运算的时候,打印了一次进行了一次新计算
    console.log(proxyAddAll(addAll, 1, 2, 3, 4, 5, 6))
    console.log(proxyAddAll(addAll, 1, 2, 3, 4, 5, 6))
    console.log(proxyAddAll(addAll, 1, 2, 3, 4, 5, 6))
    // 1234567 因为是一个全新的参数所以打印了一次进行了一次新计算
    console.log(proxyAddAll(addAll, 1, 2, 3, 4, 5, 7))
  </script>
</body>

</html>

虚拟代理

图片预加载,预加载主要是为了避免网络不好、或者图片太大时,页面长时间给用户留白的尴尬。原理也很简单创建一个图片实例指向图片真实地址,当完成加载时,把占位图的地址替换成真实的地址,这个时候浏览器会直接从缓存里面拿。

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport">
  <meta content="yes" name="apple-mobile-web-app-capable">
  <meta content="black" name="apple-mobile-web-app-status-bar-style">
  <meta content="telephone=no,email=no" name="format-detection">
  <meta name="App-Config" content="fullscreen=yes,useHistoryState=yes,transition=yes">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title></title>

</head>

<body>
  <p>图片列表--虚拟代理</p>
  <ul>
    <li>
      1、<img id="img_1" src="" alt="">
    </li>
    <li>
      2、<img id="img_2" src="" alt="">
    </li>
    <li>
      3、<img id="img_3" src="" alt="">
    </li>
    <li>
      4、<img id="img_4" src="" alt="">
    </li>
    <li>
      5、<img id="img_5" src="" alt="">
    </li>
    <li>
      6、<img id="img_6" src="" alt="">
    </li>
    <li>
      7、<img id="img_7" src="" alt="">
    </li>
    <li>
      8、<img id="img_8" src="" alt="">
    </li>
  </ul>
  <script>
    // 替换成你的base64,拷贝上来太长
    let defualtSrc = ""
    let initPage = (function () {
      document.querySelectorAll('img').forEach(item => {
        item.src = defualtSrc
      })
    })();

    // 设置图片地址
    function setImgUrl(dom, src) {
      dom.src = src;
    }

    // 中间的代理图片地址
    function proxyImg(element, url) {
      // 创建一个虚拟Image实例
      const virtualImage = new Image()
      virtualImage.onload = function () {
        setImgUrl(element, url)
      }
      virtualImage.src = url
    }
    function preLoadImg() {
      const urlList = [
        'https://t7.baidu.com/it/u=2621658848,3952322712&fm=193&f=GIF',
        'https://t7.baidu.com/it/u=4080826490,615918710&fm=193&f=GIF',
        'https://t7.baidu.com/it/u=334080491,3307726294&fm=193&f=GIF',
        'https://t7.baidu.com/it/u=3713375227,571533122&fm=193&f=GIF',
        'https://t7.baidu.com/it/u=801209673,1770377204&fm=193&f=GIF',
        'https://t7.baidu.com/it/u=1856946436,1599379154&fm=193&f=GIF',
        'https://t7.baidu.com/it/u=1010739515,2488150950&fm=193&f=GIF',
        'https://t7.baidu.com/it/u=813347183,2158335217&fm=193&f=GIF']
      document.querySelectorAll('img').forEach((element, index) => {
        proxyImg(element, urlList[index])
        element.src = defualtSrc
      })
    }
    setTimeout(() => {
      preLoadImg()
    }, 0.5 * 1000);

    /*
    核心: 有个虚拟的实例去请求地址,拿到之后替换到真实的dom
    */
  </script>
</body>

</html>

保护代理

可以通过es6 的proxy 的get、set 访问器实现

const handler = {
  get: function(obj, prop) {
      return prop in obj ? obj[prop] : '你不能访问';
  }
};

const p = new Proxy({}, handler);
p.a = 1;
p.b = undefined;

console.log(p.a, p.b);      // 1, undefined
console.log('c' in p, p.c); // false, 你不能访问

具体查看
proxy

Proxy

小结

A 不能直接访问 B,A 需要借助一个帮手来访问 B,这个帮手就是代理器,
通常开发中最常见的四种代理类型:事件代理、虚拟代理、缓存代理、保护代理;
  1. 事件代理:事件冒泡,代理 DOM
  2. 虚拟代理:通过Image加载图片,代理 DOM
  3. 缓存代理:缓存计算结果,代理函数
  4. 保护代理:get,set保护核心数据,代理对象

声明(
叠甲
):鄙人水平有限,本文为作者的学习总结,仅供参考。


1. RMQ 介绍

在开始介绍 ST 表前,我们先了解以下它以用的场景
RMQ问题
。RMQ (Range Minimum/Maximum Query)问题是指:对于长度为n的数列A,回答若干询问RMQ(A,i,j)(i,j<=n),返回数列A中下标在i,j里的最小(大)值,也就是说,RMQ问题是指求区间最值的问题,其主要的特征是查询的区间是静态的。
在上一篇关于
线段树
的文章中我们解决了动态的区间的维护,先是进行O(nlog(n))时间负载度的建树预处理,然后就能以O(log(n))的时间复杂度进行维护与查询。对于 RMQ 问题来说线段树也是能过比较好的处理,总的时间复杂度为O(nlog(n)+log(n)),比暴力法的时间复杂O(n^2)还行快一些。


2. ST 表介绍

虽然线段树也能比较好的解决 RMQ 问题,但是它的特性还是更加符合动态的情况,故对于静态的来说就引入了 ST 表来进行解决。ST 表是先对数据进行 O(nlog(n)) 的预处理,然后就可以进行 O(1) 的查询。(是常数!!!)
故,一般的 ST 表题的解法解法可以分为以下步骤:

【1】 进行预处理,一般来说使用动态规划的思想进行的
【2】 进行查询


3. 举些栗子

3.1
ST 表模板题

题目描述

给定一个长度为 N 的数列,和 M 次询问,求出每一次询问的区间内数字的最大值。

这是一道 ST 表经典题——静态区间最大值,根据上述的描述,解题思路如下(具体的数学关系就不做解释,自己画画图就可以推理出来的):

【1】 进行预处理 :这里我们使用一个数组 st[i][j] 进行打表,其含义为从第 i 个数开始数 2^j 个数中的最大值,故我们可以得到动态规划的状态转移方程:
                        st[i][j] = max(st[i][j-1],st[i-(1<<j)][j-1])
【2】 进行查询 : 对于区间 [l,r] 来说,我可以使用以下来表达其的最大值:
                                    m = log2(r-l+1)
                        max[l,r] = max(st[l][m],st[r-(1<<m)][m])

根据以上的思路可以得到以下代码

#include <bits/stdc++.h>
using namespace std;
#define NMAX 100000
int n,m,x,y;
int st[NMAX+1][20]; // st[i][j] 表示从 i 开始 2^j 个数中需要的答案 
// ST 表的查询函数
int calc(int l,int r)
{
	int m = log2(r - l + 1);
	return max(st[l][m],st[r-(1<<m)+1][m]);	
} 
int main()
{
	// [1] 获取数据并进行预处理
	cin >> n >> m;
	for(int i = 1;i <= n;++i)
	{
		cin >> st[i][0];
	}  
    // 需要注意的是我们要从 i 开始遍历 st[i][j]
	for(int j = 1; (1 << j) <= n;++j)
	{
		for(int i = 1;i + (1<<j) - 1 <= n;++i)
		{
			st[i][j] = max(st[i][j-1],st[i + (1<<(j-1))][j-1]);
		}
	}
	// [2] 查询
	while(m--)
	{
		scanf("%d%d",&x,&y);
		printf("%d\n",calc(x,y));	
	} 
    return 0;
}

3.2
质量检测

题目描述

为了检测生产流水线上总共 N 件产品的质量,我们首先给每一件产品打一个分数 A 表示其品质,然后统计前 M 件产品中质量最差的产品的分值 Q[m] = min{A_1, A_2, ... A_m},以及第 2 至第
\(M + 1\)
件的 Q[m + 1], Q[m + 2] ... 最后统计第 N - M + 1 至第 N 件的 Q[n]。根据 Q 再做进一步评估。

请你尽快求出 Q 序列。

解题思路如下

总的思路如上题一致,无非就是从查询最大最变成了最少小值,以及查询时给定了区间左右边界的规定关系 [i,i+M-1]
具体 AC 代码如下

#include <bits/stdc++.h>

#define NMAX 1000000
using namespace std;

int m,n;
int st[NMAX+1][32];

int calc(int x,int y)
{
	int m = log2(y-x+1);
	return min(st[x][m],st[y-(1<<m)+1][m]);
}

int main()
{
	cin >> n >> m;
	// [1] 获取数据,并进行预处理
	for(int i = 1;i <= n;i++)
	{
		cin >> st[i][0];	
	} 
	for(int j = 1;(1<<j) - 2<= n;j++)
	{
		for(int i = 1;i+(1<<j)-1 <= n;i++)
		{
			st[i][j] = min(st[i][j-1],st[i+(1<<(j-1))][j-1]);	
		}	
	}	
	// [2] 查询
	for(int i = m;i <= n;i++)
	{
		cout << calc(i-m+1,i) << endl;
	}
	
	return 0;
} 

3.3
[蓝桥杯 2022 省 A] 选数异或

题目描述

给定一个长度为 n 的数列 A1 A2 ... An 和一个非负整数 x, 给定 m 次查询, 每次询问能否从某个区间 [l, r] 中选择两个数使得他们的异或等于 x

这题一眼看出就是很明显的静态区间查询问题,但是与上述中不同的是,这次查询的不再是最值,而是满足关系数对的下标。

总的解题思路还是不变的:

【1】 预处理:这里的两数异或我们可以联想到两数和的问题,故可以利用一个 Hash 数组记录其每个数的下标来辅助我们处理(具体实现见代码),需要注意的是我 ST 表中记录的应该是与这个数满足关系对象中的最近一个,故状态转移方程为:st[i] = max(st[i-1],Hash[Ai])
【2】 根据预处理得到的 ST 表,我们只要查表看该区间得到的值是否大于区间的左边界值

AC 代码如下:

#include <bits/stdc++.h>
using namespace std;

int main()
{
    int n,m,x;
	cin >> n >> m >> x;
	map<int,int> Hash;
    // st[i] 表示 1~i 中满足关系的数对的最后出现的一个的下标
	int st[n+1] = {0,};
    // [1] 预处理
    for(int i = 1;i <= n;i++)
    {
        int data;
        cin >> data;
        st[i] = max(st[i-1],Hash[data]);
        Hash[data^x] = i;
    }
    // [2] 查询
    while(m--)
    {
        int l,r;
        cin >> l >> r;
        if(st[r] >= l) cout << "yes" << endl;
        else cout << "no" << endl;
    }
    return 0;
}

4.参考

洛谷ST表模板题题解
本文到此结束,希望对您有所帮助。

说明

使用 VLD 内存泄漏检测工具辅助开发时整理的学习笔记。


1. 使用方式

在 QT 中使用 VLD 的方法可以查看另外几篇博客:

本次测试使用的环境为:
QT 5.9.2

MSVC 2015 32bit

Debug
模式,VLD 版本为 2.5.1,VLD 配置文件不做任何更改使用默认配置,测试工程所在路径为:
E:\Cworkspace\Qt 5.9\QtDemo\testVLD

2. 无内存泄漏时的输出报告

写一个简单的无内存泄漏的程序,如下:

#include <QCoreApplication>
#include "vld.h"

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    return a.exec();
}

程序运行结束后,并没有检测到内存泄漏,VLD 会输出以下 4 行报告:

Visual Leak Detector read settings from: D:\Program Files (x86)\Visual Leak Detector\vld.ini
Visual Leak Detector Version 2.5.1 installed.
No memory leaks detected.
Visual Leak Detector is now exiting.

第 1 行
表示 VLD 读取的配置文件路径,可以根据路径找到该文件,然后更改里面的相关配置,获得想要的效果。

第 2 行
表示 VLD 2.5.1 在程序中初始化成功。

第 3 行
表示本次运行没有检测到内存泄漏。

第 4 行
表示 VLD 正常退出。

一些用户界面

数据文件 (XML)

参考
: 该主题关联文档可以查看
Data Files
.

上一章,我们通过CSV文件添加了数据。当需要添加数据格式简单时,用CSV格式还是很方便的,当数据格式更复杂时(比如视图架构或者一个邮件模板),我们使用XML格式。比如包含HTML tags的
help field
。虽然可以通过CSV文件加载这样的数据,但是使用XML更方便。

类似CSV文件,XML文件也必须按约定添加到合适的目录,并在
__manifest__.py
中进行定义。数据文件中的内容也是在模块安装或者更新时按序加载。因此,对CSV文件所做的所有说明对XML文件都适用。当数据链接到视图时,我们将它们添加到
views
文件夹中

本章,我们将通过XML文件加载我们第一个action和菜单。Actions 和菜单为数据库中的标准记录。

注解:

当程序很注重性能时,CSV格式优先于XML格式。这是因为,在odoo中加载CSV文件比加载XML文件更快。

odoo中,用户接口(action,菜单和视图)大部分是通过创建和组装XML文件中的记录来定义的。常见的模式为 菜单> action > 视图。为了访问记录,用户在几个菜单级中导航。最深层是触发打开记录列表的action。

操作(Actions)

参考
: 主题相关文档可以查看
Actions
.

动作可以通过三种方式触发 :

  1. 点击菜单项目(链接接到指定动作)
  2. 点击视图按钮(如果与action关联)
  3. 对象的上下文action

本章仅涵盖第一种情况。 我们Real Estate例子中,希望将一个菜单连接到
estate.property
model, 以便创建一个新记录。 action可以视为菜单和model之间的链接

test.model
的基本action:

<record id="test_model_action" model="ir.actions.act_window">
    <field name="name">Test action</field>
    <field name="res_model">test.model</field>
    <field name="view_mode">tree,form</field>
</record>
  • id
    外部标识。它可以用于引用记录(不需要知道其在数据库中的标识符)。
  • model
    ir.actions.act_window
    (
    Window Actions (ir.actions.act_window)
    )的一个固定值
  • name
    action名称
  • res_model
    action应用的范围。
  • view_mode
    可获取的视图。本例中为列表(树)和表格视图。

odoo中到处都可以找到例子,但是
这个
简单action的好例子。关注XML 数据文件结构,因为你在后续的练习中会用到。

<?xml version="1.0"?>
<odoo>
    <record id="crm_lost_reason_view_search" model="ir.ui.view">
        <field name="name">crm.lost.reason.view.search</field>
        <field name="model">crm.lost.reason</field>
        <field name="arch" type="xml">
            <search string="Search Opportunities">
                <field name="name"/>
                <filter string="Include archived" name="archived" domain="['|', ('active', '=', True), ('active', '=', False)]"/>
                <separator/>
                <filter string="Archived" name="inactive" domain="[('active', '=', False)]"/>
            </search>
        </field>
    </record>

    <record id="crm_lost_reason_view_form" model="ir.ui.view">
        <field name="name">crm.lost.reason.form</field>
        <field name="model">crm.lost.reason</field>
        <field name="arch" type="xml">
            <form string="Lost Reason">
                <sheet>
                    <div class="oe_button_box" name="button_box">
                        <button name="action_lost_leads" type="object"
                            class="oe_stat_button" icon="fa-star">
                            <div class="o_stat_info">
                                <field name="leads_count" class="o_stat_value"/>
                                <span class="o_stat_text"> Leads</span>
                            </div>
                        </button>
                    </div>
                    <widget name="web_ribbon" title="Archived" bg_color="bg-danger" attrs="{'invisible': [('active', '=', True)]}"/>
                    <div class="oe_title">
                        <div class="oe_edit_only">
                            <label for="name"/>
                        </div>
                        <h1 class="mb32">
                            <field name="name" class="mb16"/>
                        </h1>
                        <field name="active" invisible="1"/>
                    </div>
                </sheet>
            </form>
        </field>
    </record>

    <record id="crm_lost_reason_view_tree" model="ir.ui.view">
        <field name="name">crm.lost.reason.tree</field>
        <field name="model">crm.lost.reason</field>
        <field name="arch" type="xml">
            <tree string="Channel" editable="bottom">
                <field name="name"/>
            </tree>
        </field>
    </record>

    <!-- Configuration/Lead & Opportunities/Lost Reasons Menu -->
    <record id="crm_lost_reason_action" model="ir.actions.act_window">
        <field name="name">Lost Reasons</field>
        <field name="res_model">crm.lost.reason</field>
        <field name="view_mode">tree,form</field>
        <field name="help" type="html">
          <p class="o_view_nocontent_smiling_face">
            Define a new lost reason
          </p><p>
            Use lost reasons to explain why an opportunity is lost.
          </p><p>
            Some examples of lost reasons: "We don't have people/skill", "Price too high"
          </p>
        </field>
    </record>

    <record id="menu_crm_lost_reason" model="ir.ui.menu">
        <field name="action" ref="crm.crm_lost_reason_action"/>
    </record>
</odoo>

练习


estate.property
model 创建action。

在适当的位置(本例中为
odoo14/custom/estate/models/views
)创建
estate_property_views.xml

<?xml version="1.0"?>
<odoo>
    <record id="link_estate_property_action" model="ir.actions.act_window">
        <field name="name">Properties</field>
        <field name="res_model">estate.property</field>
        <field name="view_mode">tree,form</field>
    </record>
</odoo>

修改
odoo14/custom/estate/__manifest__.py

#!/usr/bin/env python
# -*- coding:utf-8 -*-
{
    'name': 'estate',
    'depends': ['base'],
    'data':['security/ir.model.access.csv',
            'views/estate_property_views.xml'
            ]
}

重启服务并观察文件加载日志。

菜单(Menus)

参考
: 和本主题关联文档可以查看
Shortcuts
.

为了减少菜单(
ir.ui.menu
)定义和链接到对应action的复杂性,我们可以使用
shortcut

test_model_action
一个的基础菜单:

<menuitem id="test_model_menu_action" action="test_model_action"/>

test_model_menu_action
菜单被链接到
test_model_action

action
链接到model
test.model
。正如前面所述,
action
可以看做是菜单和
model
之间的连接。

注意:这里的id的值和action的值不能设置成一样,否则会报错。

然而,菜单总是遵循一种体系结构,实际上有三个层次的菜单:

  1. 根菜单,显示在App切换器中(Odoo社区版切换器是一个下拉菜单)
  2. 第一级菜单,显示在顶部栏中
  3. 动作菜单

最容易的方式是在XML文件中定义结构来创建菜单。


test_model_action
定义的一个基础菜单结构:

<menuitem id="test_menu_root" name="Test">
    <menuitem id="test_first_level_menu" name="First Level">
        <menuitem id="test_model_menu_action" action="test_model_action"/>
    </menuitem>
</menuitem>

第三级菜单的名称,直接从
action
获取,即为
action
属性值

练习

添加菜单

在合适的目录(本例中为
odoo14/custom/estate/models/views
)创建
estate_menus.xml
文件

<?xml version="1.0"?>
<odoo>
    <menuitem id="test_menu_root" name="Test">
        <menuitem id="test_first_level_menu" name="First Level">
            <menuitem id="estate_property_menu_action" action="link_estate_property_action"/>
        </menuitem>
    </menuitem>
</odoo>

修改
odoo14/custom/estate/__manifest__.py

#!/usr/bin/env python
# -*- coding:utf-8 -*-
{
    'name': 'estate',
    'depends': ['base'],
    'data':['security/ir.model.access.csv',
            'views/estate_property_views.xml',
            'views/estate_menus.xml'
            ]
}

重启odoo服务,查看效果

字段,属性和视图(Fields, Attributes And View)

到目前为止,我们只对房产广告使用了通用视图,但在大多数情况下,我们希望对视图进行微调。Odoo有许多微调方式,但通常第一步是确保:

  • 某些字段有默认值
  • 某些字段只读
  • 当记录重复时,某些字段不能被拷贝

在我们的房产业务案例中,我们希望::

  • 售价只读(往后将自动填充)
  • 当记录重复时,可用日期和售价不能被拷贝
  • 卧室数量应该默认为2
  • 默认可用日期应该为3个月

一些新属性

在进一步进行视图设计之前,让我们回到模型定义。我们看到一些属性,如
required=True
,会影响数据库中的表模式。其他属性也将影响视图或提供默认值。

练习 -- 添加一些属性到字段。

查找一些合适的属性 (查看
字段
) 来:

  • 设置售价为只读
  • 阻止复制可用日期和售价

修改
odoo14\custom\estate\models\estate_property.py

EstateProperty
类属性
expected_price

selling_price
的值如下:

expected_price = fields.Float('expected price', digits=(8, 2),  required=True) 
selling_price = fields.Float('selling price', digits=(8, 2), readonly=True, copy=False)

重启服务和并刷新浏览器界面,我们可以看到无法设置任何售价。复制记录时,可用日期应为空。

预期效果可参考该动画连接:
https://www.odoo.com/documentation/14.0/zh_CN/_images/attribute_and_default.gif

默认值

可以为任何字段设置默认值。字段定义中,添加
default=X
, 其中的
X
可以是Python文本值(boolean, integer, float, string) ,也可以是一个以model对象自身为入参并返回一个值的函数:

name = fields.Char(default="Unknown")
last_seen = fields.Datetime("Last Seen", default=lambda self: fields.Datetime.now())

例中
name
字段默认值为‘Unknown’,而
last_seen
字段默认值为当前时间

练习 -- 设置默认值

添加适当的默认值:

  • 卧室数量默认值为 2
  • 可用日期默认为3个月内

修改
odoo14\custom\estate\models\estate_property.py

EstateProperty
类属性
bedrooms

selling_price
的值如下:

bedrooms = fields.Integer(default=2)
date_availability = fields.Datetime('Availability Date', copy=False, default= lambda self: fields.Datetime.today())

重启服务和并刷新浏览器界面验证

保留字段

参考
: 主题相关文档可参考
保留字段名称
.

odoo为预定义行为保留了一些字段名称。当需要相关行为时,需要在模型中定义这些保留字段。

练习 -- 添加
active
字段

添加一个
active
字段到
estate.property
模型。

修改
odoo14\custom\estate\models\estate_property.py

EstateProperty
类,增加
active
属性

active = fields.Boolean('Active')

重启服务,刷新浏览器界面,新增一条记录,新增时勾选
Active
复选框,即
active=True
,验证效果。

预期效果可参考该动画链接:
https://www.odoo.com/documentation/14.0/zh_CN/_images/inactive.gif

注意,已存在的记录的
active
字段默认值为
False

练习--为
active
字段添加设置


active
字段设置默认值


active
字段设置适当的属性值,让它不再出现在页面。

练习 -- 添加
state
字段


estate.property
model添加
state
字段(字段名可自定义),一个选择列表。可选值:
New
,
Offer Received,
Offer Accepted, Sold

Canceled
。必选字段,且不能被拷贝,默认值
New

修改
odoo14\custom\estate\models\estate_property.py

EstateProperty
类,修改
active
字段,增加
state
字段

    active = fields.Boolean('Active', default=True, invisible=True) # 注意:实践发现,invisible字段不起作用
    state = fields.Selection(
        string='State',
        selection=[('New','New'),
                   ('Offer Received','Offer Received'),
                   ('Offer Accepted', 'Offer Accepted'),
                   ('Sold','Sold'),
                   ('Canceled', 'Canceled')],
        copy=False
    )

重启服务,验证效果

在 wsl 中用 docker-compose 搭建了一台 zookeeper + 三台 broker 的 kafka 集群,使用的镜像是
bitnami/kafka
,在按照镜像文档运行容器后,发现运行在宿主机里的客户端程序无法正确的推送/消费消息,研究后发现镜像文档只适用于客户端程序和 kafka 集群同属于一个 docker 网段,外部访问还需要一些额外的配置,过程中出现过以下几个主要的错误:

  • dial tcp: lookup 333be5d4e335 on 172.30.96.1:53: no such host
  • kafka: client has run out of available brokers to talk to: dial tcp 127.0.0.1:19092: connect: connection refused
  • [Controller id=1, targetBrokerId=3] Client requested connection close from node 3 (org.apache.kafka.clients.NetworkClient)

这里先贴一个可以用的 docker-compose.yml 配置,后面对其中的关键配置做一个解释,最后再解释出现上面错误的原因,文件最后的 kafka-ui 是一个可视化管理界面,可以不要

version: "3"

services:
  zookeeper:
    container_name: kafka_zookeeper
    image: bitnami/zookeeper
    user: root
    ports:
      - "2181:2181"
    environment:
      - ALLOW_ANONYMOUS_LOGIN=yes
    volumes:
      - ./zookeeper:/bitnami/zookeeper
  broker1:
    container_name: kafka_broker1
    image: bitnami/kafka
    user: root
    ports:
      - "19092:9092"
    environment:
      - KAFKA_CFG_ZOOKEEPER_CONNECT=zookeeper:2181
      - ALLOW_PLAINTEXT_LISTENER=yes
      - KAFKA_BROKER_ID=1
      - KAFKA_LISTENERS=INTERNAL://0.0.0.0:9000,EXTERNAL://0.0.0.0:9092
      - KAFKA_ADVERTISED_LISTENERS=INTERNAL://broker1:9000,EXTERNAL://localhost:19092
      - KAFKA_LISTENER_SECURITY_PROTOCOL_MAP=INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT
      - KAFKA_INTER_BROKER_LISTENER_NAME=INTERNAL
    volumes:
      - ./broker1:/bitnami/kafka
    depends_on:
      - zookeeper
  broker2:
    container_name: kafka_broker2
    image: bitnami/kafka
    user: root
    ports:
      - "29092:9092"
    environment:
      - KAFKA_CFG_ZOOKEEPER_CONNECT=zookeeper:2181
      - ALLOW_PLAINTEXT_LISTENER=yes
      - KAFKA_BROKER_ID=2
      - KAFKA_LISTENERS=INTERNAL://0.0.0.0:9000,EXTERNAL://0.0.0.0:9092
      - KAFKA_ADVERTISED_LISTENERS=INTERNAL://broker2:9000,EXTERNAL://localhost:29092
      - KAFKA_LISTENER_SECURITY_PROTOCOL_MAP=INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT
      - KAFKA_INTER_BROKER_LISTENER_NAME=INTERNAL
    volumes:
      - ./broker2:/bitnami/kafka
    depends_on:
      - broker1
  broker3:
    container_name: kafka_broker3
    image: bitnami/kafka
    user: root
    ports:
      - "39092:9092"
    environment:
      - KAFKA_CFG_ZOOKEEPER_CONNECT=zookeeper:2181
      - ALLOW_PLAINTEXT_LISTENER=yes
      - KAFKA_BROKER_ID=3
      - KAFKA_LISTENERS=INTERNAL://0.0.0.0:9000,EXTERNAL://0.0.0.0:9092
      - KAFKA_ADVERTISED_LISTENERS=INTERNAL://broker3:9000,EXTERNAL://localhost:39092
      - KAFKA_LISTENER_SECURITY_PROTOCOL_MAP=INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT
      - KAFKA_INTER_BROKER_LISTENER_NAME=INTERNAL
    volumes:
      - ./broker3:/bitnami/kafka
    depends_on:
      - broker2
  kafka-ui:
    container_name: kafka-ui
    image: provectuslabs/kafka-ui
    ports:
      - "8080:8080"
    restart: always
    environment:
      - KAFKA_CLUSTERS_0_NAME=broker1
      - KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS=broker1:9000
      - KAFKA_CLUSTERS_1_NAME=broker2
      - KAFKA_CLUSTERS_1_BOOTSTRAPSERVERS=broker2:9000
      - KAFKA_CLUSTERS_2_NAME=broker3
      - KAFKA_CLUSTERS_2_BOOTSTRAPSERVERS=broker3:9000
    depends_on:
      - broker3

其中几个容易搞错的关键配置如下:

ports:
  - "19092:9092"
environment:
  - KAFKA_LISTENERS=INTERNAL://0.0.0.0:9000,EXTERNAL://0.0.0.0:9092
  - KAFKA_ADVERTISED_LISTENERS=INTERNAL://broker1:9000,EXTERNAL://localhost:19092

参数 KAFKA_LISTENERS 和 KAFKA_ADVERTISED_LISTENERS 的作用:

  • KAFKA_LISTENERS 代表 broker 的监听地址,kafka客户端首先需要与这个地址建立连接,完成必要的认证工作
  • KAFKA_ADVERTISED_LISTENERS 代表 broker 的数据传输地址,这里配置的地址会注册到 zookeeper 中,在客户端完成身份认证后会从 zk
    原封不动
    地获得这里配置的 ip+port 用于消息推送/消费

在上面的配置中,KAFKA_ADVERTISED_LISTENERS 的 EXTERNAL 配置了 localhost:19092,这是因为我的客户端程序运行在 wsl 中,而 19092 端口已经映射到了容器的 9092 端口上所以可以正确访问,如果 kafka 集群和客户端程序运行在两个不同的服务器上,这里应该配置 kfaka 集群所在的主机 ip,只需要记住这一串地址的 ip+port 部分是原封不动的传给客户端的,想想客户端程序所在的机器能不能解析它吧

另外,关于 KAFKA_LISTENERS 中 port 的配置与上面 ports 属性中的端口映射的关系是:
先有端口监听后有端口映射
,这个地方没理解清楚的话就很容易对这两个配置项感到迷惑,例如上面配置了 9000 和 9092 两个监听端口,然后将 9092 映射到了宿主机的 19092,9000 作为未公开的端口只有同属一个 docker 网络的机器才能访问

一开始出现的几个主要错误也都是由这几个配置引起:

  • dial tcp: lookup 333be5d4e335 on 172.30.96.1:53: no such host

未配置 KAFKA_LISTENERS 的情况下默认是该 broker 容器的主机名+9092,未配置 KAFKA_ADVERTISED_LISTENERS 的情况下该值等于 KAFKA_LISTENERS,这种情况下宿主机的程序建立连接后拿到了一个未知的主机名 333be5d4e335 发送消息,当然行不通(宿主机无法将该主机名转换成 ip 访问)

  • kafka: client has run out of available brokers to talk to: dial tcp 127.0.0.1:19092: connect: connection refused

端口配置没有理解清楚,KAFKA_LISTENERS 中对外的监听端口必须是被映射出去的 9092 本身,否则宿主机无法访问

  • [Controller id=1, targetBrokerId=3] Client requested connection close from node 3 (org.apache.kafka.clients.NetworkClient)

端口配置没有理解清楚,brokers 之间的通信是内部通信,内部监听端口可以不公开映射出去,但是流程是一样的

另外在配置项变更后,最好删除容器,并删除各个目录里面的 data 目录里面的文件再重新创建容器,不确定是哪一个配置变更会出现以下错误:

  • org.apache.zookeeper.KeeperException$NodeExistsException: KeeperErrorCode = NodeExists