2023年4月

1.Copy和Clone

Rust中的
Copy

Clone
trait都允许创建类型实例的副本。它们都提供了一种复制类型实例的方法,但它们之间存在一些重要的区别。了解这些区别有助更好地使用这两个特征。

2.
Copy
trait

Copy
trait允许按位复制类型的实例。这意味着当您将一个变量赋值给另一个变量时,如果该类型实现了
Copy
trait,则会创建一个新的副本。这与移动语义不同,其中原始变量不再可用。

要使用
derive
属性为类型自动生成
Copy
trait的实现,只需在类型定义之前添加
#[derive(Copy)]
即可。例如:

#[derive(Copy)]
struct Point {
x: i32,
y: i32,
}
复制代码

请注意,并非所有类型都可以实现
Copy
trait。例如,具有堆分配字段(如
String

Vec<T>
)的类型不能实现
Copy

3.
Clone
trait

与之相反,
Clone
trait提供了一个
clone
方法,用于创建类型实例的深层副本。这意味着即使类型具有堆分配字段(如
String

Vec<T>
),也可以实现
Clone
trait。

要为类型自动生成
Clone
trait的实现,只需在类型定义之前添加
#[derive(Clone)]
即可。例如:

#[derive(Clone)]
struct Point {
x: i32,
y: i32,
}
复制代码

但是,并非所有类型都可以使用
derive
属性自动生成
Clone
trait的实现。如果类型的某些字段没有实现

4.
Copy

Clone
trait之间的区别

尽管
Copy

Clone
trait都允许您创建类型实例的副本,但它们之间存在一些重要的区别。

首先,当您使用赋值语句复制一个实现了
Copy
trait的类型时,复制操作是隐式执行的。而当您使用
clone
方法复制一个实现了
Clone
trait的类型时,复制操作是显式执行的。 例如:

#[derive(Copy)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let p1 = Point { x: 1, y: 2 };
let p2 = p1
assert_eq!(p1.x, p2.x);
assert_eq!(p1.y, p2.y);
}
复制代码
#[derive(Clone)]
struct Point {
x: i32,
y: i32,
}

fn main() {
let p1 = Point { x: 1, y: 2 };
let p2 = p1.clone();
assert_eq!(p1.x, p2.x);
assert_eq!(p1.y, p2.y);
}
复制代码

此外,这两个trait分别适用于不同的场景。对于那些具有简单按位复制语义的类型(如标量类型和由它们组成的数组和元组),使用
Copy
trait更为方便。而对于那些需要执行深层复制操作的类型(如具有堆分配字段的类型),则应使用
Clone
trait。from刘金,转载请注明原文链接。感谢!

本文首发于公众号:Hunter后端
原文链接:
Django笔记二十七之数据库函数之文本函数

这篇笔记将介绍如何使用数据库函数里的文本函数。

顾名思义,文本函数,就是针对文本字段进行操作的函数,如下是目录汇总:

  1. Concat() —— 合并
  2. Left() —— 从左边开始截取
  3. Length() —— 获取字符串长度
  4. Lower() —— 小写处理
  5. LPad() —— 从左边填充指定字符串
  6. MD5() —— 获取字符串MD5哈希值
  7. Repeat() —— 重复指定字段值
  8. Replace() —— 替换指定内容
  9. Reverse() —— 字段内容反转返回
  10. StrIndex() —— 获取第一个匹配指定字符串的下标
  11. SubStr() —— 字符串截取
  12. Trim() —— 去除给定字段空格

这一篇笔记记录的函数有点多,可以慢慢看,慢慢测试,其中有一些函数是左右都有对应操作的,我这里只介绍一个,另一个对应的函数除了函数名不一样和作用相反外,用法都是一样的。

我们这次用到的是 Author 这个 model:

class Author(models.Model):
    name = models.CharField(max_length=200)
    email = models.EmailField(null=True, default=None)
    age = models.IntegerField(null=True, blank=True)
    alias = models.CharField(max_length=50, null=True, blank=True)
    goes_by = models.CharField(max_length=50, null=True, blank=True)

1、Concat() —— 合并

Concat() 函数,是合并的作用,接受至少两个文本字段或者表达式参数,将其合并成一个字段返回。

示例如下:

from django.db.models.functions import Concat
from django.db.models import CharField, Value
from blog.models import Author

author = Author.objects.create(name="hunter", alias="alias")


author = Author.objects.annotate(
    concat_name=Concat('name', Value('_'), 'alias', output_field=CharField()
    )
).get(id=author.id)


print(author.concat_name)

在示例中,我们将 name 字段和 alias 字段以及 _ 这个字符串用 Value() 函数修饰,传入 Concat(),并通过 output_field 来指定输出字符串类型,将三者合并成一个字符串返回

注意:
如果是将 TextField() 和 CharField() 字段进行合并,那么 output_field 必须是 TextField()

2、Left() —— 从左边开始截取

输入两个参数,一个是指定字段,一个是指定的长度,表示将对该字段从左边开始截取指定长度返回

以下是示例:

from django.db.models.functions import Left

author = Author.objects.annotate(left_three_str=Left('name', 3)).get(id=10)

print(author.left_three_str)

注意一下,我在示例中使用到的 id 的值都是在我自己数据库的 id值,读者在自己测试的时候,需要替换成自己数据的真实 id

同理,django.db.models.functions.Right 是从右边开始截取

3、Length() —— 获取字符串长度

接受文本字段或者表达式作为参数,返回字符串长度

如果字段或者表达式为 null,那么在 Python 里会返回 None

以下是使用示例:

from django.db.models.functions import Length

author = Author.objects.annotate(name_length=Length("name"), email_length=Length("email")).get(id=10)

print(author.name_length)
# 返回数字

print(author.email_length)
# 字段值为 null, 所以返回 None

这里也可以用于搜索,假设说我想搜索 name 字段长度大于3的数据,可以如下实现:

from django.db.models import CharField
from django.db.models.functions import Length

CharField.register_lookup(Length)

authors = Author.objects.filter(name__length__gt=3)
print(authors.count())

或者 annotate() 出一个新字段,然后进行 filter()

Author.objects.annotate(name_length=Length("name")).filter(name_length__gt=3)

4、Lower() —— 小写处理

接受文本字段名或者表达式作为参数传入,然后将其小写化处理返回

以下是使用示例:

from django.db.models.functions import Lower

Author.objects.create(name="HUNTER")

author = Author.objects.annotate(name_lower=Lower("name")).get(id=11)

print(author.name_lower)

跟 Length() 函数一样,也可以使用注册的方式来搜索:

from django.db.models import CharField
from django.db.models.functions import Lower

CharField.register_lookup(Lower)

authors = Author.objects.filter(name__lower="hunter")
print(authors.values("name"))

同理,大写化的函数为 django.db.models.functions.Upper()

5、LPad() —— 从左边填充指定字符串

LPad() 意思为从左边填充指定字符串,接受三个参数:

第一个参数为字段名或表达式

第二个参数为需要填充到的长度,参数名为 length,需要指定值

第三个参数名为 fill_text,值为填充的内容,默认为空字符串

假设我们需要将
abc
填充到 name 字段,需要填充到 10 个字符长度

那么如果 name 的原始值为
hunter
,结果则会是
abcahunter

如果需要填充的值短了,那么就会重复填充,如果长了,就会被截取填充,在刚刚的例子里,第二次填充的时候,再重复一次 abc 则超出 10个长度的限制,所以 abc 被截取了。

以下是使用示例:

from django.db.models.functions import LPad
from django.db.models import Value

Author.objects.create(name="HUNTER")

author = Author.objects.annotate(
    name_1=LPad('name', 4, fill_text=Value('abc')),
    name_2=LPad('name', 8, fill_text=Value('abc')),
    name_3=LPad('name', 16, fill_text=Value('abc'))
).get(id=11)


print(author.name_1)
# HUNT

print(author.name_2)
# abHUNTER

print(author.name_3)
# abcabcabcaHUNTER

更新操作

我们还可以利用 LPad() 函数来对字段进行更新操作

Author.objects.filter(id=11).update(name=LPad('name', 10, Value('abv')))
author = Author.objects.get(id=11)
print(author.name)

这段代码的含义为,将 name 字段原有值的左边填充
abc
字符串填充到10个字符长度后更新到 name 字段

同理,还有一个从右边开始填充的函数 RPad(),也是同样的用法

6、MD5() —— 获取字符串MD5哈希值

接受单个文本字段或者表达式作为参数,返回字符串的 MD5 哈希值

from django.db.models.functions import MD5

author = Author.objects.annotate(name_md5=MD5('name')).get(id=11)

print(author.name_md5)

7、Repeat() —— 重复指定字段值

Repeat(expression, number)
接受字段参数,和重复的次数,返回字段内容重复 number 遍之后的数据

from django.db.models.functions import Repeat

Author.objects.create(name="Python")
# id = 13

author = Author.objects.annotate(repeat_name=Repeat("name", 3)).get(id=13)
print(author.repeat_name)

# 打印出的值为:PythonPythonPython

更新字段数据

将 id=13 的数据的 name 字段重复三遍之后更新到该字段:

Author.objects.filter(id=13).update(name=Repeat("name", 3))

8、Replace() —— 替换指定内容

Replace(expression, text, replacement=Value(''))
替换,即将 expression 字段的值的所有内容为 text 的替换成 replacement 的内容,replacement 默认为空字符串

在下面的例子中,我们将
name
字段中所有的
Ma
字符串更新为
Je

from django.db.models.functions import Replace
from django.db.models import Value

Author.objects.create(name="Match-Mary")
# id = 14

Author.objects.filter(id=14).update(name=Replace('name', Value('Ma'), Value('Je')))

author = Author.objects.get(id=14)

print(author.name)
# Jetch-Jery

9、Reverse() —— 字段内容反转返回

接受字段或者表达式为参数,将原字段内容倒序后返回

from django.db.models.functions import Reverse

author = Author.objects.annotate(reverse_name=Reverse('name')).get(id=11)
print(author.reverse_name)

10、StrIndex() —— 获取第一个匹配指定字符串的下标

接受两个参数,一个参数为字段名,第二个参数为需要匹配的子串

如果子串在字段中被匹配上了,将会返回第一个匹配上的子串的下标

注意1
:匹配上的下标是从1开始计数的,如果没有匹配上,那就回返回0

注意2
:这个匹配的过程是忽略大小写的

from django.db.models.functions import StrIndex
from django.db.models import Value

author = Author.objects.create(name="thIs is a Test")

author = Author.objects.annotate(
    is_index=StrIndex("name", Value("is")),
    test_index=StrIndex("name", Value("test")),
    xx_index=StrIndex("name", Value("xx"))
).get(id=author.id)


print(author.is_index)
# 3,is 字符串匹配忽略大小写,下标从1开始,所以是3

print(author.test_index)
# 11 

print(author.xx_index)
# 0 找不到对应的字符串,所以返回 0,可以根据 0 这个标志位来判断字段中是否包含某个特定字符

而这个操作我们可以用来筛选字段中是否包含某个特定字符串的数据,根据返回的结果是否为 0 来判断:

authors = Author.objects.annotate(ter_index=StrIndex("name", Value("ter"))).filter(ter_index__gt=0)
print(authors.count())

11、SubStr() —— 字符串截取

SunStr(expression, pos, length=None)

这是一个字符串截取的函数,给定一个字段名,和开始的下标(
下标从1开始计数
),和需要计数的长度

表示将某字段,从指定下标开始,截取指定长度的字符串


from django.db.models.functions import Substr

# 将 name 字段 从 第二个字符开始往后截取三个长度的字符
author = Author.objects.annotate(name_sub_str=Substr('name', 2, 3)).get(id=12)

print(author.name_sub_str)

可以用于直接更新:

Author.objects.filter(id=12).update(name=Substr('name', 2, 3))

12、Trim() —— 去除给定字段空格

去除空格给定字段左右两边的空格

Author.objects.create(name=" test trim ")  # id = 15

from django.db.models.functions import Trim

author = Author.objects.annotate(trim_name=Trim("name")).get(id=15)

print(author.trim_name)

也可以直接用于更新:

Author.objects.filter(id=15).update(name=Trim("name"))

同理,还有去除左边空格的函数 LTrim() 和 去除右边空格的函数 RTrim()

以上就是本篇笔记全部内容,下一篇将会是比较重要也比较长的一篇笔记,将会对 Django 系统操作的数据库优化做一次汇总。

如果想获取更多后端相关文章,可扫码关注阅读:

image

前言

最近项目上有一个使用事务相对复杂的业务场景报错了。在绝大多数情况下,都是风平浪静,没有问题。其实内在暗流涌动,在有些异常情况下就会报错,这种偶然性的问题很有可能就会在暴露到生产上造成事故,那究竟是怎么回事呢?

问题描述

我们用一个简单的例子模拟下,大家也可以看看下面这段代码输出的结果是什么。

  1. 在类
    SecondTransactionService
    定义一个简单接口
    transaction2
    ,插入一个用户,同时必然会抛出错误
@Override
@Transactional(rollbackFor = Exception.class)
public void transaction2() {
    System.out.println("do transaction2.....");
    User user = new User("tx2", "666666", 18);
    // 插入一个用户
    userService.insertUser(user);
    // 跑错了
    throw new RuntimeException();
}
  1. 在另外一个类
    FirstTransactionService
    定义一个接口
    transaction1
    ,它调用
    transaction2
    方法,同时做了
    try catch
    处理
@Override
@Transactional(rollbackFor = Exception.class)
public void transaction1() {
    System.out.println("do transaction1 .......");
    try {
        // 调用另外一个事务,try catch住
        secondTransactionService.transaction2();
    } catch (Exception e) {
        e.printStackTrace();
    }

    // 插入当前用户tx1
    User user = new User("tx1", "666666", 18);
    userService.insertUser(user);
}
  1. 定义一个
    controller
    ,调用
    transaction1
    方法
@GetMapping("/testNestedTx")
public String testNestedTx() {
    firstTransactionService.transaction1();
    return "success";
}

大家觉得调用这个
http
接口,最终数据库插入的是几条数据呢?

问题结果

正确答案是数据库插入了0条数据。

同时控制台也报错了,报错原因是:
org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only

是否和你预想的一样呢?你知道是为什么吗?

原因追溯

其实原因很简单,我们都知道,一个事务要么全成功提交事务,要么失败全部回滚。如果出现在一个事务中部分SQL要回滚,部分SQL要提交,这不就主打的一个”前后矛盾,精神分裂“吗?

controller.testNestedTx() 
  || 
  / 
FirstTransactionService.transaction1()   REQUIRED隔离级别
       || 
       || 
       || 捕获异常,提交事务,出错啦
       / || 
FirstTransactionService.transaction2()   REQUIRED隔离级别
       || || 
       || 抛出异常,标记事务为rollback only
       =======================
  1. 事务的隔离级别为
    REQUIRED
    ,那么发现没有事务开启一个事务操作,有的话,就合并到这个事务中,所以
    transaction1()

    transaction2()
    是在同一个事务中。
  2. transaction2()
    抛出异常,那么事务会被标记为
    rollback only
    , 源码如下所示:

  1. transaction1()
    由于
    try catch
    异常,正常运行,想必就要可以提交事务了,在提交事务的时候,会检查
    rollback
    标记,如果是true, 这时候就会抛出上面的异常了。源码如下图所示:


这下,是不是很清楚知道报错的原因了,那想想该怎么处理呢?

解决之道

知道了根本原因之后,是不是解决的方案就很明朗了,我们可以通过调整事务的传播方式分拆多个事务管理,或者让一个事务"前后一致",做一个诚信的好事务。


  • try catch
    放到内层事务中,也就是
    transaction2()
    方法中,这样内层事务会跟着外部事务进行提交或者回滚。
@Override
    @Transactional(rollbackFor = Exception.class)
    public void transaction2() {
        try {
            System.out.println("do transaction2.....");
            User user = new User("tx2", "666666", 18);
            userService.insertUser2(user);
            throw new RuntimeException();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
  • 如果希望内层事务抛出异常时中断程序执行,直接在外层事务的
    catch
    代码块中抛出
    e
    ,这样同一个事务就都会回滚。
  • 如果希望内层事务回滚,但不影响外层事务提交,需要将内层事务的传播方式指定为
    PROPAGATION_NESTED

    PROPAGATION_NESTED
    基于数据库
    savepoint
    实现的嵌套事务,外层事务的提交和回滚能够控制嵌内层事务,而内层事务报错时,可以返回原始
    savepoint
    ,外层事务可以继续提交。

事务的传播机制

前面提到了事务的传播机制,我们再看都有哪几种。

  • PROPAGATION_REQUIRED
    :加入到当前事务中,如果当前没有事务,就新建一个事务。这是最常见的选择,也是Spring中默认采用的方式。
  • PROPAGATION_SUPPORTS
    :支持当前事务,如果当前没有事务,就以非事务方式执行。
  • PROPAGATION_MANDATORY
    :支持当前事务,如果当前没有事务,就抛出异常。
  • PROPAGATION_REQUIRES_NEW
    :新建一个事务,如果当前存在事务,把当前事务挂起。
  • PROPAGATION_NOT_SUPPORTED
    :以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
  • PROPAGATION_NEVER
    : 以非事务方式执行,如果当前存在事务,则抛出异常。
  • PROPAGATION_NESTED
    :如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则进行与
    PROPAGATION_REQUIRED
    类似的操作。

如何理解
PROPAGATION_NESTED
的传播机制呢,和
PROPAGATION_REQUIRES_NEW
又有什么区别呢?我们用一个例子说明白。

  • 定义
    serviceA.methodA()

    PROPAGATION_REQUIRED
    修饰;
  • 定义
    serviceB.methodB()以
    表格中三种方式修饰;
  • methodA
    中调用
    methodB
    ;

总结

在我的项目中之所以会报“
rollback-only
”异常的根本原因是代码风格不一致的原因。外层事务对错误的处理方式是返回true或false来告诉上游执行结果,而内层事务是通过抛出异常来告诉上游(这里指外层事务)执行结果,这种差异就导致了“
rollback-only
”异常。大家也可以去review自己项目中的代码,是不是也偷偷犯下同样的错误了。

欢迎关注个人公众号【JAVA旭阳】交流学习

前言

在Java中,基本数据类型与其对应的封装类之间可以进行自动转换,这种特性称为自动装箱(autoboxing)和自动拆箱(unboxing)。自动装箱和自动拆箱使得我们在使用基本数据类型时更加方便,同时也提高了代码的可读性和健壮性。本文将详细介绍Java中的自动装箱和自动拆箱机制。

基本数据类型和封装类

在Java中,基本数据类型包括byte、short、int、long、float、double、char和boolean等8种。而封装类(wrapper classes)则是对应上述基本数据类型的类,例如Byte、Short、Integer、Long、Float、Double、Character和Boolean。

基本数据类型和封装类之间可以进行相互转换,通过new关键字或者valueOf()方法可以将基本数据类型转换为封装类,通过xxxValue()方法可以将封装类转换为基本数据类型。

int a = 10;
Integer b = new Integer(a); // 将int类型的a转换为Integer类型的对象b
int c = b.intValue(); // 将Integer类型的b转换为int类型的c

自动装箱

自动装箱是指将一个基本数据类型的值赋给对应的封装类对象时,编译器会自动地将基本类型转换为封装类对象。例如:

Integer a = 10; // 自动装箱,将int类型的10赋给Integer类型的a

在这个例子中,编译器会自动地将整型字面值10转换为Integer类型的对象。

自动拆箱

自动拆箱是指将一个封装类对象赋给对应的基本数据类型时,编译器会自动地将封装类对象转换为基本数据类型。例如:

Integer a = 10;
int b = a; // 自动拆箱,将Integer类型的a转换为int类型的b

在这个例子中,编译器会自动地将Integer类型的对象a转换为整型。

自动装箱和自动拆箱的性能问题

虽然自动装箱和自动拆箱非常便利,但是它们也可能带来性能问题。因为自动装箱和自动拆箱都需要创建新的对象或者进行对象的拆解,所以频繁使用自动装箱和自动拆箱可能会产生大量的临时对象,增加垃圾回收的压力,从而影响程序的性能。

因此,在编写Java代码时,应该尽量避免频繁使用自动装箱和自动拆箱,可以通过手动装箱和拆箱的方式来提高程序的性能。例如:

int a = 10;
Integer b = Integer.valueOf(a); // 手动装箱,将int类型的a转换为Integer类型的对象
int c = b.intValue(); // 手动拆箱,将Integer类型的b转换为int类型的c

总结

本文介绍了Java中自动装箱和自动拆箱的机制,以及它们的性能问题。在实际编写Java代码时,应该尽量避免使用过多的自动装箱和自动拆箱,提高程序的性能和健壮性。

实践环境

Odoo 14.0-20221212 (Community Edition)

需求描述

如下图(非实际项目界面截图,仅用于介绍本文主题),打开记录详情页(form视图),点击某个按钮(图中的"选取ffers"按钮),弹出一个向导(wizard)界面,并将详情页中内联tree视图("Offers" Tab页)的列表记录展示到向导界面,且要支持复选框,用于选取目标记录,然执行目标操作。

详情页所属模型
EstateProperty

class EstateProperty(models.Model):
    _name = 'estate.property'
    _description = 'estate property table'
    # ... 略
    offer_ids = fields.One2many("estate.property.offer", "property_id", string="PropertyOffer")

    def action_do_something(self, args):
        # do something 
        print(args)

Offers
Tab页Tree列表所属模型
EstatePropertyOffer

class EstatePropertyOffer(models.Model):
    _name = 'estate.property.offer'
    _description = 'estate property offer'
    
    # ... 略
    property_id = fields.Many2one('estate.property', required=True)

代码实现

代码组织结构

为了更好的介绍本文主题,下文给出了项目文件大致组织结构(为了让大家看得更清楚,仅保留关键文件)

odoo14          
├─custom
│  ├─estate
│  │  │  __init__.py
│  │  │  __manifest__.py
│  │  │          
│  │  ├─models
│  │  │  estate_property.py
│  │  │  estate_property_offer.py
│  │  │  __init__.py
│  │  │          
│  │  ├─security
│  │  │      ir.model.access.csv
│  │  │      
│  │  ├─static
│  │  │  │      
│  │  │  └─src
│  │  │      │          
│  │  │      └─js
│  │  │              list_renderer.js
│  │  │              
│  │  ├─views
│  │  │      estate_property_offer_views.xml
│  │  │      estate_property_views.xml
│  │  │      webclient_templates.xml     
│  │  │          
│  │  └─wizards
│  │        demo_wizard.py
│  │        demo_wizard_views.xml
│  │        __init__.py
│  │          
├─odoo
│  │  api.py
│  │  exceptions.py
│  │  ...略
│  │  __init__.py
│  │  
│  ├─addons
│  │  │  __init__.py
│  ...略
...略       

wizard简介

wizard(向导)通过动态表单描述与用户(或对话框)的交互会话。向导只是一个继承
TransientModel
而非
model
的模型。
TransientModel
类扩展
Model
并重用其所有现有机制,具有以下特殊性:

  • wizard记录不是永久的;它们在一定时间后自动从数据库中删除。这就是为什么它们被称为瞬态(
    transient
    )。

  • wizard可以通过关系字段(
    many2one

    many2many
    )引用常规记录或wizard记录,但常规记录不能通过
    many2one
    字段引用wizard记录

详细代码

注意:为了更清楚的表达本文主题,代码文件中部分代码已略去

wizard实现

odoo14\custom\estate\wizards\demo_wizard.py
实现版本1
#!/usr/bin/env python
# -*- coding:utf-8 -*-

import logging
from odoo import models,fields,api
from odoo.exceptions import UserError

_logger = logging.getLogger(__name__)

class DemoWizard(models.TransientModel):
    _name = 'demo.wizard'
    _description = 'demo wizard'

    property_id = fields.Many2one('estate.property', string='property')
    offer_ids = fields.One2many(related='property_id.offer_ids')

    def action_confirm(self):
        '''选中记录后,点击确认按钮,执行的操作'''

        #### 根据需要对获取的数据做相应处理
        # ... 获取数据,代码略(假设获取的数据存放在 data 变量中)
     
        record_ids = []
        for id, value_dict in data.items():
            record_ids.append(value_dict.get('data', {}).get('id'))
        if not record_ids: 
            raise UserError('请选择记录')

        self.property_id.action_do_something(record_ids)                
        return True      
    

    @api.model
    def action_select_records_via_checkbox(self, args):
        '''通过wizard窗口界面复选框选取记录时触发的操作
        @params: args 为字典
        '''
        # ...存储收到的数据(假设仅存储data部分的数据),代码略
        
        return True # 注意,执行成功则需要配合前端实现,返回True

    @api.model
    def default_get(self, fields_list):
        '''获取wizard 窗口界面默认值,包括记录列表 #因为使用了@api.model修饰符,self为空记录集,所以不能通过self.fieldName = value 的方式赋值'''

        res = super(DemoWizard, self).default_get(fields_list)
        record_ids = self.env.context.get('active_ids') # 获取当前记录ID列表(当前记录详情页所属记录ID列表) # self.env.context.get('active_id') # 获取当前记录ID

        property = self.env['estate.property'].browse(record_ids)
        res['property_id'] = property.id

        offer_ids = property.offer_ids.mapped('id')
        res['offer_ids'] = [(6, 0, offer_ids)]
        return res

说明:

  • 注意,不能使用类属性来接收数据,因为类属性供所有对象共享,会相互影响,数据错乱。

  • action_select_records_via_checkbox
    函数接收的
    args
    参数,其类型为字典,形如以下,其中
    f412cde5-1e5b-408c-8fc0-1841b9f9e4de
    为UUID,供web端使用,用于区分不同页面操作的数据,
    'estate.property.offer_3'
    为供web端使用的记录ID,
    'data'
    键值代表记录的数据,其
    id
    键值代表记录在数据库中的主键id,
    context
    键值代表记录的上下文。
    arg
    数据格式为:

    {'uuid':{'recordID1':{'data': {}, 'context':{}}, 'recordID2': {'data': {}, 'context':{}}}}
    

    {'f412cde5-1e5b-408c-8fc0-1841b9f9e4de': {'estate.property.offer_3': {'data': {'price': 30000, 'partner_id': {'context': {}, 'count': 0, 'data': {'display_name': 'Azure Interior, Brandon Freeman', 'id': 26}, 'domain': [], 'fields': {'display_name': {'type': 'char'}, 'id': {'type': 'integer'}}, 'id': 'res.partner_4', 'limit': 1, 'model': 'res.partner', 'offset': -1, 'ref': 26, 'res_ids': [], 'specialData': {}, 'type': 'record', 'res_id': 26}, 'validity': 7, 'date_deadline': '2022-12-30', 'status': 'Accepted', 'id': 21}, 'context': {'lang': 'en_US', 'tz': 'Europe/Brussels', 'uid': 2, 'allowed_company_ids': [1], 'params': {'action': 85, 'cids': 1, 'id': 41, 'menu_id': 70, 'model': 'estate.property', 'view_type': 'form'}, 'active_model': 'estate.property', 'active_id': 41, 'active_ids': [41], 'property_pk_id': 41}}}}
    
实现版本2
#!/usr/bin/env python
# -*- coding:utf-8 -*-

import uuid
import logging
from odoo import models, fields, api
from odoo.exceptions import UserError, ValidationError, MissingError

_logger = logging.getLogger(__name__)

class DemoWizard(models.TransientModel):
    _name = 'demo.wizard'
    _description = 'demo wizard'

    property_id = fields.Many2one('estate.property', string='property')
    property_pk_id = fields.Integer(related='property_id.id') # 用于action_confirm中获取property
    offer_ids = fields.One2many(related='property_id.offer_ids')

    @api.model
    def action_confirm(self, data:dict): 
        '''选中记录后,点击确认按钮,执行的操作'''

        #### 根据需要对获取的数据做相应处理
        record_ids = []
        for id, value_dict in data.items():
            record_ids.append(value_dict.get('data', {}).get('id'))
        if not record_ids:
            raise UserError('请选择记录')
            
        property_pk_id = None
        for id, value_dict in data.items():
            property_pk_id = value_dict.get('context', {}).get('property_pk_id')
            break

        if not property_pk_id:
            raise ValidationError('do something fail')
            
        property = self.env['estate.property'].browse([property_pk_id]) # 注意,,所以,这里不能再通过self.property_id获取了
        if property.exists():
            property.action_do_something(record_ids)
        else:
            raise MissingError('do something fail:当前property记录(id=%s)不存在' % property_pk_id)
        return True

    
    @api.model
    def default_get(self, fields_list):
        '''获取wizard 窗口界面默认值,包括记录列表'''

        res = super(DemoWizard, self).default_get(fields_list)
        record_ids = self.env.context.get('active_ids')
        
        property = self.env['estate.property'].browse(record_ids)
        res['property_id'] = property.id
        res['property_pk_id'] = property.id

        offer_ids = property.offer_ids.mapped('id')
        res['offer_ids'] = [(6, 0, offer_ids)]
        return res
odoo14\custom\estate\wizards\__init__.py
#!/usr/bin/env python
# -*- coding:utf-8 -*-

from . import demo_wizard
odoo14\custom\estate\__init__.py
#!/usr/bin/env python
# -*- coding:utf-8 -*-

from . import models
from . import wizards
odoo14\custom\estate\wizards\demo_wizard_views.xml
实现版本1

对应
demo_wizard.py
实现版本1

<?xml version="1.0" encoding="UTF-8"?>
<odoo>
    <data>
        <record id="demo_wizard_view_form" model="ir.ui.view">
            <field name="name">demo.wizard.form</field>
            <field name="model">demo.wizard</field>
            <field name="arch" type="xml">
                <form>
                    <field name="offer_ids">
                        <tree hasCheckBoxes="true" modelName="demo.wizard" modelMethod="action_select_records_via_checkbox" jsMethodOnModelMethodDone="enableActionConfirmButton()" jsMethodOnToggleCheckbox="disableActionConfirmButton()">
                            <field name="price" string="Price"/>
                            <field name="partner_id" string="partner ID"/>
                            <field name="validity" string="Validity(days)"/>
                            <field name="date_deadline" string="Deadline"/>
                            <button name="action_accept_offer" string=""  type="object" icon="fa-check" attrs="{'invisible': [('status', 'in', ['Accepted','Refused'])]}"/>
                            <button name="action_refuse_offer" string=""  type="object" icon="fa-times" attrs="{'invisible': [('status', 'in', ['Accepted','Refused'])]}"/>
                            <field name="status" string="Status"/>
                        </tree>
                    </field>
                    <footer>
                        <button name="action_confirm" type="object" string="确认(do something you want)" class="oe_highlight"/>
                        <button string="取消" class="oe_link" special="cancel"/>
                    </footer>
                </form>
            </field>
        </record>
        
        <record id="action_demo_wizard" model="ir.actions.act_window">
            <field name="name">选取offers</field>
            <field name="res_model">demo.wizard</field>
            <field name="type">ir.actions.act_window</field>
            <field name="view_mode">form</field>
            <field name="target">new</field>            
        </record>
    </data>
</odoo>

说明:

<tree hasCheckBoxes="true" modelName="demo.wizard" modelMethod="action_select_records_via_checkbox" jsMethodOnModelMethodDone="enableActionConfirmButton()" jsMethodOnToggleCheckbox="disableActionConfirmButton()">
  • hasCheckBoxes
    设置
    "true"
    ,则显示复选框。以下属性皆在
    hasCheckBoxes

    "true"
    的情况下起作用。
  • modelName
    点击列表复选框时,需要访问的模型名称,需要配合
    modelMethod
    方法使用,缺一不可。可选
  • modelMethod
    点击列表复选框时,需要调用的模型方法,通过该方法收集列表勾选记录的数据。可选。
  • jsMethodOnModelMethodDone
    定义
    modelMethod
    方法执行完成后,需要调用的javascript方法(
    注意,包括参数,如果没有参数则写成
    ()
    ,形如
    jsMethod()

    )。可选。
  • jsMethodOnToggleCheckbox
    定义点击列表复选框时需要调用的javascript方法,比
    modelMethod
    优先执行(
    注意,包括参数,如果没有参数则写成
    ()
    ,形如
    jsMethod()

    )。可选。

以上参数同下文
saveSelectionsToSessionStorage
参数可同时共存

如果需要将action绑定到指定模型指定视图的Action,可以在
ir.actions.act_window
定义中添加
binding_model_id

binding_view_types
字段,如下:

        <record id="action_demo_wizard" model="ir.actions.act_window">
            <field name="name">选取offers</field>
            <field name="res_model">demo.wizard</field>
            <field name="type">ir.actions.act_window</field>
            <field name="view_mode">form</field>
            <field name="target">new</field>            
            <!-- 添加Action菜单 -->
            <field name="binding_model_id" ref="estate.model_estate_property"/>
            <field name="binding_view_types">form</field>
        </record>

效果如下

参考连接:
https://www.odoo.com/documentation/14.0/zh_CN/developer/reference/addons/actions.html

实现版本2

对应
demo_wizard.py
实现版本2

<?xml version="1.0" encoding="UTF-8"?>
<odoo>
    <data>
        <record id="demo_wizard_view_form" model="ir.ui.view">
            <field name="name">demo.wizard.form</field>
            <field name="model">demo.wizard</field>
            <field name="arch" type="xml">
                <form>
                    <field name="property_pk_id" invisible="1"/>
                    <field name="offer_ids" context="{'property_pk_id': property_pk_id}">
                        <tree string="List" hasCheckBoxes="true" saveSelectionsToSessionStorage="true">
                            <field name="price" string="Price"/>
                            <field name="partner_id" string="partner ID"/>
                            <field name="validity" string="Validity(days)"/>
                            <field name="date_deadline" string="Deadline"/>
                            <button name="action_accept_offer" string=""  type="object" icon="fa-check" attrs="{'invisible': [('status', 'in', ['Accepted','Refused'])]}"/>
                            <button name="action_refuse_offer" string=""  type="object" icon="fa-times" attrs="{'invisible': [('status', 'in', ['Accepted','Refused'])]}"/>
                            <field name="status" string="Status"/>
                        </tree>
                    </field>
                    <footer>
                        <button name="action_confirm" onclick="do_confirm_action('demo.wizard','action_confirm')"  string="确认(do something you want)" class="oe_highlight"/>
                        <button string="取消" class="oe_link" special="cancel"/>
                    </footer>
                </form>
            </field>
        </record>
        
        <record id="action_demo_wizard" model="ir.actions.act_window">
            <field name="name">选取offers</field>
            <field name="res_model">demo.wizard</field>
            <field name="type">ir.actions.act_window</field>
            <field name="view_mode">form</field>
            <field name="target">new</field>            
        </record>
    </data>
</odoo>

说明:

  • saveSelectionsToSessionStorage

    "true"
    则表示点击复选框时,将当前选取的记录存到浏览器
    sessionStorage
    中,可选
odoo14\custom\estate\security\ir.model.access.csv
id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink
# ...略
access_demo_wizard_model,access_demo_wizard_model,model_demo_wizard,base.group_user,1,1,1,1

注意:
wizard
模型也是需要添加模型访问权限配置的

复选框及勾选数据获取实现

大致思路通过继承
web.ListRenderer
实现自定义ListRenderer,进而实现复选框展示及勾选数据获取。

odoo14\custom\estate\static\src\js\list_renderer.js

注意:之所以将
uuid
函数定义在
list_renderer.js
中,是为了避免因为js顺序加载问题,可能导致加载
list_renderer.js
时找不到
uuid
函数定义问题。

function uuid() {
	var s = [];
	var hexDigits = "0123456789abcdef";
	for (var i = 0; i < 36; i++) {
		s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
	}
	s[14] = "4"; // bits 12-15 of the time_hi_and_version field to 0010
	s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1); // bits 6-7 of the clock_seq_hi_and_reserved to 01
	s[8] = s[13] = s[18] = s[23] = "-";

	var uuid = s.join("");
	return uuid;
}

odoo.define('estate.ListRenderer', function (require) {
    "use strict";

 	var ListRenderer = require('web.ListRenderer');
	ListRenderer = ListRenderer.extend({
	    init: function (parent, state, params) {
		    this._super.apply(this, arguments);
		    this.hasCheckBoxes = false;
			if ('hasCheckBoxes' in params.arch.attrs && params.arch.attrs['hasCheckBoxes']) {
                this.objectID = uuid();
                $(this).attr('id', this.objectID);

			    this.hasCheckBoxes = true;
			    this.hasSelectors = true;
			    this.records = {}; // 存放当前界面记录
			    this.recordsSelected = {}; // 存放选取的记录
			    this.modelName = undefined; // 定义点击列表复选框时需要访问的模型
			    this.modelMethod = undefined; // 定义点击列表复选框时需要调用的模型方法
			    this.jsMethodOnModelMethodDone = undefined; // 定义modelMethod方法执行完成后,需要调用的javascript方法
			    this.jsMethodOnToggleCheckbox = undefined; // 定义点击列表复选框时需要调用的javascript方法,比modelMethod优先执行


			    if ('modelName' in params.arch.attrs && params.arch.attrs['modelName']) {
			        this.modelName = params.arch.attrs['modelName'];
			    }
			    if ('modelMethod' in params.arch.attrs && params.arch.attrs['modelMethod']) {
			        this.modelMethod = params.arch.attrs['modelMethod'];
			    }
			    if ('jsMethodOnModelMethodDone' in params.arch.attrs && params.arch.attrs['jsMethodOnModelMethodDone']){
			        this.jsMethodOnModelMethodDone = params.arch.attrs['jsMethodOnModelMethodDone'];
			    }

			    if ('jsMethodOnToggleCheckbox' in params.arch.attrs && params.arch.attrs['jsMethodOnToggleCheckbox']) {
			        this.jsMethodOnToggleCheckbox = params.arch.attrs['jsMethodOnToggleCheckbox'];
			    }
                
                if ('saveSelectionsToSessionStorage' in params.arch.attrs && params.arch.attrs['saveSelectionsToSessionStorage']) {
			        this.saveSelectionsToSessionStorage = params.arch.attrs['saveSelectionsToSessionStorage'];
			    }
            }
		},
//		_onToggleSelection: function (ev) {
            // 点击列表表头的全选/取消全选复选框时会调用该函数
//		    this._super.apply(this, arguments);
//        },
        _onToggleCheckbox: function (ev) {
            if (this.hasCheckBoxes) {
                var classOfEvTarget = $(ev.target).attr('class');
                /* cstom-control-input 刚好点中复选框input,
                custom-control custom-checkbox 刚好点中复选框input的父元素div
                o_list_record_selector 点击到复选框外上述div的父元素*/                
                if (['custom-control custom-checkbox', 'custom-control-input', 'o_list_record_selector'].includes(classOfEvTarget)){
                    if (this.jsMethodOnToggleCheckbox) {
                        eval(this.jsMethodOnToggleCheckbox)
                    }

                    var id = $(ev.currentTarget).closest('tr').data('id'); // 'custom-control-input' == classOfEvTarget
                    var checked = !this.$(ev.currentTarget).find('input').prop('checked') // 获取复选框是否框选 'custom-control-input' != classOfEvTarget
                    if ('custom-control-input' ==  classOfEvTarget) {
                        checked = this.$(ev.currentTarget).find('input').prop('checked')
                    }
                    
                    if (id == undefined) {
                        if (checked == true) { // 全选
                            this.recordsSelected = JSON.parse(JSON.stringify(this.records));
                        } else { // 取消全选
                            this.recordsSelected = {};
                        }
                    } else {
                        if (checked == true) { // 勾选单条记录
                            this.recordsSelected[id] = this.records[id];
                        } else { // 取消勾选单条记录
                            delete this.recordsSelected[id];
                        }
                    }

                    if (this.saveSelectionsToSessionStorage) {
                        window.sessionStorage[this.objectID] = JSON.stringify(this.recordsSelected);
                    }
                    
                    // 通过rpc请求模型方法,用于传输界面勾选的记录数据
                    if (this.modelName && this.modelMethod) {
                        self = this;
                        this._rpc({
                                model: this.modelName,
                                method: this.modelMethod,
                                args: [this.recordsSelected],
                            }).then(function (res) {
                                if (self.jsMethodOnModelMethodDone) {
                                    eval(self.jsMethodOnModelMethodDone);
                                }
                            });
                    }
                }
            }

            this._super.apply(this, arguments);

        },
        _renderRow: function (record) {
            // 打开列表页时会渲染行,此时存储渲染的记录
            if (this.hasCheckBoxes) {
                this.records[record.id] = {'data': record.data, 'context': record.context};
            }
            return this._super.apply(this, arguments);
        }

	});

odoo.__DEBUG__['services']['web.ListRenderer'] = ListRenderer; //覆盖原有的ListRender服务
});

实践过程中,有尝试过以下实现方案,视图通过指定相同服务ID
web.ListRenderer
来覆盖框架自带的
web.ListRenderer
定义,这种实现方案只能在非
Debug
模式下正常工作,且会导致无法开启
Debug
模式,
odoo.define
实现中会对服务是否重复定义做判断,如果重复定义则会抛出JavaScript异常。

odoo.define('web.ListRenderer', function (require) {
    "use strict";
    //...略,同上述代码
    // odoo.__DEBUG__['services']['web.ListRenderer'] = ListRenderer; 
    return ListRenderer;
});

笔者后面发现,可以使用
include
替代
extend
方法修改现有的
web.ListRenderer
,如下

odoo.define('estate.ListRenderer', function (require) {
    "use strict";

 	var ListRenderer = require('web.ListRenderer');
	ListRenderer = ListRenderer.include({//...略,同上述代码});
    
    // odoo.__DEBUG__['services']['web.ListRenderer'] = ListRenderer;  //不需要添加这行代码了
});
odoo14\custom\estate\static\src\js\demo_wizard_views.js
实现版本1


demo_wizard_views.xml
实现版本1使用

function disableActionConfirmButton(){ // 禁用按钮
    $("button[name='action_confirm']").attr("disabled", true);
}

function enableActionConfirmButton(){ // 启用按钮
    $("button[name='action_confirm']").attr("disabled", false);
}

这里的设计是,
执行复选框操作时,先禁用按钮,不允许执行确认操作,因为执行复选框触发的请求可能没那么快执行完成,前端数据可能没完全传递给后端,此时去执行操作,可能会导致预期之外的结果。所以,等请求完成再启用按钮。

实现版本2


demo_wizard_views.xml
实现版本2使用

function do_confirm_action(modelName, modelMethod, context){
    $("button[name='action_confirm']").attr("disabled", true); // 点击按钮后,禁用按钮状态,比较重复点击导致重复发送请求    
    var wizard_dialog = $(event.currentTarget.offsetParent.parentElement.parentElement);
    var dataUUID = $(event.currentTarget.parentElement.parentElement.parentElement.parentElement).find('div.o_list_view').prop('id');
    var rpc = odoo.__DEBUG__.services['web.rpc'];
    rpc.query({
        model: modelName,
        method: modelMethod,
        args: [JSON.parse(window.sessionStorage.getItem(dataUUID) || '{}')]
    }).then(function (res)         if (res == true) {
            wizard_dialog.css('display', 'none'); // 隐藏对话框
            window.sessionStorage.removeItem(dataUUID);
        } else {
            $("button[name='action_confirm']").attr("disabled", false);
        }
    }).catch(function (err) {
        $("button[name='action_confirm']").attr("disabled", false);
    });
}
odoo14\odoo\addons\base\rng\tree_view.rng

可选操作。如果希望
hasCheckBoxes

modelName

modelMethod
等也可作用于非内联tree视图,则需要编辑该文件,添加
hasCheckBoxes

modelName

modelMethod
等属性,否则,更新应用的时候会报错。

<?xml version="1.0" encoding="UTF-8"?>
<rng:grammar xmlns:rng="http://relaxng.org/ns/structure/1.0"
             xmlns:a="http://relaxng.org/ns/annotation/1.0"
             datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes">
    <!-- ...此处内容已省略 -->
    <rng:define name="tree">
        <rng:element name="tree">
            <!-- ...此处内容已省略 -->
            <rng:optional><rng:attribute name="decoration-warning"/></rng:optional>
            <rng:optional><rng:attribute name="banner_route"/></rng:optional>
            <rng:optional><rng:attribute name="sample"/></rng:optional>
            <!--在此处添加新属性>
            <rng:optional><rng:attribute name="hasCheckBoxes"/></rng:optional>
            <rng:optional><rng:attribute name="modelName"/></rng:optional>
            <rng:optional><rng:attribute name="modelMethod"/></rng:optional>
            <rng:optional><rng:attribute name="jsMethodOnModelMethodDone"/></rng:optional>
            <rng:optional><rng:attribute name="jsMethodOnToggleCheckbox"/></rng:optional>
            <rng:optional><rng:attribute name="saveSelectionsToSessionStorage"/></rng:optional>
            <!-- ...此处内容已省略 -->
        </rng:element>
    </rng:define>
    <!-- ...此处内容已省略 -->
</rng:grammar>
odoo14\custom\estate\views\webclient_templates.xml

用于加载自定义js

<?xml version="1.0" encoding="utf-8"?>
<odoo>
    <template id="assets_common" inherit_id="web.assets_common" name="Backend Assets (used in backend interface)">
         <xpath expr="//script[last()]" position="after">
             <script type="text/javascript" src="/estate/static/src/js/list_renderer.js"></script>
             <script type="text/javascript" src="/estate/static/src/js/demo_wizard_views.js"></script>
         </xpath>
    </template>
</odoo>
odoo14\custom\estate\__manifest__.py

加载自定义模板文件,进而实现自定义js文件的加载

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

记录详情页视图实现

odoo14\custom\estate\views\estate_property_views.xml
<?xml version="1.0"?>
<odoo>
    <!--...略-->
    <record id="estate_property_view_form" model="ir.ui.view">
        <field name="name">estate.property.form</field>
        <field name="model">estate.property</field>
        <field name="arch" type="xml">
            <form string="estate property form">
                <header>
                     <button name="%(action_demo_wizard)d"
                                type="action"
                                string="选取offers" class="oe_highlight"/>
                    <!--...略-->
                </header>
                <sheet>
                    <!--...略-->                    
                    <notebook>
                        <!--...略-->                        
                        <page string="Offers">
                            <field name="offer_ids" attrs="{'readonly': [('state', 'in', ['Offer Accepted','Sold','Canceled'])]}"/>
                        </page>
                        <!--...略-->     
                    </notebook>
                </sheet>
            </form>
        </field>
    </record>    
</odoo>

说明:
class="oe_highlight"
设置按钮高亮显示

参考连接

https://blog.csdn.net/CBGCampus/article/details/128196983