ORM增强,增删改查语法糖
注意:调用业务实现的方法不能用this.xxx,否则扩展模型无法正确的调用方法。
规范:模型里使用this去操作,业务类才使用ModelUtils或DbUtils
解决痛点:
- 优化完善数据操作方法、重载提高效率
- 完善常用方法强类型输入
- super差异与影响扩展问题
- 统一规范写法。
- 详细操作的文档
获取模型
返回RecordSet
- ModelUtils.getModel(clazz): 根据模型类获取模型
-
ModelUtils.getModel(modelName):根据模型名称获取模型
- this.getModel():获取当前模型
- this.getModel(modelName): 指定名字获取模型
- this.getModel(clazz):
新增数据
- this.create(): 插入一条数据,相当于call("create")
- this.create(data): 插入一条数据
-
this.create(dataLis): 插入多条数据
- this.superCreate(): 执行父模型插入一条数据,相当于callSuper("create")
- this.superCreate(data): 执行父模型插入一条数据,相当于callSuper("create")
-
this.superCreate(dataLis): 执行父模型插入多条数据,相当于callSuper("create")
- ModelUtils.getModel(modelName).create(data): 获取模型并根据数据集合插入
- ModelUtils.getModel(modelName).create(dataLis): 获取模型并根据数据集合插入
-
ModelUtils.getModel(clazz).create(dataLis): 获取模型并根据数据集合插入
- ModelUtils.getModel(clazz).superCreate(): 获取模型的父模型插入一条数据,相当于callSuper("create")
-
删除数据
- this.delete(): 删除当前模型
- this.delete(id): 根据id删除
- this.delete(ids): 根据id集合删除
- this.delete(recordSet): 根据数据集合删除
-
this.delete(filter): 根据条件过滤删除
- this.superDelete(): 执行父模型删除当前模型,相当于callSuper("delete")
- this.superDelete(id): 执行父模型,根据id删除,相当于callSuper("delete")
- this.superDelete(ids): 执行父模型,根据ids删除,相当于callSuper("delete")
- this.superDelete(recordSet): 执行父模型,根据数据集合删除,相当于callSuper("delete")
-
this.superDelete(filter): 执行父模型,根据条件过滤删除,相当于callSuper("delete")
- ModelUtils.getModel(modelName).delete(id): 获取模型并根据id删除
- ModelUtils.getModel(clazz).delete(ids): 获取模型并根据ids删除
-
ModelUtils.getModel(clazz).delete(filter): 获取模型并根据filter条件删除
-
ModelUtils.getModel(clazz).superDelete(filter): 获取模型的父模型根据filter条件删除
- DbUtils.delete(id,clazz): 并根据id删除
- DbUtils.delete(modelName,id): 并根据id删除
- DbUtils.delete(modelName,ids): 并根据id删除
- DbUtils.delete(modelName,recordSet): 并根据id删除
-
DbUtils.delete(modelName,filter): 并根据filter条件删除
- DbUtils.superDelete(modelName,filter): 模型的父模型根据filter条件删除
修改数据
- this.update(data): 根据当前模型更新指定列数据
- this.update(id,data): 根据id更新指定数据
- this.update(id,data,ignoreNulls): 根据id更新指定数据,是否忽略值为null的字段
- this.update(ids,data): 根据id集合更新指定数据
- this.update(ids,data,ignoreNulls): 根据id集合更新指定数据,是否忽略值为null的字段
- this.update(filter):根据过滤条件更新当前模型数据
- this.update(filter,data):根据过滤条件更新数据
-
this.update(filter,data,,ignoreNulls):根据过滤条件更新数据,是否忽略值为null的字段
- this.superUpdate(data): 执行父模型,根据当前模型更新指定列数据
- this.superUpdate(id,data): 执行父模型,根据id更新指定数据
- this.superUpdate(id,data,ignoreNulls): 执行父模型,根据id更新指定数据,是否忽略值为null的字段
- this.superUpdate(ids,data):
- this.superUpdate(ids,data,ignoreNulls):
- this.superUpdate(filter):
- this.superUpdate(filter,data):
-
this.superUpdate(filter,data,ignoreNulls):
- ModelUtils.getModel(modelName).update(data): 获取模型并根据id更新数据
-
ModelUtils.getModel(modelName).update(filter,data): 获取模型并根据id更新数据
- ModelUtils.getModel(modelName).superUpdate(filter,data):
查询数据
- this.searchOneById(id):根据主键查询数据。
- this.searchOneById(id,properties):根据主键查询数据,并指定查询字段。
- this.searchOneByFilter(filter):根据条件列表查询数据
- this.searchOneByFilter(filter,properties):根据条件列表查询数据,并指定查询字段。
- this.searchListByIds(ids):根据主键列表查询数据。
- this.searchListByIds(ids,properties):根据主键列表查询数据,并指定查询字段。
-
this.searchListByIds(ids):根据主键列表查询数据。
- this.searchAll(): 查询所有数据
- this.searchAll(properties): 查询所有数据,并指定查询字段
- this.search(filter): 根据过滤条件查询数据
- this.search(filter,properties): 根据过滤条件查询数据,并指定查询字段
- this.search(filter,limit,offset,order): 根据过滤条件查询数据,并分页
- this.search(filter,properties,limit,offset): 根据过滤条件查询数据,并分页
-
this.search(filter,properties,limit,offset,order): 根据过滤条件查询数据,并分页
- this.superSearchOneById(id):
- this.superSearchOneById(id,properties):
- this.superSearchOneByFilter(filter):根据条件列表查询数据
- this.superSearchOneByFilter(filter,properties):根据条件列表查询数据,并指定查询字段。
- this.superSearchListByIds(ids):
- this.superSearchListByIds(ids,properties):
- this.superSearchAll():
- this.superSearchAll(properties):
- this.superSearch(filter):
- this.superSearch(filter,properties):
- this.superSearch(filter,limit,offset,order):
- this.superSearch(filter,properties,limit,offset):
-
this.superSearch(filter,properties,limit,offset,order):
- ModelUtils.getModel(modelName).searchOneById(id):指定模型根据主键查询数据。
- ModelUtils.getModel(modelName).superSearchOneById(id):
说明:返回Optional<>
查找数据
- this.findOneById(id):根据主键查询数据。
- this.findByIds(ids):根据主键集查询数据。
-
this.find(filter):根据过滤条件查询数据。
- this.superFindOneById(id)
- this.superFindByIds(id)
-
this.superFind(filter)
- ModelUtils.getModel(modelName).findOneById(id):
查询数据数量
- this.count(): 查询数据数量
- this.count(filter): 根据过滤条件查询数据
- this.exist(filter): 要据条件判断数据是否存在
- this.exist(id): 要据条件判断数据是否存在
-
this.exist(ids): 要据条件判断数据是否存在
- this.superCount():
- this.superCount(filter):
- this.superExist(filter):
链式操作
查询示例
创建一条数据
DbChain.app("aps").model("Account")
.setId(RowKey.AUTO)
.set("user_name", "zhang san")
.set("age", 18)
.set("birthday", new Date())
.create();
更新数据
DbChain.app("aps").model("Account")
.setId(RowKey.AUTO)
.set("user_name", "zhang san")
.set("age", 18)
.set("birthday", new Date())
.update();
查询一条数据
DbChain.app("aps").model("Account")
.select(ARTICLE.ALL_COLUMNS)
.where(ARTICLE.ID.ge(100))
.limit(1)
.list();
DbChain.app("aps").model("Account")
.select(ARTICLE.ALL_COLUMNS)
.where(ARTICLE.ID.ge(100))
.one();
查询多条数据
DbChain.app("aps").model("Account")
.select(properties)
.where(ARTICLE.ID.ge(100))
.or(ACCOUNT.USER_NAME.like("michael"))
.list();
常用扩展方法
- one():获取一条数据
- list():获取多条数据
- page():分页查询
- obj():当 SQL 查询只返回 1 列数据的时候,且只有 1 条数据时,可以使用此方法
- objList():当 SQL 查询只返回 1 列数据的时候,可以使用此方法
- count():查询数据条数
- exists():是否存在,判断 count 是否大于 0
group by
DbChain.app("aps").model("Account")
.select(properties)
.groupBy("name");
having
DbChain.app("aps").model("Account")
.select(properties)
.having(ACCOUNT.AGE.between(18,25));
orderBy
DbChain.app("aps").model("Account")
.select(properties)
.orderBy("age","asc", "name","desc");
SQL 函数支持
DbChain.app("aps").model("Account")
.select(properties)
.where(ARTICLE.ID.ge(100))
.or(ACCOUNT.USER_NAME.like("michael"))
.list();
批量操作
提供了许多批量操作(批量插入、批量更新等)的方法,当有多个方法的时候,会经常导致误用的情况。
createBatch 方法
List<Account> accounts = ....
this.createBatch(accounts);
组装一个如下的 SQL:
insert into tb_account(id,nickname, .....) values
(100,"miachel100", ....),
(101,"miachel101", ....),
(102,"miachel102", ....),
(103,"miachel103", ....),
(104,"miachel104", ....),
(105,"miachel105", ....);
条件过滤
where
Filter filter=Filter.create()
.and(ACCOUNT.ID.ge(100))
.or(ACCOUNT.USER_NAME.like("michael"))
WHERE id >= ?
AND user_name LIKE ?
and (…) or (…)
Filter filter=Filter.create()
.where(ACCOUNT.ID.ge(100))
.and(ACCOUNT.SEX.eq(1).or(ACCOUNT.SEX.eq(2)))
.or(ACCOUNT.AGE.in(18,19,20).and(ACCOUNT.USER_NAME.like("michael")));
其查询生成的 Sql 如下:
WHERE id >= ?
AND (sex = ? OR sex = ? )
OR (age IN (?,?,?) AND user_name LIKE ? )
动态条件
Filter filter=Filter.create()
.where(ACCOUNT.ID.ge(100).when(flag)) //flag为false,忽略该条件
.and(ACCOUNT.USER_NAME.like("michael"));
使用重载方法
Filter filter=Filter.create()
.and(ACCOUNT.USER_NAME.like("michael", flag)); //flag为false,忽略该条件
Filter filter=Filter.create()
.where(ACCOUNT.USER_NAME.like(name, StringUtil::isNotBlank)); //name为空字符串,忽略该条件
模型事件
模型事件是指在进行模型的查询和写入操作的时候触发的操作行为。
模型支持如下事件:
事件 | 描述 | 事件方法名 |
---|---|---|
afterRead | 查询后 | onAfterRead |
beforeInsert | 新增前 | onBeforeInsert |
afterInsert | 新增后 | onAfterInsert |
beforeUpdate | 更新前 | onBeforeUpdate |
afterUpdate | 更新后 | onAfterUpdate |
beforeDelete | 删除前 | onBeforeDelete |
afterDelete | 删除后 | onAfterDelete |
beforeWrite | 写入前 | onBeforeWrite |
afterWrite | 写入后 | onAfterWrite |
写入事件
onBeforeWrite和onAfterWrite事件会在新增操作和更新操作都会触发.
注意:模型的新增或更新是自动判断的.
具体的触发顺序:
// 执行 onBeforeWrite
// 如果事件没有返回`false`,那么继续执行
// 执行新增或更新操作(onBeforeInsert/onAfterInsert或onBeforeUpdate/onAfterUpdate)
// 新增或更新执行成功
// 执行 onAfterWrite
模型事件定义
最简单的方式是在模型类里面定义静态方法来定义模型的相关事件响应。
参考资料:https://doc.thinkphp.cn/v8_0/model_event.htmlclass User extends BaseModel<User> { public boolean onBeforeUpdate(RecordSet rs, Map<String, Object> value) { return false; } public boolean onAfterDelete(RecordSet rs) { return false; } }
模型监听器
onInsert
用于监听 Entity 实体类数据被新增到数据库,我们可以在实体类被新增时做一些前置操作。比如:
- 数据填充。
- 数据修改。
示例代码如下:
//配置 onInsert = MyInsertListener.class
@Model(tableName = "tb_account", onInsert = MyInsertListener.class)
public class Account extends BaseModel<Account>{
}
public class MyInsertListener implements InsertListener {
@Override
public void onInsert(RecordSet entity,List<Map<String, Object>> valuesList) {
Account account = (Account)entity;
//设置 account 被新增时的一些默认数据
account.setInsertTime(new Date());
account.setInsertUserId("...");
}
}
onUpdate
使用方式同 onInsert 一致,用于在数据被更新的时候,设置一些默认数据。
onSet
onSet 可以用于配置:查询数据 数据模型 (或者 模型 列表、分页等)时,对 模型 的属性设置的监听,可以用于如下的场景。
- 场景1:字段权限,不同的用户或者角色可以查询不同的字段内容。
- 场景2:字典回写,模型中定义许多业务字段,当数据库字段赋值时,主动去设置业务字段。
- 场景3:一对多,一对一查询,模型中定义关联实体,在监听到字段赋值时,主动去查询关联表赋值。
- 场景4:字段加密,监听到内容被赋值时,对内容进行加密处理。
- 场景5:字段脱敏,出字段内容进行脱敏处理
示例代码如下:
//配置 onSet = MySetListener.class
@Model(tableName = "tb_account", onSet = MySetListener.class)
public class Account extends BaseModel<Account> {
}
public class MySetListener implements SetListener {
@Override
public Object onSet(RecordSet rs, String property, Object value){
//场景1:模型中可能定义某个业务值
// 当监听到某个字段被赋值了,这里可以主动去给另外的其他字段赋值
//场景2:内容转换和二次加工,对 value 值进行修改后返回
return value;
}
}
全局设置
//为模型 Account1 和 Account2 注册 insertListner
config.registerInsertListener(insertListener, Account1.class, Account2.class);
//为模型 Account1 和 Account2 注册 updateListener
config.registerUpdateListener(updateListener, Account1.class, Account2.class);
//为模型 Account1 和 Account2 注册 setListener
config.registerSetListener(setListener, Account1.class, Account2.class);
字段加密
字段加密,指的是数据库在存入了明文内容,但是当我们进行查询时,返回的内容为加密内容,而非明文内容。
step 1: 为实体类编写一个 set 监听器(SetListener)
public class MySetListener implements SetListener {
@Override
public Object onSet(RecordSet rs, String property, Object value){
if ( property=="password" && value != null){
//对字段内容进行加密
value = encrypt(value);
}
return value;
}
}
step 2: 为实体类配置 onSet 监听
//配置 onSet = MySetListener.class
@Model(tableName = "tb_account", onSet = MySetListener.class)
public class Account extends BaseModel<Account> {
}
SQL 审计
SQL 审计是一项非常重要的工作,是企业数据安全体系的重要组成部分,通过 SQL 审计功能为数据库请求进行全程记录,为事后追溯溯源提供了一手的信息,同时可以通过可以对恶意访问及时警告管理员,为防护策略优化提供数据支撑。
开启审计功能
SQL 审计功能,默认是关闭的,若开启审计功能,需添加如下配置。
默认情况下,Sql的审计消息(日志)只会输出到控制台,如下所示:config.setAuditEnable(true)
>>>>>>Sql Audit: {platform='mbm', app='aps', url='null', user='null', userIp='null', hostIp='192.168.3.24', query='SELECT * FROM `tb_account` WHERE `id` = ?', queryParams=[1], queryTime=1679991024523, elapsedTime=1}
>>>>>>Sql Audit: {platform='mbm', app='aps', url='null', user='null', userIp='null', hostIp='192.168.3.24', query='SELECT * FROM `tb_account` WHERE `id` = ?', queryParams=[1], queryTime=1679991024854, elapsedTime=3}
>>>>>>Sql Audit: {platform='mbm', app='aps', url='null', user='null', userIp='null', hostIp='192.168.3.24', query='SELECT * FROM `tb_account` WHERE `id` = ?', queryParams=[1], queryTime=1679991025100, elapsedTime=2}
消息包含了如下内容:
- platform:平台,或者是运行的应用
- app:应用
- url:执行这个 SQL 涉及的 URL 地址
- user:执行这个 SQL 涉及的 平台用户
- userIp:执行这个 SQL 的平台用户 IP 地址
- hostIp:执行这个 SQL 的服务器 IP 地址
- query:SQL 内容
- queryParams:SQL 参数
- queryTime:SQL 执行的时间点(当前时间)
- elapsedTime:SQL 执行的消耗时间(毫秒)
- metas:其他扩展元信息
提示 :
通过以上的消息内容可知:每个 SQL 的执行,都包含了:哪个访问用户、哪个 IP 地址访问,访问的是哪个 URL 地址,这个 SQL 的参数是什么,执行的时间是什么,执行 花费了多少时间等等。这样,通过SQL 审计功能,我们能全盘了解到每个 SQL 的执行情况。
自定义 SQL 审计内容
内置了一个名为 MessageFactory 的接口,我们只需实现该接口,并为 AuditManager 配置新的 MessageFactory 即可,如下所示:
public class MyMessageFactory implements MessageFactory {
@Override
public AuditMessage create() {
AuditMessage message = new AuditMessage();
// 在这里
// 设置 message 的基础内容,包括 platform、module、url、user、userIp、hostIp 内容
// 剩下的 query、queryParams、queryCount、queryTime、elapsedTime 为 mybatis-flex 设置
return message;
}
}
自定义 SQL 收集器
负责把收集的 SQL 审计日志发送到指定位置
public class MyMessageReporter implements MessageReporter {
public void sendMessages(List<AuditMessage> messages) {
//在这里把 messages 审计日志发送到指定位置
//比如
// 1、通过 http 协议发送到指定服务器
// 2、通过日志工具发送到日志平台
// 3、通过 Kafka 等 MQ 发送到指定平台
}
}
SQL 日志
数据脱敏
对某些敏感信息通过脱敏规则进行数据的变形, 实现敏感隐私数据的可靠保护。在涉及客户安全数据或者一些商业性敏感数据的情况下,在不违反系统规则条件下,对真实数据进行改造并提供使用, 如身份证号、手机号、卡号、客户号等个人信息都需要进行数据脱敏。
@DataMask
提供了 @DataMask() 注解,以及内置的9种脱敏规则,帮助开发者方便的进行数据脱敏。例如:
@Model("tb_account")
public class Account {
@DataMask(Masks.CHINESE_NAME)
private String userName;
}
以上的示例中,使用了 CHINESE_NAME 的脱敏规则,其主要用于处理 "中文名字" 的场景。当我们查询到 userName 为 张三丰 的时候,其内容自动被处理成 张**。
方便开发者直接使用:
- 手机号脱敏
- 固定电话脱敏
- 身份证号脱敏
- 车牌号脱敏
- 地址脱敏
- 邮件脱敏
- 密码脱敏
- 银行卡号脱敏
自定义脱敏规则
内置的9种脱敏规则无法满足要求时,我们还可以自定义脱敏规则
@Model("tb_account")
public class Account {
@DataMask("自定义规则名称")
private String userName;
}
取消脱敏处理
在某些场景下,程序希望查询得到的数据是原始数据,而非脱敏数据。比如要去查询用户的手机号,然后给用户发送短信。
乐观锁
用于当有多个用户(或者多场景)去同时修改同一条数据的时候,只允许有一个修改成功。
实现原理
使用一个字段,用于记录数据的版本,当修改数据的时候,会去检测当前版本是否是正在修改的版本,同时修改成功后会把 版本号 + 1。
UPDATE account SET nickname = ?, version = version + 1
WHERE id = ? AND version = ?
示例代码:
@Model("tb_account")
public class Account {
@Version(version = true)
private Long version;
}
DB+模型Row操作
参考资料:https://mybatis-flex.com/zh/base/db-row.html
使用 Row 插入数据
Row account = new Row();
account.set("id",100);
account.set(ACCOUNT.USER_NAME,"Michael");
DbChain.app("aps").model("Account").create(account);
根据主键查询数据
Row row = DbChain.app("aps").model("Account").selectOneById(1);
根据条件查询数据
Row row = DbChain.app("aps").model("Account")
.where(ACCOUNT.AGE.ge(18))
.selectOne();
Row 可以直接转换为实体类,且性能要求极高
Account account = row.toEntity(Account.class);
事务管理
事务传播属性
//若存在当前事务,则加入当前事务,若不存在当前事务,则创建新的事务
REQUIRED(0),
//若存在当前事务,则加入当前事务,若不存在当前事务,则已非事务的方式运行
SUPPORTS(1),
//若存在当前事务,则加入当前事务,若不存在当前事务,则抛出异常
MANDATORY(2),
//始终以新事务的方式运行,若存在当前事务,则暂停(挂起)当前事务。
REQUIRES_NEW(3),
//以非事务的方式运行,若存在当前事务,则暂停(挂起)当前事务。
NOT_SUPPORTED(4),
//以非事务的方式运行,若存在当前事务,则抛出异常。
NEVER(5),
//暂时不支持
NESTED(6),
嵌套事务
支持无限极嵌套,默认情况下,嵌套事务直接的关系是:REQUIRED(若存在当前事务,则加入当前事务,若不存在当前事务,则创建新的事务)。
属性生成器
数据源加密
数据源加密指的是我们对数据库的链接信息进行加密,支持加密的内容有
- 数据源 URL
- 数据源用户名
- 数据源用户密码
通过数据源加密功能,我们可以有效的保证数据库安全,减少数据盗用风险。
多数据源
读写分离
读写分离的功能,要求当前环境必须是多个数据库(也可理解为多个数据源),其原理是: 让主数据库(master)处理事务性操作,比如:增、删、改(INSERT、DELETE、UPDATE),而从数据库(slave)处理查询(SELECT)操作。