JDBC
JDBC 持久化模块针对关系型数据库(RDBMS)数据存取的一套简单解决方案,主要关注数据存取的效率、易用性、稳定和透明,其具备以下功能特征:
- 基于 JDBC 框架 API 进行轻量封装,结构简单、便于开发、调试和维护;
- 优化批量数据更新、标准化结果集、预编译 SQL 语句处理;
- 支持单实体 ORM 操作,无需编写 SQL 语句;
- 提供脚手架工具,快速生成数据实体类,支持链式调用;
- 支持通过存储器注解自定义 SQL 语句或从配置文件中动态加载 SQL 并自动执行;
- 支持结 果集与值对象的自动装配,支持自定义装配规则;
- 支持多数据源,默认支持 C3P0、DBCP、Druid、HikariCP、JNDI 连接池配置,支持数据源扩展;
- 支持多种数据库(如:Oracle、MySQL、SQLServer、SQLite、H2、PostgreSQL 等);
- 支持面向对象的数据库查询封装,有助于减少或降低程序编译期错误;
- 支持数据库事务嵌套;
- 支持数据库视图和存储过程;
Maven包依赖
<dependency>
<groupId>net.ymate.platform</groupId>
<artifactId>ymate-platform-persistence-jdbc</artifactId>
<version>2.1.4-dev</version>
</dependency>
模块配置
配置文件参数说明
#-------------------------------------
# JDBC持久化模块初始化参数
#-------------------------------------
# 默认数据源名称, 默认值: default
ymp.configs.persistence.jdbc.ds_default_name=
# 数据源列表, 多个数据源名称间用'|'分隔, 默认值: default
ymp.configs.persistence.jdbc.ds_name_list=
# 是否自动连接, 即模块初始化时完成连接动作, 默认值: false
ymp.configs.persistence.jdbc.ds.default.auto_connection=true
# 是否显示执行的SQL语句, 默认值: false
ymp.configs.persistence.jdbc.ds.default.show_sql=true
# 是否开启堆栈跟踪, 默认值: false
ymp.configs.persistence.jdbc.ds.default.stack_traces=true
# 堆栈跟踪层级深度, 默认值: 0(即全部)
ymp.configs.persistence.jdbc.ds.default.stack_trace_depth=
# 堆栈跟踪包名前缀过滤, 默认值: 空
ymp.configs.persistence.jdbc.ds.default.stack_trace_packages=
# 自定义引用标识符, 根据数据库类型进行设置, 默认值: 空
ymp.configs.persistence.jdbc.ds.default.identifier_quote=
# 数据库表前缀名称, 多个前缀名称间用'|'分隔, 默认值: 空
ymp.configs.persistence.jdbc.ds.default.table_prefix=
# 数据源适配器, 可选值为已知适配器名称或自定义适配置类名称, 默认值: default, 目前支持已知适配器[default|dbcp|c3p0|druid|hikaricp|jndi|...]
ymp.configs.persistence.jdbc.ds.default.adapter_class=dbcp
# 数据源适配器配置文件,可选参数,若未设置或设置的文件路径无效将被忽略,默认值为空
ymp.configs.persistence.jdbc.ds.default.config_file=
# 数据库类型, 可选参数, 默认值将通过连接字符串分析获得, 目前支持[mysql|oracle|sqlserver|db2|sqlite|postgresql|hsqldb|h2]
ymp.configs.persistence.jdbc.ds.default.type=
# 数据库方言, 可选参数, 自定义方言将覆盖默认配置
ymp.configs.persistence.jdbc.ds.default.dialect_class=
# 数据库连接驱动, 可选参数, 框架默认将根据数据库类型进行自动匹配
ymp.configs.persistence.jdbc.ds.default.driver_class=
# 数据库连接字符串, 必填参数
ymp.configs.persistence.jdbc.ds.default.connection_url=jdbc:mysql://localhost:3306/db_name?useUnicode=true&useSSL=false&characterEncoding=UTF-8
# 数据库访问用户名称, 必填参数
ymp.configs.persistence.jdbc.ds.default.username=root
# 数据库访问密码, 可选参数, 经过默认密码处理器加密后的admin字符串为wRI2rASW58E
ymp.configs.persistence.jdbc.ds.default.password=wRI2rASW58E
# 数据库访问密码是否已加密, 默认值: false
ymp.configs.persistence.jdbc.ds.default.password_encrypted=true
# 数据库密码处理器, 可选参数, 用于对已加密码数据库访问密码进行解密, 默认值: 空
ymp.configs.persistence.jdbc.ds.default.password_class=
配置注解参数说明
当 JDBC 持久化模块初始化时,若在配置文件中存在数据源相关配置,则基于注解的数据源配置将全部失效。
@DatabaseConf
| 配置项 | 描述 |
|---|---|
| dsDefaultName | 默认数据源名称 |
| value | 数据源配置 |
@DatabaseDataSource
| 配置项 | 描述 |
|---|---|
| name | 数据源名称 |
| connectionUrl | 数据库连接字符串 |
| username | 数据库访问用户名称 |
| password | 数据库访问密码 |
| passwordEncrypted | 数据库访问密码是否已加密 |
| passwordClass | 数据库密码处理器 |
| type | 数据库类型 |
| dialectClass | 数据库方言 |
| adapterClass | 数据源适配器 |
| configFile | 数据源适配器配置文件 |
| driverClass | 数据库默认驱动类名称 |
| autoConnection | 是否自动连接 |
| showSql | 是否显示执行的 SQL 语句 |
| stackTraces | 是否开启堆栈跟踪 |
| stackTraceDepth | 堆栈跟踪层级深度 |
| stackTracePackages | 堆栈跟踪过滤包名前缀集合 |
| tablePrefix | 数据库表前缀名称 |
| identifierQuote | 数据库引用标识符 |
模块事件
事件枚举对象 DatabaseEvent 包括以下事件类型:
| 事务类型 | 说明 |
|---|---|
| QUERY_AFTER | 执行查询操作后 |
| INSERT_AFTER | 执行插入操作后 |
| INSERT_IF_NOT_EXIST_AFTER | 执行插入(如果记录不存在)操作后 |
| UPDATE_AFTER | 执行更新操作后 |
| UPSERT_AFTER | 执行更新插入操作后 |
| REMOVE_AFTER | 执行删除操作后 |
数据源(DataSource)
多数据源连接
JDBC 持久化模块默认支持多数据源配置,下面通过简单的配置来展示如何连接多个数据库:
# 定义两个数据源分别用于连接MySQL和Oracle数据库,同时指定默认数据源为default(即MySQL数据库)
ymp.configs.persistence.jdbc.ds_default_name=default
ymp.configs.persistence.jdbc.ds_name_list=default|oracledb
# 连接到MySQL数据库的数据源配置
ymp.configs.persistence.jdbc.ds.default.connection_url=jdbc:mysql://localhost:3306/mydb
ymp.configs.persistence.jdbc.ds.default.username=root
ymp.configs.persistence.jdbc.ds.default.password=123456
# 连接到Oracle数据库的数据源配置
ymp.configs.persistence.jdbc.ds.oracledb.connection_url=jdbc:oracle:thin:@localhost:1521:ORCL
ymp.configs.persistence.jdbc.ds.oracledb.username=ORCL
ymp.configs.persistence.jdbc.ds.oracledb.password=123456
从上述配置中可以看出,配置不同的数据源时只需要定义数据源名称列表,再根据列表逐一配置即可。
通过注解方式配置多数据源,如下所示:
@DatabaseConf(dsDefaultName = "default", value = {
@DatabaseDataSource(name = "default",
connectionUrl = "jdbc:mysql://localhost:3306/mydb",
username = "root",
password = "123456"),
@DatabaseDataSource(name = "oracledb",
connectionUrl = "jdbc:oracle:thin:@localhost:1521:ORCL",
username = "ORCL",
password = "123456")
})
连接池配置
JDBC 持久化模块提供的数据源类型如下:
| 名称 | 类型 | 描述 |
|---|---|---|
| default | DefaultDataSourceAdapter | 默认数据源适配器,通过 DriverManager 直接连接数据库,建议仅用于测试。 |
| c3p0 | C3P0DataSourceAdapter | 基于 C3P0 连接池的数据源适配器。 |
| dbcp | DBCPDataSourceAdapter | 基于 DBCP 连接池的数据源适配器。 |
| druid | DruidDataSourceAdapter | 基于阿里巴巴开源的 Druid 连接池的数据源适配器。 |
| hikaricp | HikariCPDataSourceAdapter | 基于 HikariCP 连接池的数据源适配器。 |
| jndi | JNDIDataSourceAdapter | 基于 JNDI 的数据源适配器。 |
只需根据实际情况调整对应数据源名称的配置,如:
ymp.configs.persistence.jdbc.ds.default.adapter_class=dbcp
通过注解配置方式,如下所示:
@DatabaseConf(dsDefaultName = "default",
value = {
@DatabaseDataSource(name = "default",
connectionUrl = "jdbc:mysql://localhost:3306/mydb",
username = "root",
password = "123456",
adapterClass = DBCPDataSourceAdapter.class)
})
针对于 dbcp、druid、hikaricp 和 c3p0 连接池的配置文件及内容,请将对应的配置文件(如:dbcp.properties、c3p0.properties等)放置在工程的 classpath 根路径下,若上述配置文件不存在,JDBC 持久化模块在初始化时将自动创建。
另外,dbcp、druid、hikaricp 和 c3p0 连接池支持根据数据源名称进行单独配置(如:dbcp_oracledb.properties,此文件将优先于 dbcp.properties 被加载),其 中 druid 连接池可以兼容 dbcp 连接池的配置文件。
当然,也可以通过 IDatabaseDataSourceAdapter 接口自行实现,框架针对该接口提供了 AbstractDatabaseDataSourceAdapter 抽象类,直接继承即可。
数据库连接持有者(IDatabaseConnectionHolder)
用于记录真正的数据库连接对象(Connection)原始的状态及与数据源对应关系,在 JDBC 持久化模块中获取到的所有连接对象均由数据库连接持有者对象包装,基于数据库连接持有者接口可以进行如下操作:
public class Main {
public static void main(String[] args) throws Exception {
try (IApplication application = YMP.run(args)) {
if (application.isInitialized()) {
// 获取当前容器内JDBC模块实例
IDatabase database = application.getModuleManager().getModule(JDBC.class);
// 获取默认数据源的连接持有者实例,等同于:database.getConnectionHolder("default");
IDatabaseConnectionHolder connectionHolder = database.getDefaultConnectionHolder();
// 获取指定名称的数据源连接持有者实例
connectionHolder = database.getConnectionHolder("oracledb");
// 获取连接对象
Connection connection = connectionHolder.getConnection();
// 获取数据源配置对象
IDatabaseDataSourceConfig dataSourceConfig = connectionHolder.getDataSourceConfig();
// 获取当前数据源适配器对象
IDatabaseDataSourceAdapter dataSourceAdapter = connectionHolder.getDataSourceAdapter();
// 获取当前数据库方言
IDialect dialect = connectionHolder.getDialect();
// 获取当前连接持有者所属JDBC模块实例
IDatabase owner = connectionHolder.getOwner();
}
}
}
}
数据实体(Entity)
数据实体是以对象的形式与数据库表之间的一种映射关系,实体中的属性与表中字段一一对应。
数据实体类包含以下几个部份:
- 基本属性:注解配置属性与字段之间的关系及属性的 Getter 和 Setter 方法。
- FIELDS:字段名常量。
- Builder:基本属性构建器类,支持以链式调用方式为实体属性赋值。
- FieldConditionBuilder:属性条件构建器类,为具体实体属性构建字段查询条件。
实体类与表对应关系示例
假定数据库中的 ym_user 数据表结构如下:
CREATE TABLE `ym_user` (
`id` varchar(32) NOT NULL COMMENT '用户唯一标识',
`username` varchar(32) DEFAULT NULL COMMENT '用户名称',
`nickname` varchar(32) DEFAULT NULL COMMENT '昵称',
`gender` varchar(1) DEFAULT NULL COMMENT '性别',
`age` int(2) unsigned DEFAULT '0' COMMENT '年龄',
`avatar_url` varchar(255) DEFAULT NULL COMMENT '头像URL地址',
`password` varchar(32) DEFAULT NULL COMMENT '登录密码',
`email` varchar(100) DEFAULT NULL COMMENT '电子邮件',
`mobile` varchar(20) DEFAULT NULL COMMENT '手机号码',
`type` smallint(2) unsigned DEFAULT '0' COMMENT '类型',
`status` smallint(2) unsigned DEFAULT '0' COMMENT '状态',
`create_time` bigint(13) NOT NULL COMMENT '注册时间',
`last_modify_time` bigint(13) DEFAULT '0' COMMENT '最后修改时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户信息';
通过实体生成工具自动构建的实体类如下:
@Entity(UserEntity.TABLE_NAME)
@Comment("用户信息")
public class UserEntity extends BaseEntity<UserEntity, String> {
private static final long serialVersionUID = 1L;
@Id
@Property(name = FIELDS.ID, nullable = false, length = 32)
@Comment("用户唯一标识")
@PropertyState(propertyName = FIELDS.ID)
private String id;
@Property(name = FIELDS.USERNAME, length = 32)
@Comment("用户名称")
@PropertyState(propertyName = FIELDS.USERNAME)
private String username;
@Property(name = FIELDS.NICKNAME, length = 32)
@Comment("昵称")
@PropertyState(propertyName = FIELDS.NICKNAME)
private String nickname;
@Property(name = FIELDS.GENDER, length = 1)
@Comment("性别")
@PropertyState(propertyName = FIELDS.GENDER)
private String gender;
@Property(name = FIELDS.AGE, length = 2)
@Comment("年龄")
@PropertyState(propertyName = FIELDS.AGE)
private Integer age;
@Property(name = FIELDS.AVATAR_URL, length = 255)
@Comment("头像URL地址")
@PropertyState(propertyName = FIELDS.AVATAR_URL)
private String avatarUrl;
@Property(name = FIELDS.PASSWORD, length = 32)
@Comment("登录密码")
@PropertyState(propertyName = FIELDS.PASSWORD)
private String password;
@Property(name = FIELDS.EMAIL, length = 100)
@Comment("电子邮件")
@PropertyState(propertyName = FIELDS.EMAIL)
private String email;
@Property(name = FIELDS.MOBILE, length = 20)
@Comment("手机号码")
@PropertyState(propertyName = FIELDS.MOBILE)
private String mobile;
@Property(name = FIELDS.TYPE, unsigned = true, length = 2)
@Default("0")
@Comment("类型")
@PropertyState(propertyName = FIELDS.TYPE)
private Integer type;
@Property(name = FIELDS.STATUS, unsigned = true, length = 2)
@Default("0")
@Comment("状态")
@PropertyState(propertyName = FIELDS.STATUS)
private Integer status;
@Property(name = FIELDS.CREATE_TIME, nullable = false, length = 13)
@Comment("注册时间")
@PropertyState(propertyName = FIELDS.CREATE_TIME)
@Readonly
private Long createTime;
@Property(name = FIELDS.LAST_MODIFY_TIME, length = 13)
@Default("0")
@Comment("最后修改时间")
@PropertyState(propertyName = FIELDS.LAST_MODIFY_TIME)
private Long lastModifyTime;
public UserEntity() {
}
public UserEntity(IDatabase dbOwner) {
super(dbOwner);
}
public UserEntity(String id, Long createTime) {
this.id = id;
this.createTime = createTime;
}
public UserEntity(IDatabase dbOwner, String id, Long createTime) {
super(dbOwner);
this.id = id;
this.createTime = createTime;
}
public UserEntity(String id, String username, String nickname, String gender, Integer age, String avatarUrl, String password, String email, String mobile, Integer type, Integer status, Long createTime, Long lastModifyTime) {
this.id = id;
this.username = username;
this.nickname = nickname;
this.gender = gender;
this.age = age;
this.avatarUrl = avatarUrl;
this.password = password;
this.email = email;
this.mobile = mobile;
this.type = type;
this.status = status;
this.createTime = createTime;
this.lastModifyTime = lastModifyTime;
}
public UserEntity(IDatabase dbOwner, String id, String username, String nickname, String gender, Integer age, String avatarUrl, String password, String email, String mobile, Integer type, Integer status, Long createTime, Long lastModifyTime) {
super(dbOwner);
this.id = id;
this.username = username;
this.nickname = nickname;
this.gender = gender;
this.age = age;
this.avatarUrl = avatarUrl;
this.password = password;
this.email = email;
this.mobile = mobile;
this.type = type;
this.status = status;
this.createTime = createTime;
this.lastModifyTime = lastModifyTime;
}
@Override
public String getId() {
return id;
}
@Override
public void setId(String id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getNickname() {
return nickname;
}
public void setNickname(String nickname) {
this.nickname = nickname;
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public String getAvatarUrl() {
return avatarUrl;
}
public void setAvatarUrl(String avatarUrl) {
this.avatarUrl = avatarUrl;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getMobile() {
return mobile;
}
public void setMobile(String mobile) {
this.mobile = mobile;
}
public Integer getType() {
return type;
}
public void setType(Integer type) {
this.type = type;
}
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
public Long getCreateTime() {
return createTime;
}
public void setCreateTime(Long createTime) {
this.createTime = createTime;
}
public Long getLastModifyTime() {
return lastModifyTime;
}
public void setLastModifyTime(Long lastModifyTime) {
this.lastModifyTime = lastModifyTime;
}
public Builder bind() {
return new Builder(this);
}
public static Builder builder() {
return new Builder();
}
public static Builder builder(IDatabase dbOwner) {
return new Builder(dbOwner);
}
public static class Builder {
private final UserEntity targetEntity;
public Builder() {
targetEntity = new UserEntity();
}
public Builder(IDatabase dbOwner) {
targetEntity = new UserEntity(dbOwner);
}
public Builder(UserEntity targetEntity) {
this.targetEntity = targetEntity;
}
public UserEntity build() {
return targetEntity;
}
public IDatabaseConnectionHolder connectionHolder() {
return targetEntity.getConnectionHolder();
}
public Builder connectionHolder(IDatabaseConnectionHolder connectionHolder) {
targetEntity.setConnectionHolder(connectionHolder);
return this;
}
public IDatabase dbOwner() {
return targetEntity.getDbOwner();
}
public Builder dbOwner(IDatabase dbOwner) {
targetEntity.setDbOwner(dbOwner);
return this;
}
public String dataSourceName() {
return targetEntity.getDataSourceName();
}
public Builder dataSourceName(String dataSourceName) {
targetEntity.setDataSourceName(dataSourceName);
return this;
}
public IShardingable shardingable() {
return targetEntity.getShardingable();
}
public Builder shardingable(IShardingable shardingable) {
targetEntity.setShardingable(shardingable);
return this;
}
public String id() {
return targetEntity.getId();
}
public Builder id(String id) {
targetEntity.setId(id);
return this;
}
public String username() {
return targetEntity.getUsername();
}
public Builder username(String username) {
targetEntity.setUsername(username);
return this;
}
public String nickname() {
return targetEntity.getNickname();
}
public Builder nickname(String nickname) {
targetEntity.setNickname(nickname);
return this;
}
public String gender() {
return targetEntity.getGender();
}
public Builder gender(String gender) {
targetEntity.setGender(gender);
return this;
}
public Integer age() {
return targetEntity.getAge();
}
public Builder age(Integer age) {
targetEntity.setAge(age);
return this;
}
public String avatarUrl() {
return targetEntity.getAvatarUrl();
}
public Builder avatarUrl(String avatarUrl) {
targetEntity.setAvatarUrl(avatarUrl);
return this;
}
public String password() {
return targetEntity.getPassword();
}
public Builder password(String password) {
targetEntity.setPassword(password);
return this;
}
public String email() {
return targetEntity.getEmail();
}
public Builder email(String email) {
targetEntity.setEmail(email);
return this;
}
public String mobile() {
return targetEntity.getMobile();
}
public Builder mobile(String mobile) {
targetEntity.setMobile(mobile);
return this;
}
public Integer type() {
return targetEntity.getType();
}
public Builder type(Integer type) {
targetEntity.setType(type);
return this;
}
public Integer status() {
return targetEntity.getStatus();
}
public Builder status(Integer status) {
targetEntity.setStatus(status);
return this;
}
public Long createTime() {
return targetEntity.getCreateTime();
}
public Builder createTime(Long createTime) {
targetEntity.setCreateTime(createTime);
return this;
}
public Long lastModifyTime() {
return targetEntity.getLastModifyTime();
}
public Builder lastModifyTime(Long lastModifyTime) {
targetEntity.setLastModifyTime(lastModifyTime);
return this;
}
}
public interface FIELDS {
String ID = "id";
String USERNAME = "username";
String NICKNAME = "nickname";
String GENDER = "gender";
String AGE = "age";
String AVATAR_URL = "avatar_url";
String PASSWORD = "password";
String EMAIL = "email";
String MOBILE = "mobile";
String TYPE = "type";
String STATUS = "status";
String CREATE_TIME = "create_time";
String LAST_MODIFY_TIME = "last_modify_time";
}
public static final String TABLE_NAME = "user";
public static FieldConditionBuilder conditionBuilder() {
return new FieldConditionBuilder();
}
public static FieldConditionBuilder conditionBuilder(String prefix) {
return new FieldConditionBuilder(prefix);
}
public static FieldConditionBuilder conditionBuilder(Query<?> query) {
return conditionBuilder(query, null);
}
public static FieldConditionBuilder conditionBuilder(Query<?> query, String prefix) {
return new FieldConditionBuilder(query.owner(), query.dataSourceName(), prefix);
}
public static FieldConditionBuilder conditionBuilder(UserEntity entity) {
return conditionBuilder(entity, null);
}
public static FieldConditionBuilder conditionBuilder(UserEntity entity, String prefix) {
return new FieldConditionBuilder(entity.doGetSafeOwner(), entity.getDataSourceName(), prefix);
}
public static FieldConditionBuilder conditionBuilder(IDatabase owner, String prefix) {
return new FieldConditionBuilder(owner, prefix);
}
public static FieldConditionBuilder conditionBuilder(IDatabase owner, String dataSourceName, String prefix) {
return new FieldConditionBuilder(owner, dataSourceName, prefix);
}
public static class FieldConditionBuilder extends AbstractFieldConditionBuilder {
public FieldConditionBuilder() {
super(null, null, null);
}
public FieldConditionBuilder(String prefix) {
super(null, null, prefix);
}
public FieldConditionBuilder(Query<?> query) {
super(query.owner(), null, null);
}
public FieldConditionBuilder(Query<?> query, String prefix) {
super(query.owner(), query.dataSourceName(), prefix);
}
public FieldConditionBuilder(IDatabase owner) {
super(owner, null, null);
}
public FieldConditionBuilder(IDatabase owner, String prefix) {
super(owner, null, prefix);
}
public FieldConditionBuilder(IDatabase owner, String dataSourceName, String prefix) {
super(owner, dataSourceName, prefix);
}
public FieldCondition id() {
return createFieldCondition(UserEntity.FIELDS.ID);
}
public FieldCondition username() {
return createFieldCondition(UserEntity.FIELDS.USERNAME);
}
public FieldCondition nickname() {
return createFieldCondition(UserEntity.FIELDS.NICKNAME);
}
public FieldCondition gender() {
return createFieldCondition(UserEntity.FIELDS.GENDER);
}
public FieldCondition age() {
return createFieldCondition(UserEntity.FIELDS.AGE);
}
public FieldCondition avatarUrl() {
return createFieldCondition(UserEntity.FIELDS.AVATAR_URL);
}
public FieldCondition password() {
return createFieldCondition(UserEntity.FIELDS.PASSWORD);
}
public FieldCondition email() {
return createFieldCondition(UserEntity.FIELDS.EMAIL);
}
public FieldCondition mobile() {
return createFieldCondition(UserEntity.FIELDS.MOBILE);
}
public FieldCondition type() {
return createFieldCondition(UserEntity.FIELDS.TYPE);
}
public FieldCondition status() {
return createFieldCondition(UserEntity.FIELDS.STATUS);
}
public FieldCondition createTime() {
return createFieldCondition(UserEntity.FIELDS.CREATE_TIME);
}
public FieldCondition lastModifyTime() {
return createFieldCondition(UserEntity.FIELDS.LAST_MODIFY_TIME);
}
}
}
数据实体注解
@Entity
声明一个类为数据实体对象。
| 配置项 | 描述 |
|---|---|
| value | 实体名称(数据库表名称),默认采用当前类名称 |
@Id
声明一个类成员为主键,与 @Property 注解配合使用,无参数。
@Property
声明一个类成员为数据实体属性。
| 配置项 | 描述 |
|---|---|
| name | 实现属性名称,默认采用当前成员名称 |
| autoincrement | 是否为自动增长,默认为 false |
| sequenceName | 序列名称,适用于类似 Oracle 等数据库,配合 autoincrement 参数一同使用 |
| useKeyGenerator | 指定键值生成器名称,默认为空表示不启用(仅当非自动增长且主键值为空时调用)。 目前框架提供了 IKeyGenerator.UUID 键值生成器,其采用 UUID 策略。可通过实现 IKeyGenerator 接口自行实现并通过 SPI 方式向框架注册。 |
| nullable | 允许为空,默认为 true |
| unsigned | 是否为无符号,默认为 false |
| length | 数据长度,默认为 0 表示不限制 |
| decimals | 小数位数,默认为 0 表示无小数 |
| type | 数据类型,默认为 Type.FIELD.UNKNOWN |
示例: 使用自定义主键生成器 custom 为实体类的 id 属性自动赋值。
步骤1: 编写自定义主键生成器,为其命名为 custom。
package net.ymate.platform.examples.persistence.jdbc;
import net.ymate.platform.core.persistence.IKeyGenerator;
import net.ymate.platform.core.persistence.IPersistence;
import net.ymate.platform.core.persistence.base.IEntity;
import net.ymate.platform.core.persistence.base.PropertyMeta;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;
@KeyGenerator(value = "custom")
public class CustomKeyGenerator implements IKeyGenerator {
@Override
public Object generate(IPersistence<?, ?, ?, ?> owner, PropertyMeta propertyMeta, IEntity<?> entity) {
// 判断当前主键属性类型,仅对字符串类型生成
if (propertyMeta.getField().getType().equals(String.class)) {
if (entity instanceof UserEntity) {
String username = ((UserEntity) entity).getUsername();
String mobile = ((UserEntity) entity).getMobile();
if (StringUtils.isNotBlank(username) && StringUtils.isNotBlank(mobile)) {
// 将用户密和密码值联合进行MD5运算
return DigestUtils.md5Hex(username + mobile);
}
throw new IllegalArgumentException("username and mobile can not be empty.");
}
}
return null;
}
}
步骤2: 注册自定义主键生成器
方式一:通过在 META-INF/service/或META-INF/service/internal/ 目录下对应的 SPI 配置文件中增加由 步骤1 创建的自定义主键生成器类名。配置文件名称应为 net.ymate.platform.core.persistence.IKeyGenerator,若不存在请手动创建并追加如下内容:
net.ymate.platform.examples.persistence.jdbc.CustomKeyGenerator
方式二:手动向主键生成器管理器进行注册。
IKeyGenerator.Manager.registerKeyGenerator("custom", CustomKeyGenerator.class);
步骤3: 调整数据实体类的 id 属性 @Property 注解配置:useKeyGenerator = "custom"
@Entity(UserEntity.TABLE_NAME)
@Comment("用户信息")
public class UserEntity extends BaseEntity<UserEntity, String> {
private static final long serialVersionUID = 1L;
@Id
@Property(name = FIELDS.ID, nullable = false, length = 32, useKeyGenerator = "custom")
@Comment("用户唯一标识")
@PropertyState(propertyName = FIELDS.ID)
private String id;
......
}
步骤4: 测试
下面的代码中包含了如何通过捕获 JDBC 模块初始化事件向管理器注册自定义主键生成器,如果与 SPI 方式同时使用会造成重复注册动作,尽管管理器会忽略重复注册的行为,但仍然建议避免重复操作,两种注册方式达到的目的是一样的,但更推荐使用 SPI 方式。
@EnableAutoScan
@EnableBeanProxy
public class Starter implements IApplicationInitializer {
static {
System.setProperty(IApplication.SYSTEM_MAIN_CLASS, Starter.class.getName());
}
private static final Log LOG = LogFactory.getLog(Starter.class);
@Override
public void afterEventInit(IApplication application, Events events) {
events.registerListener(ModuleEvent.class, new IEventListener<ModuleEvent>() {
@Override
public boolean handle(ModuleEvent context) {
if (Objects.equals(JDBC.MODULE_NAME, context.getSource().getName())
&& context.getEventName() == ModuleEvent.EVENT.MODULE_INITIALIZED) {
try {
IKeyGenerator.Manager.registerKeyGenerator("custom", CustomKeyGenerator.class);
} catch (Exception ignored) {
}
}
return false;
}
});
}
public static void main(String[] args) throws Exception {
try (IApplication application = YMP.run(args, new Starter())) {
UserEntity userEntity = UserEntity.builder()
.username("abc")
.mobile("13088888888")
.createTime(System.currentTimeMillis()).build();
LOG.info("Custom UserEntity Id: " + userEntity.save().getId());
}
}
}
@PK
声明一个类为某数据实体的复合主键对象,无参数。
示例:
@PK
public class UserExtPK {
@Property
private String uid;
@Property(name = "wx_id")
private String wxId;
// 省略Get/Set方法...
}
@Entity("user_ext")
public class UserExt {
@Id
private UserExtPK id;
@Property(name = "open_id", nullable = false, length = 32)
private String openId;
// 省略Get/Set方法...
}
@Readonly
声明一个成员为只读属性,数据实体更新时其值将被忽略,与 @Property 注解配合使用,无参数。
示例:
@Entity("demo")
public class Demo {
@Id
@Property
private String id;
@Property(name = "create_time")
@Readonly
private Date createTime;
// 省略Get/Set方法...
}
@Indexes
声明一组数据实体的索引。
| 配置项 | 描述 |
|---|---|
| value | 索引注解 @Index 集合 |
@Index
声明一个数据实体的索引。
| 配置项 | 描述 |
|---|---|
| name | 索引名称 |
| unique | 是否唯一索引,默认为 true |
| fields | 索引字段名称集合 |
示例:
@Indexes({
@Index(name="unique_uname", unique = true, fields = {UserEntity.FIELDS.USERNAME})
})
@Comment
实体或成员属性的注释内容。
@Default
为一个成员属性指定默认值。
| 配置项 | 描述 |
|---|---|
| value | 默认值 |
| ignored | 是否忽略(即该默认值仅用于生成 DDL 语句,主要是为了避免函数名称导致 SQL 执行错误),默认为 false |
自动生成实体类
YMP 框架自 v1.x 开始就支持通过数据库表结构自动生成实体类代码,所以 v2.x 版本不但重构了实体代码生成器,而且更简单好用!
步骤1: 配置数据实体代码生成器所需参数:
#-------------------------------------
# JDBC数据实体代码生成器配置参数
#-------------------------------------
# 是否生成新的BaseEntity类, 默认值: false(即表示使用框架提供的BaseEntity类)
ymp.params.jdbc.use_base_entity=
# 是否使用类名后缀, 不使用和使用的区别如: User->UserModel, 默认值: false
ymp.params.jdbc.use_class_suffix=true
# 实体类名后缀, 默认值: model
ymp.params.jdbc.class_suffix=entity
# 是否采用链式调用模式, 默认值: false
ymp.params.jdbc.use_chain_mode=true
# 为兼容历史数据库保持原表和字段名称的大小写,默认值: false
ymp.params.jdbc.keep_case=
# 自定义表或字段名称过滤器, 默认值: 空
ymp.params.jdbc.named_filter_class=
# 是否添加类成员属性值状态变化注解, 默认值: false
ymp.params.jdbc.use_state_support=true
# 数据库名称, 默认值: 空
ymp.params.jdbc.db_name=mydb
# 数据库用户名称, 默认值: 空
ymp.params.jdbc.db_username=root
# 数据库表名称 前缀, 多个用'|'分隔, 默认值: 空
ymp.params.jdbc.table_prefix=ym_
# 否剔除生成的实体映射表名前缀, 默认值: false
ymp.params.jdbc.remove_table_prefix=true
# 预生成实体的数据表名称列表, 多个用'|'分隔, 默认值: 空(即全部生成)
ymp.params.jdbc.table_list=
# 排除的数据表名称列表, 在此列表内的数据表将不被生成实体, 多个用'|'分隔, 默认值: 空
ymp.params.jdbc.table_exclude_list=
# 需要添加@Readonly注解声明的字段名称列表, 多个用'|'分隔, 默认值: 空
ymp.params.jdbc.readonly_field_list=create_time
# 生成的代码文件输出路径, 默认值: ${root}/src/main/java
ymp.params.jdbc.output_path=
# 生成的代码所属包名称, 默认值: packages
ymp.params.jdbc.package_name=
实际上你可以什么都不用配置(请参看以上配置项说明,根据实际情况进行调整),但使用过程中需要注意以下几点:
-
在多数据源模式下,需要指定具体数据源名称,否则代码生成器使用的是默认数据源;
-
如果使用的 JDBC 驱动是
mysql-connector-java-6.x及以上版本时,则必须配置db_name和db_username参数; -
实体及属性命名过滤器参数
named_filter_class指定的类需要实现INamedFilter接口;插件已提供了中文转拼音的过滤器接口实现类:
net.ymate.maven.plugins.support.ChinesePinyinNamedFilter
步骤2: 添加插件配置,数据实体生成器是以 Maven 插件的形式提供的,需要在工程的 pom.xml 文件添加如下内容:
<plugin>
<groupId>net.ymate.maven.plugins</groupId>
<artifactId>ymate-maven-plugin</artifactId>
<version>1.0.2</version>
</plugin>
插件中默认已经包含 mysql-connector-java-8.0.32 驱动,若需要其它版本或其它类型数据库驱动时,需要在插件中配置相关依赖,如:
<plugin>
<groupId>net.ymate.maven.plugins</groupId>
<artifactId>ymate-maven-plugin</artifactId>
<version>1.0.2</version>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.32</version>
</dependency>
<dependency>
<groupId>com.oracle.database.jdbc</groupId>
<artifactId>ojdbc8</artifactId>
<version>21.7.0.0</version>
</dependency>
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>mssql-jdbc</artifactId>
<version>11.2.0.jre8</version>
</dependency>
</dependencies>
</plugin>
步骤3: 在工程根路径下执行插件命令:
mvn ymate:entity -Doverwrite=true
插件命令参数说明:
| 参数 | 描述 |
|---|---|
| dev | 是否使用开发模式,默认为 false |
| overwrite | 是否覆盖已存在的文件,默认为 false |
| cfgFile | 加载指定的框架初始化配置文件,默认为空 |
| dataSource | 指定数据源名称,默认为 default |
| view | 是否为视图,默认为 false |
| showOnly | 是否仅在控制台输出结构信息(不生成任何文件),默认为 false |
| format | 控制台输出格式,配合 showOnly 使用,可选 值:table markdown csv,默认为 table |
| beanOnly | 是否仅生成 JavaBean(非实体类),默认为 false |
| apidocs | 是否使用 @ApiProperty 文档注解,配合 beanOnly 使用,默认为 false |
通过插件生成的代码默认放置在 src/main/java 路径,当数据库表发生变化时,重新执行插件命令就可以快速更新数据实体对象,是不是很更方便呢,大家可以动手尝试一下!:p
数据实体操作
本小节中使用的数据实体类是指通过实体生成工具自动生成并继承了框架提供的 BaseEntity 抽象类。
针对非工具自动生成或并未继承 BaseEntity 的实体类,通过 EntityWrapper 类包装也可以达到同样的效果:
UserEntity user = new UserEntity();
user.setId(UUIDUtils.UUID());
user.setUsername("suninformation");
// ......
EntityWrapper<UserEntity> wrapper = EntityWrapper.bind(user);
wrapper.saveOrUpdate();
插入(Insert)
UserEntity user = UserEntity.builder()
.id(UUIDUtils.UUID())
.username("suninformation")
.nickname("有理想的鱼")
.password(DigestUtils.md5Hex("123456"))
.email("suninformation@163.com")
.build();
// 执行数据插入
user.save();
// 或者在插入时也可以指定/排除某些字段
user.save(Fields.create(UserEntity.FIELDS.NICKNAME, UserEntity.FIELDS.EMAIL).excluded(true));
插入或更新(Upsert)
@since 2.1.4
saveOrUpdate 方法用于执行插入或更新操作,根据数据库类型采用原子操作实现:
| 数据库类型 | SQL 语法 |
|---|---|
| MySQL | INSERT ... ON DUPLICATE KEY UPDATE ... |
| PostgreSQL | INSERT ... ON CONFLICT ... DO UPDATE SET ... |
| SQLite | INSERT ... ON CONFLICT ... DO UPDATE SET ... |
| 其他数据库 | 回退到先查询后更新/插入的方式(非原子操作) |
UserEntity user = UserEntity.builder()
.id(UUIDUtils.UUID())
.username("suninformation")
.nickname("有理想的鱼")
.password(DigestUtils.md5Hex("123456"))
.email("suninformation@163.com")
.build();
// 插入前判断记录是否已存在,若已存在则执行记录更新操作
user.saveOrUpdate();
// 或者插入前判断记录是否已存在,若已存在则执行记录更新操作时仅更新指定的字段
user.saveOrUpdate(Fields.create(UserEntity.FIELDS.NICKNAME, UserEntity.FIELDS.EMAIL));
插入如果不存在(InsertIfNotExist)
@since 2.1.4
saveIfNotExist 方法用于执行"如果不存在则插入"操作,根据数据库类型采用原子操作实现:
| 数据库类型 | SQL 语法 |
|---|---|
| MySQL | INSERT IGNORE INTO ... |
| PostgreSQL | INSERT INTO ... ON CONFLICT DO NOTHING |
| SQLite | INSERT OR IGNORE INTO ... |
| Oracle/SQLServer/H2/DB2/HSQLDB | MERGE INTO ... WHEN NOT MATCHED THEN INSERT ... |
| 其他数据库 | 回退到先查询后插入的方式(非原子操作) |
UserEntity user = UserEntity.builder()
.id(UUIDUtils.UUID())
.username("suninformation")
.nickname("有理想的鱼")
.password(DigestUtils.md5Hex("123456"))
.email("suninformation@163.com")
.build();
// 如果记录不存在则插入,若记录已存在则返回false
boolean success = user.saveIfNotExist();
// 或者指定/排除某些字段
success = user.saveIfNotExist(Fields.create(UserEntity.FIELDS.NICKNAME, UserEntity.FIELDS.EMAIL).excluded(true));
更新(Update)
方式一:常规更新
UserEntity user = UserEntity.builder()
.id("bc19f5645aa9438089c5e9954e5f1ac5")
.password(DigestUtils.md5Hex("654321"))
.gender("F")
.build();
// 执行记录更新
user.update();
// 或者仅更新指定的字段/排除某些字段
user.update(Fields.create(UserEntity.FIELDS.PASSWORD));
方式二:仅更新值发生变化的字段
// 从数据库中加载记录
UserEntity user = UserEntity.builder()
.id("bc19f5645aa9438089c5e9954e5f1ac5").build().load();
// 获取记录类成员属性状态包装器
EntityStateWrapper<UserEntity> stateWrapper = user.stateWrapper();
// 为字段赋值
stateWrapper.getEntity().bind()
.password(DigestUtils.md5Hex("654321"))
.gender("M");
// 执行更新(将排除掉值未发生变化的字段)
stateWrapper.update();
查询(Find)
方式一:根据记录ID加载
UserEntity user = UserEntity.builder()
.id("bc19f5645aa9438089c5e9954e5f1ac5")
.build();
// 根据记录ID加载全部字段
user = user.load();
// 或者根据记录ID加载指定的字段
user = user.load(Fields.create(UserEntity.FIELDS.USERNAME, UserEntity.FIELDS.NICKNAME));
方式二:根据实体属性设置条件
UserEntity user = UserEntity.builder()
.username("suninformation")
.email("suninformation@163.com")
.build();
// 非空属性之间将使用and条件连接,查询所有符合条件的记录并返回所有字段
IResultSet<UserEntity> users = user.find();
// 或者返回指定的字段
users = user.find(Fields.create(UserEntity.FIELDS.ID, UserEntity.FIELDS.PASSWORD));
// 非空属性之间将使用or条件连接,查询所有符合条件的记录并返回
users = user.matchAny().find();
方式三:自定义属性条件并分页查询
// 构建字段条件:邮件后缀为"@163.com"的记录
FieldCondition cond = UserEntity.conditionBuilder().email().like(Like.create("@163.com").endsWith());
// 构建Where对象,并设置按创建日期降序排列
Where where = Where.create(cond.build()).orderByDesc(UserEntity.FIELDS.CREATE_TIME);
// 执行分页查询第1页且每页10行记录,返回全部字段
IResultSet<UserEntity> users = new UserEntity().find(where, Page.create(1).pageSize(10));
方式四:返回符合条件的第一条记录
UserEntity user = UserEntity.builder()
.username("suninformation")
.password(DigestUtils.md5Hex("654321"))
.build();
// 返回与用户名称和密码匹配的第一条记录
user = user.findFirst();
// 或者返回与用户名称和密码匹配的第一条记录的ID和NICKNAME字段
user = user.findFirst(Fields.create(UserEntity.FIELDS.ID, UserEntity.FIELDS.NICKNAME));
方式五:统计符合条件的记录数
UserEntity user = UserEntity.builder()
.username("suninformation")
.password(DigestUtils.md5Hex("654321"))
.build();
// 返回与用户名称和密码匹配的记录数
long count = user.count();
删除(Delete)
// 根据实体主键删除记录
UserEntity user = UserEntity.builder()
.id("bc19f5645aa9438089c5e9954e5f1ac5")
.build().delete();
// 根据实体属性进行有条件删除
UserEntity user = UserEntity.builder()
.username("suninformation")
.password(DigestUtils.md5Hex("654321"))
.build().delete();
基于数据实体类可以帮助你完成的事情还有很多,上面的示例仅是一部份比较典型的应用,请大家在实际应用中结合源码和 API 文档去尝试,也随时欢迎与您一起沟通、交流经验和建议。
事务(Transaction)
JDBC 持久化模块对数据库事务的处理是基于 YMP 框架的 AOP 特性实现的,任何被应用容器管理的对象都可以通过 @Transaction 注解开启事务。
@Transaction注解仅作用于公有非静态、非抽象且不属于 Object 基类方法上才能开启事务,不支持接口方法。- 当类方法上声明的
@Transaction注解的事务级别为Type.TRANSACTION.NONE时,将判断当前类是否存在@Transaction注解声明并尝试获取其事务级别设置。 - 在同一个线程内的事务将被合并为一个事务,称之为嵌套事务,支持事务的无限层级嵌套,如果每一层嵌套,指定的事务级别有所不同,不同的数据库,可能引发不可预知的错误。 所以嵌套的事务将以最顶层的事务级别为标准,也就是说,如果最顶层的事务级别为
TRANSACTION_READ_COMMITTED, 那么下面所包含的所有事务,无论你指定什么样的事务级别都将视为无效。
@Transaction
声明一个类方法开启数据库事务。
| 配置项 | 描述 |
|---|---|
| value | 事务类型(参考 JDBC 事务类型),默认为 Type.TRANSACTION.READ_COMMITTED |
示例:
public interface IUserService {
UserEntity findUser(String username, String pwd) throws Exception;
boolean login(String username, String pwd) throws Exception;
}
@Bean
public class UserServiceImpl implements IUserService {
@Override
public UserEntity findUser(String username, String pwd) throws Exception {
return JDBC.get().openSession(new IDatabaseSessionExecutor<UserEntity>() {
public UserEntity execute(IDatabaseSession session) throws Exception {
Cond cond = Cond.create()
.eq(UserEntity.FIELDS.USERNAME).param(username)
.and().eq(UserEntity.FIELDS.PASSWORD).param(pwd);
return session.findFirst(EntitySQL.create(UserEntity.class), Where.create(cond));
}
});
}
@Override
@Transaction
public boolean login(String username, String pwd) throws Exception {
UserEntity user = findUser(username, pwd);
if (user != null) {
long now = System.currentTimeMillis();
user.bind().lastModifyTime(now).build()
.update(Fields.create(UserEntity.FIELDS.LAST_MODIFY_TIME));
return true;
}
return false;
}
}
@EnableAutoScan
@EnableBeanProxy
public class Starter {
static {
System.setProperty(IApplication.SYSTEM_MAIN_CLASS, Starter.class.getName());
}
private static final Log LOG = LogFactory.getLog(Starter.class);
public static void main(String[] args) throws Exception {
try (IApplication application = YMP.run(args)) {
IUserService userService = application.getBeanFactory().getBean(IUserService.class);
if (userService.login("suninformation", DigestUtils.md5Hex("123456"))) {
LOG.info("Login succeeded.");
}
}
}
}
事务管理器(ITransaction)
用于管理和执行基于 JDBC 事务相关操作(如:事务的开启、关闭、提交和回滚等)的接口类,该接口采用 SPI 方式加载以方便业务扩展(如:分步式事务实现等),一般情况下,JDBC 模块所提供的默认实现,基本可以满足常规的业务场景。
手动开启事务
手动开启事务操作需要借助 Transactions 类完成,此类提供了两种事务执行方式,分别针对无返回值和有返回值的情况。
示例: 无返回值事务,支持批量操作。
Transactions.execute(new ITrade() {
@Override
public void deal() throws Throwable {
// 具体业务逻辑
}
});
// 支持批量业务逻辑处理
Transactions.execute(new ITrade() {
@Override
public void deal() throws Throwable {
// 具体业务逻辑1
}
}, new ITrade() {
@Override
public void deal() throws Throwable {
// 具体业务逻辑2
}
});
// 可以指定事务级别,默认为Type.TRANSACTION.READ_COMMITTED
Transactions.execute(Type.TRANSACTION.REPEATABLE_READ, new ITrade() {
@Override
public void deal() throws Throwable {
// 具体业务逻辑
}
});
示例: 有返回值事务,不支持批量操作。
UserEntity userEntity = Transactions.execute(new AbstractTrade<UserEntity>() {
@Override
public UserEntity dealing() throws Throwable {
// 具体业务逻辑
return null;
}
});
// 可以指定事务级别,默认为Type.TRANSACTION.READ_COMMITTED
UserEntity userEntity = Transactions.execute(Type.TRANSACTION.REPEATABLE_READ, new AbstractTrade<UserEntity>() {
@Override
public UserEntity dealing() throws Throwable {
// 具体业务逻辑
return null;
}
});
事务回滚(Rollback)
不论是手动开启事务还是通过 @Transaction 注解自动开启事务,只要在具体业务逻辑处理过程中抛出任何异常都将终止事务并回滚。
会话(Session)
会话是对应用中具体业务操作触发的一系列与数据库之间的交互过程的封装,通过建立一个临时通道,负责与数据库之间连接资源的创建及回收,同时提供更为高级的抽象指令接口调用,基于会话的优点:
- 开发人员不需要担心连接资源是否正确释放。
- 严格的编码规范更利于维护和理解。
- 更好的业务封装性。
如何开启会话
示例: 使用默认数据源开启会话
UserEntity userEntity = JDBC.get().openSession(new IDatabaseSessionExecutor<UserEntity>() {
public UserEntity execute(IDatabaseSession session) throws Exception {
// TODO 此处填写业务逻辑代码...
return session.findFirst(EntitySQL.create(UserEntity.class));
}
});
示例: 使用指定的数据源开启会话
IResultSet<UserEntity> users = JDBC.get().openSession("oracledb", new IDatabaseSessionExecutor<IResultSet<UserEntity>>() {
public IResultSet<UserEntity> execute(IDatabaseSession session) throws Exception {
// TODO 此处填写业务逻辑代码...
return session.find(EntitySQL.create(UserEntity.class));
}
});
基于会话的数据库操作
以下示例代码仍然是围绕前面用到的用户数据实体(UserEntity)类来展示如何使用会话对象完成数据表的CRUD操作。
实际上,在 YMP 框架的 v2.1.x 版本中已重构了数据实体方法,针对单表的绝大部份操作完全可以通过自动生成的数据实体类完成。尽管如此,了解会话的运行机制和使用方法是非常有必要的,因为它是 JDBC 模块的基础、是框架与数据库之间的桥梁,一些更为复杂的操作仍需通过它来完成。
插入(Insert)
UserEntity userEntity = JDBC.get().openSession(new IDatabaseSessionExecutor<UserEntity>() {
public UserEntity execute(IDatabaseSession session) throws Exception {
UserEntity user = UserEntity.builder()
.id(UUIDUtils.UUID())
.username("suninformation")
.nickname("有理想的鱼")
.password(DigestUtils.md5Hex("123456"))
.email("suninformation@163.com")
.build();
// 执行数据插入
user = session.insert(user);
// 或者在插入时也可以指定/排除某些字段
user = session.insert(user, Fields.create(UserEntity.FIELDS.NICKNAME,
UserEntity.FIELDS.EMAIL).excluded(true));
return user;
}
});
更新(Update)
UserEntity userEntity = JDBC.get().openSession(new IDatabaseSessionExecutor<UserEntity>() {
public UserEntity execute(IDatabaseSession session) throws Exception {
UserEntity user = UserEntity.builder()
.id("bc19f5645aa9438089c5e9954e5f1ac5")
.password(DigestUtils.md5Hex("654321"))
.gender("F")
.build();
// 执行记录更新
user = session.update(user);
// 或者仅更新指定的字段/排除某些字段
user = session.update(user, Fields.create(UserEntity.FIELDS.PASSWORD));
return user;
}
});
插入或更新(Upsert)
@since 2.1.4
upsert 方法用于执行插入或更新操作,根据数据库类型采用原子操作实现:
| 数据库类型 | SQL 语法 |
|---|---|
| MySQL | INSERT ... ON DUPLICATE KEY UPDATE ... |
| PostgreSQL | INSERT ... ON CONFLICT ... DO UPDATE SET ... |
| SQLite | INSERT ... ON CONFLICT ... DO UPDATE SET ... |
| Oracle/SQLServer/DB2/H2/HSQLDB | MERGE INTO ... |
UserEntity userEntity = JDBC.get().openSession(new IDatabaseSessionExecutor<UserEntity>() {
public UserEntity execute(IDatabaseSession session) throws Exception {
UserEntity user = UserEntity.builder()
.id(UUIDUtils.UUID())
.username("suninformation")
.nickname("有理想的鱼")
.password(DigestUtils.md5Hex("123456"))
.email("suninformation@163.com")
.build();
// 执行插入或更新操作
user = session.upsert(user);
// 或者指定/排除某些字段
user = session.upsert(user, Fields.create(UserEntity.FIELDS.NICKNAME, UserEntity.FIELDS.EMAIL));
return user;
}
});
批量 Upsert 操作:
List<UserEntity> userEntities = JDBC.get().openSession(new IDatabaseSessionExecutor<List<UserEntity>>() {
public List<UserEntity> execute(IDatabaseSession session) throws Exception {
List<UserEntity> users = new ArrayList<>();
users.add(UserEntity.builder().id(UUIDUtils.UUID()).username("user1").build());
users.add(UserEntity.builder().id(UUIDUtils.UUID()).username("user2").build());
// 执行批量插入或更新操作
return session.upsert(users);
// 或者指定/排除某些字段
// return session.upsert(users, Fields.create(UserEntity.FIELDS.NICKNAME));
}
});
插入如果不存在(InsertIfNotExist)
@since 2.1.4
insertIfNotExist 方法用于执行"如果不存在则插入"操作,根据数据库类型采用原子操作实现:
| 数据库类型 | SQL 语法 |
|---|---|
| MySQL | INSERT IGNORE INTO ... |
| PostgreSQL | INSERT INTO ... ON CONFLICT DO NOTHING |
| SQLite | INSERT OR IGNORE INTO ... |
| Oracle/SQLServer/H2/DB2/HSQLDB | MERGE INTO ... WHEN NOT MATCHED THEN INSERT ... |
UserEntity userEntity = JDBC.get().openSession(new IDatabaseSessionExecutor<UserEntity>() {
public UserEntity execute(IDatabaseSession session) throws Exception {
UserEntity user = UserEntity.builder()
.id(UUIDUtils.UUID())
.username("suninformation")
.nickname("有理想的鱼")
.password(DigestUtils.md5Hex("123456"))
.email("suninformation@163.com")
.build();
// 如果记录不存在则插入,若记录已存在则返回null
user = session.insertIfNotExist(user);
// 或者指定/排除某些字段
user = session.insertIfNotExist(user, Fields.create(UserEntity.FIELDS.NICKNAME, UserEntity.FIELDS.EMAIL));
return user;
}
});
批量 InsertIfNotExist 操作:
List<UserEntity> userEntities = JDBC.get().openSession(new IDatabaseSessionExecutor<List<UserEntity>>() {
public List<UserEntity> execute(IDatabaseSession session) throws Exception {
List<UserEntity> users = new ArrayList<>();
users.add(UserEntity.builder().id(UUIDUtils.UUID()).username("user1").build());
users.add(UserEntity.builder().id(UUIDUtils.UUID()).username("user2").build());
// 执行批量插入如果不存在操作
return session.insertIfNotExist(users);
// 或者指定/排除某些字段
// return session.insertIfNotExist(users, Fields.create(UserEntity.FIELDS.NICKNAME));
}
});
查询(Find)
方式一:根据记录ID加载
UserEntity userEntity = JDBC.get().openSession(new IDatabaseSessionExecutor<UserEntity>() {
public UserEntity execute(IDatabaseSession session) throws Exception {
EntitySQL entitySQL = EntitySQL.create(UserEntity.class);
// 或者加载指定的字段
entitySQL.field(UserEntity.FIELDS.USERNAME).field(UserEntity.FIELDS.NICKNAME);
return session.find(entitySQL, "bc19f5645aa9438089c5e9954e5f1ac5");
}
});
方式二:通过数据实体设置条件
IResultSet<UserEntity> users = JDBC.get().openSession(new IDatabaseSessionExecutor<IResultSet<UserEntity>>() {
public IResultSet<UserEntity> execute(IDatabaseSession session) throws Exception {
// 非空属性之间将使用and条件连接,查询所有符合条件的记录并返回所有字段
UserEntity user = new UserEntity();
user.setUsername("suninformation");
user.setPassword(DigestUtils.md5Hex("654321"));
// 返回指定的字段
return session.find(user, Fields.create(UserEntity.FIELDS.ID, UserEntity.FIELDS.EMAIL));
}
});
方式三:自定义属性条件并分页查询
IResultSet<UserEntity> users = JDBC.get().openSession(new IDatabaseSessionExecutor<IResultSet<UserEntity>>() {
public IResultSet<UserEntity> execute(IDatabaseSession session) throws Exception {
return session.find(EntitySQL.create(UserEntity.class)
.field(Fields.create(UserEntity.FIELDS.ID, UserEntity.FIELDS.PASSWORD)),
Where.create(Cond.create()
.eq(UserEntity.FIELDS.USERNAME).param("suninformation").and()
.eq(UserEntity.FIELDS.PASSWORD).param(DigestUtils.md5Hex("654321")))
.orderByDesc(UserEntity.FIELDS.CREATE_TIME),
Page.create().pageSize(10));
}
});
方式四:返回符合条件的第一条记录
UserEntity user = JDBC.get().openSession(new IDatabaseSessionExecutor<UserEntity>() {
public UserEntity execute(IDatabaseSession session) throws Exception {
// 返回与用户名称包含"info"的的第一条记录
Cond cond = Cond.create().like(UserEntity.FIELDS.USERNAME).param(Like.create("info").contains()));
return session.findFirst(EntitySQL.create(UserEntity.class)
.field(Fields.create(UserEntity.FIELDS.ID, UserEntity.FIELDS.NICKNAME)),
Where.create(cond).orderByDesc(UserEntity.FIELDS.CREATE_TIME));
}
});
方式五:统计符合条件的记录数
Long count = JDBC.get().openSession(new IDatabaseSessionExecutor<Long>() {
public Long execute(IDatabaseSession session) throws Exception {
// 返回与用户名称和密码匹配的记录数
return session.count(UserEntity.class, Where.create(Cond.create()
.eq(UserEntity.FIELDS.USERNAME).param("suninformation")
.and().eq(UserEntity.FIELDS.PASSWORD).param(DigestUtils.md5Hex("654321"))));
}
});
方式六:执行自定义SQL查询
IResultSet<Object[]> resultSet = JDBC.get().openSession(new IDatabaseSessionExecutor<IResultSet<Object[]>>() {
public IResultSet<Object[]> execute(IDatabaseSession session) throws Exception {
// 查询邮件后缀为`@163.com`的全部记录
return session.find(SQL.create("SELECT * FROM user WHERE email LIKE ?")
.param(Like.create("@163.com").endsWith()), IResultSetHandler.ARRAY.create());
}
});
删除(Delete)
方式一:根据记录ID删除
Integer effectCount = JDBC.get().openSession(new IDatabaseSessionExecutor<Integer>() {
public Integer execute(IDatabaseSession session) throws Exception {
// 根据实体主键删除记录,返回影响记录数
return session.delete(UserEntity.class, "bc19f5645aa9438089c5e9954e5f1ac5");
}
});
方式二:根据条件删除记录
UserEntity user = JDBC.get().openSession(new IDatabaseSessionExecutor<UserEntity>() {
public UserEntity execute(IDatabaseSession session) throws Exception {
// 非空属性之间将使用and条件连接
UserEntity user = UserEntity.builder()
.username("suninformation")
.password(DigestUtils.md5Hex("654321"))
.build();
return session.delete(user);
}
});
执行更新类操作(ExecuteForUpdate)
该方法用于执行会话接口中并未提供对应的方法封装且执行操作会对数据库产生变化的 SQL 语句,执行该方法后将返回受影响记录行数。
示例: 删除邮件后缀为 @163.com 的记录。
Integer effectCount = JDBC.get().openSession(new IDatabaseSessionExecutor<Integer>() {
public Integer execute(IDatabaseSession session) throws Exception {
return session.executeForUpdate(Delete.create(UserEntity.class)
.where(Cond.create().like(UserEntity.FIELDS.EMAIL)
.param(Like.create("@163.com").endsWith())).toSQL());
}
});
注:以上操作均支持批量操作,具体使用请阅读 API 接口文档和相关源码。
数据库会话事件监听器
@since 2.1.4
通过实现 IDatabaseSessionEventListener 接口,可以监听数据库会话中的 CRUD 操作事件。此外,还可以在事件触发时 对 SQL 语句和参数进行修改等操作,从而实现 SQL 拦截等功能。
事件类型
数据库会话事件监听器提供以下事件方法:
| 事件方法 | 说明 |
|---|---|
onQueryBefore | 查询操作执行前触发 |
onQueryAfter | 查询操作执行后触发 |
onInsertBefore | 插入操作执行前触发 |
onInsertAfter | 插入操作执行后触发 |
onInsertIfNotExistBefore | 插入(记录不存在时)操作执行前触发 |
onInsertIfNotExistAfter | 插入(记录不存在时)操作执行后触发 |
onUpdateBefore | 更新操作执行前触发 |
onUpdateAfter | 更新操作执行后触发 |
onUpsertBefore | 更新插入操作执行前触发 |
onUpsertAfter | 更新插入操作执行后触发 |
onRemoveBefore | 删除操作执行前触发 |
onRemoveAfter | 删除操作执行后触发 |
事件上下文
DatabaseSessionEventContext 事件上下文提供以下方法:
| 方法 | 说明 |
|---|---|
getSource() | 获取数据库会话对象(IDatabaseSession) |
getOperationType() | 获取操作类型(Type.OPT 枚举) |
getSql() | 获取执行的 SQL 语句字符串 |
setSql(String sql) | 设置 SQL 语句字符串(可在 before 事件中修改) |
getParams() | 获取 SQL 参数(Params 对象) |
setParams(Params params) | 设置 SQL 参数(可在 before 事件中修改) |
getBatchSQL() | 获取批量 SQL 对象(BatchSQL) |
setBatchSQL(BatchSQL batchSQL) | 设置批量 SQL 对象(可在 before 事件中修改) |
putAttribute(String key, Object value) | 存储自定义属性 |
getAttribute(String key) | 获取自定义属性 |
getAttributes() | 获取所有自定义属性 |
特别说明:在 after 事件中,可以通过 getAttribute(IOperator.class.getName()) 获取当前操作器接口对象,从而获取执行结果,例如:
IQueryOperator:查询操作器,可获取查询结果IUpdateOperator:更新操作器,可获取影响行数IDeleteOperator:删除操作器,可获取影响行数
监听器类型
YMP 框架支持两种类型的数据库会话事件监听器:
1. 全局数据库会话事件监听器
全局监听器会对所有数据库会话的操作生效,适用于需要全局监控或处理的场景。
全局监听器的注册方式
-
通过 SPI 自动注册:
实现
IDatabaseSessionEventListener接口的类,会通过 SPI 机制在 JDBC 模块初始化时自动注册为全局监听器。 -
通过代码手动注册:
import net.ymate.platform.persistence.jdbc.JDBC;
import net.ymate.platform.persistence.jdbc.IDatabase;
import net.ymate.platform.persistence.jdbc.IDatabaseSessionEventListener;
// 获取数据库实例
IDatabase database = JDBC.get();
// 创建自定义监听器
IDatabaseSessionEventListener customListener = new CustomDatabaseSessionEventListener();
// 注册全局监听器
database.registerGlobalSessionEventListener(customListener);
// 获取全局监听器
IDatabaseSessionEventListener globalListener = database.getGlobalSessionEventListener();
2. 局部数据库会话事件监听器
局部监听器仅对特定的数据库会话生效,适用于需要针对特定会话进行处理的场景。
局部监听器的设置方式
import net.ymate.platform.persistence.jdbc.JDBC;
import net.ymate.platform.persistence.jdbc.IDatabaseSession;
import net.ymate.platform.persistence.jdbc.IDatabaseSessionEventListener;
// 获取数据库实例
IDatabase database = JDBC.get();
// 开启数据库会话
IDatabaseSession session = database.openSession();
// 创建自定义监听器
IDatabaseSessionEventListener customListener = new CustomDatabaseSessionEventListener();
// 设置局部监听器
session.setSessionEventListener(customListener);
// 使用会话执行操作
// ...
// 关闭会话
session.close();
监听器的执行顺序
当同时存在全局监听器和局部监听器(包括会话中设置的监听器和查询对象中设置的监听器)时,所有监听器会按照注册顺序依次执行:
- 全局监听器优先:通过
JDBC.get().registerGlobalSessionEventListener()注册的全局监听器最先执行 - 局部监听器随后:通过
session.setSessionEventListener()或查询对象的sessionEventListener()方法设置的监听器会在全局监听器之后执行
具体执行流程如下:
Before 阶段(数据库操作执行前):
- 依次执行所有全局监听器的
onXXXBefore方法 - 依次执行所有局部监听器的
onXXXBefore方法 - 执行数据库操作
After 阶段(数据库操作执行后):
- 依次执行所有全局监听器的
onXXXAfter方法 - 依次执行所有局部监听器的
onXXXAfter方法
重要说明:
- 查询对象中设置的监听器实际上会被添加到数据库会话中,与全局监听器和会话中设置的监听器合并执行。
- 全局监听器由于先注册,所以会最先被调用,然后才是局部监听器。
- 所有监听器的
onXXXBefore方法按顺序执行完成后,才会执行实际的数据库操作,然后按顺序执行所有监听器的onXXXAfter方法。 - 如果在任何
before事件方法中抛出异常,将中止当前数据库操作,不会执行实际的数据库操作,也不会调用对应的after事件方法。
在查询对象中使用会话监听器
除了通过 IDatabaseSession 设置监听器外,YMP 框架还支持直接在查询对象(如 Select、Insert、Update、Delete、BatchSQL、EntitySQL)中设置会话监听器。这种方式更加灵活和便捷,适用于需要针对特定查询操作进行事件监听的场景。
支持的查询类
以下查询类都支持设置会话监听器:
Select- 查询语句对象Insert- 插入语句对象Update- 更新语句对象Delete- 删除语句对象SQL- SQL 语句及参数对象BatchSQL- 批量更新 SQL 语句对象EntitySQL- 实体 SQL 及参数对象
设置方式
所有查询类都提供了 sessionEventListener(IDatabaseSessionEventListener) 方法用于设置监听器,并返回当前对象以支持链式调用;同时提供 sessionEventListener() 方法用于获取当前设置的监听器:
import net.ymate.platform.persistence.jdbc.IDatabaseSessionEventListener;
import net.ymate.platform.persistence.jdbc.query.Select;
import net.ymate.platform.persistence.jdbc.query.Insert;
import net.ymate.platform.persistence.jdbc.query.Update;
import net.ymate.platform.persistence.jdbc.query.Delete;
import net.ymate.platform.persistence.jdbc.query.BatchSQL;
import net.ymate.platform.persistence.jdbc.query.EntitySQL;
// 创建自定义监听器
IDatabaseSessionEventListener customListener = new CustomDatabaseSessionEventListener();
// 1. Select 查询中设置监听器
Select select = Select.create(UserEntity.class)
.field("id", "username", "email")
.where(Where.create().eq("status", 1))
.sessionEventListener(customListener);
IResultSet<UserEntity> users = select.find(BeanResultSetHandler.create(UserEntity.class));
// 2. Insert 插入中设置监听器
Insert insert = Insert.create(UserEntity.class)
.field("id")
.field("username")
.field("email")
.param("user_001")
.param("test")
.param("test@example.com")
.sessionEventListener(customListener);
int insertCount = insert.execute();
// 3. Update 更新中设置监听器
Update update = Update.create(UserEntity.class)
.field("nickname = ?", "新昵称")
.where(Where.create().eq("id", "user_001"))
.sessionEventListener(customListener);
int updateCount = update.execute();
// 4. Delete 删除中设置监听器
Delete delete = Delete.create(UserEntity.class)
.where(Where.create().eq("id", "user_001"))
.sessionEventListener(customListener);
int deleteCount = delete.execute();
// 5. BatchSQL 批量操作中设置监听器
BatchSQL batchSQL = BatchSQL.create()
.addSQL("INSERT INTO user (id, username) VALUES (?, ?)")
.addParameter(Params.create().add("user_001").add("user1"))
.addSQL("UPDATE user SET status = ? WHERE id = ?")
.addParameter(Params.create().add(1).add("user_001"))
.sessionEventListener(customListener);
int[] batchCounts = batchSQL.execute();
// 6. EntitySQL 实体操作中设置监听器
EntitySQL<UserEntity> entitySQL = EntitySQL.create(UserEntity.class)
.sessionEventListener(customListener);
UserEntity user = entitySQL.find("user_001");
// 7. SQL 直接执行中设置监听器
SQL sql = SQL.create("SELECT * FROM user WHERE id = ?")
.param("user_001")
.sessionEventListener(customListener);
UserEntity user = sql.findFirst(BeanResultSetHandler.create(UserEntity.class));
监听器的传递机制
当在查询对象中设置监听器后,查询对象在执行时会自动将监听器传递给数据库会话。具体流程如下:
-
查询对象转换为 SQL:对于
Select、Insert、Update、Delete等查询构建器,它们会先转换为SQL对象,然后将监听器设置到SQL对象上。 -
SQL 执行时设置监听器:
SQL对象在执行时(如execute()、find()、count()等方法),会在打开数据库会话后立即设置监听器到会话中。 -
监听器生效:设置到会话的监听器会在数据库操作执行前后被触发,执行相应的事件方法。
使用场景
查询对象中设置监听器适用于以下场景:
- 特定查询的 SQL 拦截:如为特定查询自动添加租户 ID 过滤条件
- 单次操作的性能监控:如监控某个复杂查询的执行时间
- 临时数据校验:如针对特定插入操作的参数校验
- 调试和日志记录:如临时开启某个查询的详细日志
注意:查询对象中的监听器设置是临时的,仅对当前查询对象的执行生效。如果需要全局生效,建议使用全局监听器。
示例:使用全局数据库会话事件监听器记录 SQL 执行时间
import net.ymate.platform.persistence.jdbc.IDatabaseSessionEventListener;
import net.ymate.platform.persistence.jdbc.DatabaseSessionEventContext;
public class SqlExecutionTimeListener implements IDatabaseSessionEventListener {
@Override
public void onQueryBefore(DatabaseSessionEventContext eventContext) throws Exception {
eventContext.putAttribute("startTime", System.currentTimeMillis());
}
@Override
public void onQueryAfter(DatabaseSessionEventContext eventContext) throws Exception {
long startTime = (long) eventContext.getAttribute("startTime");
long executionTime = System.currentTimeMillis() - startTime;
System.out.println("SQL execution time: " + executionTime + "ms - " + eventContext.getSql());
}
// 其他事件方法...
}
// 注册全局监听器
JDBC.get().registerGlobalSessionEventListener(new SqlExecutionTimeListener());
示例:使用局部数据库会话事件监听器修改 SQL
import net.ymate.platform.persistence.jdbc.IDatabaseSessionEventListener;
import net.ymate.platform.persistence.jdbc.DatabaseSessionEventContext;
public class SqlModifierListener implements IDatabaseSessionEventListener {
@Override
public void onQueryBefore(DatabaseSessionEventContext eventContext) throws Exception {
// 修改 SQL 语句,添加 LIMIT 子句
String originalSql = eventContext.getSql();
if (!originalSql.toLowerCase().contains("limit")) {
eventContext.setSql(originalSql + " LIMIT 100");
}
}
// 其他事件方法...
}
// 使用局部监听器
IDatabaseSession session = JDBC.get().openSession();
session.setSessionEventListener(new SqlModifierListener());
// 执行查询操作,SQL 会被自动修改
// ...
session.close();
通过数据库会话事件监听器,可以实现诸如 SQL 执行时间监控、SQL 语句日志记录、数据权限控制、SQL 语句修改等功能,为数据库操作提供更多的灵活性和可扩展性。
使用示例
import net.ymate.platform.persistence.jdbc.IDatabaseSessionEventListener;
import net.ymate.platform.persistence.jdbc.DatabaseSessionEventContext;
import net.ymate.platform.persistence.jdbc.IOperator;
import net.ymate.platform.persistence.jdbc.IQueryOperator;
import net.ymate.platform.persistence.jdbc.IUpdateOperator;
import net.ymate.platform.persistence.jdbc.IDeleteOperator;
public class DatabaseSessionEventListenerImpl implements IDatabaseSessionEventListener {
@Override
public void onQueryBefore(DatabaseSessionEventContext eventContext) {
System.out.println("执行查询前 - SQL: " + eventContext.getSql());
}
@Override
public void onQueryAfter(DatabaseSessionEventContext eventContext) {
System.out.println("执行查询后 - SQL: " + eventContext.getSql());
// 获取查询操作器,获取执行结果
IOperator operator = (IOperator) eventContext.getAttribute(IOperator.class.getName());
if (operator instanceof IQueryOperator) {
IQueryOperator queryOperator = (IQueryOperator) operator;
// 可以通过 queryOperator 获取查询结果
System.out.println("查询操作器类型: " + queryOperator.getClass().getName());
}
}
@Override
public void onInsertBefore(DatabaseSessionEventContext eventContext) {
System.out.println("执行插入前 - SQL: " + eventContext.getSql());
}
@Override
public void onInsertAfter(DatabaseSessionEventContext eventContext) {
System.out.println("执行插入后 - SQL: " + eventContext.getSql());
// 获取更新操作器,获取影响行数
IOperator operator = (IOperator) eventContext.getAttribute(IOperator.class.getName());
if (operator instanceof IUpdateOperator) {
IUpdateOperator updateOperator = (IUpdateOperator) operator;
System.out.println("插入影响行数: " + updateOperator.getEffectCounts());
}
}
@Override
public void onUpdateBefore(DatabaseSessionEventContext eventContext) {
System.out.println("执行更新前 - SQL: " + eventContext.getSql());
}
@Override
public void onUpdateAfter(DatabaseSessionEventContext eventContext) {
System.out.println("执行更新后 - SQL: " + eventContext.getSql());
// 获取更新操作器,获取影响行数
IOperator operator = (IOperator) eventContext.getAttribute(IOperator.class.getName());
if (operator instanceof IUpdateOperator) {
IUpdateOperator updateOperator = (IUpdateOperator) operator;
System.out.println("更新影响行数: " + updateOperator.getEffectCounts());
}
}
@Override
public void onRemoveBefore(DatabaseSessionEventContext eventContext) {
System.out.println("执行删除前 - SQL: " + eventContext.getSql());
}
@Override
public void onRemoveAfter(DatabaseSessionEventContext eventContext) {
System.out.println("执行删除后 - SQL: " + eventContext.getSql());
// 获取删除操作器,获取影响行数
IOperator operator = (IOperator) eventContext.getAttribute(IOperator.class.getName());
if (operator instanceof IDeleteOperator) {
IDeleteOperator deleteOperator = (IDeleteOperator) operator;
System.out.println("删除影响行数: " + deleteOperator.getEffectCounts());
}
}
@Override
public void onInsertIfNotExistBefore(DatabaseSessionEventContext eventContext) {
System.out.println("执行插入(如果记录不存在)前 - SQL: " + eventContext.getSql());
}
@Override
public void onInsertIfNotExistAfter(DatabaseSessionEventContext eventContext) {
System.out.println("执行插入(如果记录不存在)后 - SQL: " + eventContext.getSql());
// 获取更新操作器,获取影响行数
IOperator operator = (IOperator) eventContext.getAttribute(IOperator.class.getName());
if (operator instanceof IUpdateOperator) {
IUpdateOperator updateOperator = (IUpdateOperator) operator;
System.out.println("插入影响行数: " + updateOperator.getEffectCounts());
}
}
@Override
public void onUpsertBefore(DatabaseSessionEventContext eventContext) {
System.out.println("执行更新插入前 - SQL: " + eventContext.getSql());
}
@Override
public void onUpsertAfter(DatabaseSessionEventContext eventContext) {
System.out.println("执行更新插入后 - SQL: " + eventContext.getSql());
// 获取更新操作器,获取影响行数
IOperator operator = (IOperator) eventContext.getAttribute(IOperator.class.getName());
if (operator instanceof IUpdateOperator) {
IUpdateOperator updateOperator = (IUpdateOperator) operator;
System.out.println("更新插入影响行数: " + updateOperator.getEffectCounts());
}
}
}
重要说明:如果在任何 before 事件方法中抛出异常,将中止当前数据库操作,不会执行实际的数据库操作,也不会调用对应的 after 事件方法。这可以用于实现数据校验、权限控制等功能。例如:
@Override
public void onInsertBefore(DatabaseSessionEventContext eventContext) throws Exception {
// 检查参数中是否包含敏感数据
if (eventContext.getParams() != null) {
for (Object param : eventContext.getParams().params()) {
if ("sensitive_data".equals(param)) {
throw new RuntimeException("检测到敏感数据,操作被中止");
}
}
}
}
在会话中设置监听器:
jdbc.openSession((IDatabaseSessionExecutor<Void>) session -> {
// 设置事件监听器
session.setSessionEventListener(new DatabaseSessionEventListenerImpl());
// 执行数据库操作,事件监听器将被触发
UserEntity user = new UserEntity();
user.setId("user_001");
user.setUsername("test");
session.insert(user);
// 查询
UserEntity loadedUser = session.find(EntitySQL.create(UserEntity.class), user.getId());
// 更新
loadedUser.setNickname("测试用户");
session.update(loadedUser);
// 删除
session.delete(UserEntity.class, user.getId());
return null;
});
结果集(ResultSet)
JDBC 持久化模块将数据查询的结果集合统一使用 IResultSet 接口进行封装并集成分页参数,下面通过一段代码来了解它:
IResultSet<UserEntity> results = JDBC.get().openSession(new IDatabaseSessionExecutor<IResultSet<UserEntity>>() {
public IResultSet<UserEntity> execute(IDatabaseSession session) throws Exception {
return session.find(EntitySQL.create(UserEntity.class), Page.create());
}
});
// 返回当前是否分页查询
boolean isPaginated = results.isPaginated();
// 当前结果集是否可用,即是否为空或元素数量为0
boolean isAvailable = results.isResultsAvailable();
// 返回当前页号,若未分页则返回0
int pNumber = results.getPageNumber();
// 返回每页记录数,若未分页则返回0
int pSize = results.getPageSize();
// 返回总页数,若未分页则返回0
int pCount = results.getPageCount();
// 返回总记录数,若未分页则返回0
long rCount = results.getRecordCount();
// 返回结果集数据
List<UserEntity> users = results.getResultData();
对象查询(Query)
本节主要介绍 JDBC 持久化模块从 v2.x 版本开始新增的特性,主要用于辅助开发人员像写 Java 代码一样编写 SQL 语句,在一定程度上替代传统字符串拼接的模式,与数据实体的字段常量一起配合使用,这样做的好处是降低字符串拼接过程中出错的机率,让一些问题能够在编译期间被及时发现。该特性在 v2.1.x 版本中进行了重构和完善,使用起来也更简单、便捷。
Fields:字段名称集合
用于辅助拼接数据表字段名称等,支持自定义前缀和别名。
示例代码:
// 创建Fields对象
Fields fields = Fields.create(UserEntity.FIELDS.USERNAME, "password");
// 添加带前缀和别名
fields.add("u", UserEntity.FIELDS.EMAIL, "e");
// 添加带前缀
fields = Fields.create().add("u", UserEntity.FIELDS.ID).add(fields);
// 标记集合中的字段为排除的
fields.excluded(true);
// 判断是否存在排除标记
fields.isExcluded();
// 输出
System.out.println(fields.fields());
执行结果:
[u.id, username, password, u.email e]
Params:参数集合
主要存储用于替换 SQL 语句中 ? 号占位符对应的参数值对象。
示例代码:
// 创建Params对象,任何类型参数
Params params = Params.create("p1", 2, false, 0.1).add("param");
//
params = Params.create().add("paramN").add(params);
// 输出
System.out.println(params.params());
执行结果:
[paramN, p1, 2, false, 0.1, param]
Page:分页参数
示例代码:
// 默认查询第1页,每页20条记录
Page.create();
// 查询第2页, 每页10条记录
Page.create(2).pageSize(10);
// 查询第1页, 每页10条记录, 但不统计总记录数
Page.create(1).pageSize(10).count(false);
// 根据参数值尝试创建分页对象,若page或pageSize参数为空或小于等于0则返回null
Page.createIfNeed(1, Page.DEFAULT_PAGE_SIZE);
Cond:条件参数
用于生成 SQL 条件语句并存储条件参数。
示例一:构造方式
// 使用全局JDBC模块的默认数据源配置构建;
Cond.create();
// 使用指定的JDBC模块实例和数据源配置构建;
Cond.create(JDBC.get(), "default");
// 通过任何继承Query(如:Cond、Where、Select、Insert、Delete等本章节所提及的大部份对象都是其子类)类的实例对象构建;
// 通过已存在的Query对象构建的主要目的是避免重复获取其所依赖的相同容器、数据源等配置;
Cond cond = Cond.create(JDBC.get(), "oracledb");
cond.bracket(Cond.create(cond).eq("age").param(18));
以 Cond 条件参数为例,对象查询所涉及的大部份对象(如:OrderBy、GroupBy、Where、Select、Insert、Update、Delete、Join、SQL、BatchSQL 和 EntitySQL 等)均与之构造方式不尽相同,后面内容将不再赘述。
示例二:参数的传递
在 Cond 对象中除两个字段间比较之外的条件都将构建一个基于 ? 占位的SQL表达式,通过 param 方法传入与其对应的参数值,条件对象将按参数传入顺序存储:
Cond cond = Cond.create()
.bracket(Cond.create().like("username").param(Like.create("ymp").contains()).and().gtEq("age").param(20))
.or().bracket(Cond.create().eq("gender").param("F").and().lt("age").param(18));
System.out.println("SQL: " + cond.toString());
System.out.println("参数: " + cond.params().params());
执行结果:
SQL: ( username LIKE ? AND age >= ? ) OR ( gender = ? AND age < ? )
参数: [%ymp%, 20, F, 18]
示例三:比较运算符的使用
| 运算符 | 代码 | 输出SQL语句 |
|---|---|---|
= | cond.eq("age").param(18) | age = 18 |
!= | cond.notEq("age").param(18) | age != 18 |
> | cond.gt("age").param(18) | age > 18 |
< | cond.lt("age").param(18) | age < 18 |
>= | cond.gtEq("age").param(18) | age >= 18 |
<= | cond.ltEq("age").param(18) | age <= 18 |
以上操作均支持两字段之间比较,如:
// 输出SQL:username = nickname
cond.eqField("username", "nickname");
// 输出SQL:username != nickname
cond.notEqField("username", "nickname")
示例四:逻辑运算符的使用
| 运算符 | 代码 | 输出SQL语句 |
|---|---|---|
AND | cond.and()cond.andIfNeed()cond.andIfNeed(cond...)cond.and(cond...) | AND ... |
OR | cond.or()cond.orIfNeed()cond.orIfNeed(cond...)cond.or(cond...) | OR ... |
NOT | cond.not()cond.not(cond...) | NOT ... |
示例四:其它运算符的使用
| 运算符 | 代码 | 输出SQL语句 |
|---|---|---|
IN | cond.in("uid", Params.create(...)) | uid IN (...) |
EXISTS | cond.exists(...)cond.not().exists(...) | EXISTS (...) NOT EXISTS (...) |
RANGE | cond.range("age", 18, 20, LogicalOpt.AND)cond.range("age", 18, null, LogicalOpt.OR)cond.range("age", null, 20, null) | AND age BETWEEN (18 AND 20) OR age >= 18 age <= 20 |
BETWEEN | cond.between("age", 18, 20) | age BETWEEN 18 AND 20 |
() | cond.bracket(cond...)cond.bracketBegin()...bracketEnd() | (...) |
1=1 | cond.eqOne() | 1=1 |
LIKE | cond.like("username").param(Like.create("ymp").contains()) | username LIKE '%ymp%' |
OPT | cond.opt("username", OPT.EQ)cond.opt("username", OPT.EQ, "nickname") | username = ? username = nickname |
Cond 类提供的诸多方法(如:eq)中,方法名称以 Wrap 为后缀(如:eqWrap)的作用是为字段名称添加与当前数据库匹配的引用标识符。
示例五:表达式条件判断
public Cond exprBuild(Cond cond, String username) {
// 方式一:
return cond.expr(StringUtils.isNotBlank(username), new IConditionAppender() {
@Override
public void append(Cond cond) {
cond.andIfNeed().eq("username").param(username);
}
});
// 方式二:
return cond.expr(StringUtils.isNotBlank(username), new IConditionBuilder() {
@Override
public Cond build() {
return Cond.create(cond.andIfNeed()).eq("username").param(username);
}
});
}
示例六:对象非空条件判断
public Cond notEmptyBuild(Cond cond, String username) {
// 方式一:
return cond.exprNotEmpty(username, new IConditionAppender() {
@Override
public void append(Cond cond) {
cond.andIfNeed().eq("username").param(username);
}
});
// 方式二:
return cond.exprNotEmpty(username, new IConditionBuilder() {
@Override
public Cond build() {
return Cond.create(cond.andIfNeed()).eq("username").param(username);
}
});
}
FieldCondition:字段条件参数
用于为指定字段构建 Cond 条件参数对象。
假设我们要在用户表中查询使用了QQ邮箱的用户且年龄在18岁以下的女性用户,通常情况下的写 法如下:
Cond.create().like(UserEntity.FIELDS.EMAIL).param(Like.create("@qq.com").endsWith())
.and().eq(UserEntity.FIELDS.GENDER).param("F")
.and().ltEq(UserEntity.FIELDS.AGE).param(18);
将上述代码通过字段条件参数进行改写:
Cond cond = Cond.create();
cond.cond(FieldCondition.create(cond, UserEntity.FIELDS.EMAIL).like(Like.create("@qq.com").endsWith()))
.and(FieldCondition.create(cond, UserEntity.FIELDS.GENDER).eqValue("F"))
.cond(FieldCondition.create(cond, UserEntity.FIELDS.AGE).ltEqValue(18));
改写后的代码看上去并不理想,因为每个字段都需要通过 FieldCondition.create 方法创建一次,这反而变得更麻烦,别担心!我们可以通过自动生成的实体类提供的字段条件构建器(FieldConditionBuilder)再次改写:
UserEntity.FieldConditionBuilder fieldCondBuilder = UserEntity.conditionBuilder();
Cond cond = fieldCondBuilder.email().like(Like.create("@qq.com").endsWith()).build()
.and(fieldCondBuilder.gender().eqValue("F"))
.and(fieldCondBuilder.age().ltEqValue(18));
它们最终生成并执行的 SQL 语句和参数是一样的,如下所示:
email LIKE '%@qq.com' AND gender = 'F' and age <= 18
上述示例展示了同一种查询条件语句的三种不同构建方法,请开发人员根据实际情况选择适合的构建方式。
Like:模糊参数
用于模糊查询所需参数值的通配符填充,并支持将其中的特殊字符(如:%、_、\ 等)进行转义,这样做的主要目的是为了避免传统的字符串拼接过程容易产生的错误。
| 代码 | 描述 | 输出SQL语句 |
|---|---|---|
Like.create("ymp").contains() | 包含某字符串 | %ymp% |
Like.create("ymp").startsWith() | 以某字符串做为前缀 | ymp% |
Like.create("ymp").endsWith() | 以某字符串做为后缀 | %ymp |
从 v2.1.3 版本开始新增以下快捷方法:
| 代码 | 描述 | 输出SQL语句 |
|---|---|---|
Like.contains("ymp") | 包含某字符串 | %ymp% |
Like.startsWith("ymp") | 以某字符串做为前缀 | ymp% |
Like.endsWith("ymp") | 以某字符串做为后缀 | %ymp |
OrderBy:排序对象
用于生成 SQL 语句中的 ORDER BY 子句。
示例代码:
OrderBy orderBy = OrderBy.create().asc("age").desc("u", "create_time");
//
System.out.println(orderBy.toString());
执行结果:
ORDER BY age, u.create_time DESC
GroupBy:分组对象
用于 生成 SQL 语句中的 GROUP BY 子句。
示例代码:
GroupBy groupBy = GroupBy.create(Fields.create().add("u", "gender").add("age"))
.having(Cond.create().lt("age").param(18));
//
System.out.println("SQL: " + groupBy.toString());
System.out.println("参数: " + groupBy.having().params().params());
执行结果:
SQL: GROUP BY u.gender, age HAVING age < ?
参数: [18]
Where:条件对象
用于生成 SQL 语句中的 WHERE 子句,同时集成了 OrderBy 和 GroupBy 参数对象。
示例代码:
// 方式一:通过Cond条件参数直接构建Where对象
Where where = Cond.create().like("username").param("%ymp%").and().gtEq("age").param(20).buildWhere()
.groupBy("u", "gender")
.groupBy("age")
.orderByAsc("age")
.orderByDesc("u", "create_time");
// 方式二:分解各个部份
Cond cond = Cond.create()
.like("username").param("%ymp%")
.and().gtEq("age").param(20);
OrderBy orderBy = OrderBy.create().asc("age").desc("u", "creaate_time");
GroupBy groupBy = GroupBy.create(Fields.create().add("u", "gender").add("age"));
//
Where where = Where.create(cond).orderBy(orderBy).groupBy(groupBy);
//
System.out.println("SQL: " + where.toString());
System.out.println("参数: " + where.params().params());
执行结果:(为方便阅读,此处美化了 SQL 的输出格式)
SQL: WHERE
username LIKE ?
AND age >= ?
GROUP BY
u.gender,
age
ORDER BY
age,
u.create_time DESC
参数: [%ymp%, 20]
Select:查询语句对象
用于构建 SELECT 数据库查询语句。
示例一:通过用户数据实体查询
采用分页查询用户表中年龄大于等于20岁的数据,返回每条记录的主键、昵称和年龄字段并按年龄倒序排列。
Select select = Select.create(UserEntity.class, "u")
.field("u", UserEntity.FIELDS.ID)
.field("u", UserEntity.FIELDS.NICKNAME)
.field("u", UserEntity.FIELDS.AGE)
.orderByDesc("u", UserEntity.FIELDS.AGE)
.page(Page.create())
.where(Cond.create().gtEq("u", UserEntity.FIELDS.AGE).param(20))
.distinct();
System.out.println("SQL: " + select.toString());
System.out.println("参数: " + select.params().params());
执行结果:(为方便阅读,此处美化了 SQL 的输出格式)
SQL: SELECT DISTINCT u.id, u.nickname, u.age
FROM user u
WHERE u.age >= ?
ORDER BY u.age DESC
LIMIT 0, 20
参数: [%ymp%, 20]
示例二:子查询
Select subSelect = Select.create().from("user", "u").where(Cond.create().gtEq("u", "age").param(20));
Select select = Select.create(subSelect.alias("u"))
.field(Func.Aggregate.MAX(Fields.field("u", "age")))
.groupBy("u", "age");
System.out.println("SQL: " + select.toString());
System.out.println("参数: " + select.params().params());
执行结果:
SQL: SELECT MAX(u.age) FROM (SELECT * FROM user u WHERE u.age >= ?) u GROUP BY u.age LIMIT 0, 20
参数: [20]
示例三:执行查询
本例代码用另一种方式生成与 示例一 相同的 SQL 并执行。
IResultSet<UserEntity> users = Select.create("user", "u")
.field("u", Fields.create("id", "nickname", "age"))
.orderByDesc("u", "age")
.page(1)
.where(Cond.create().gtEq("u", "age").param(20))
.distinct()
.find(new EntityResultSetHandler<>(UserEntity.class));
Insert:插入语句对象
用于构建 INSERT 数据插入语句。
示例一:单记录插入
// 方式一:
Insert insert = Insert.create(UserEntity.class)
.field(UserEntity.FIELDS.ID).param(UUIDUtils.UUID())
.field(UserEntity.FIELDS.USERNAME).param("suninformation")
.field(UserEntity.FIELDS.NICKNAME).param("有理想的鱼")
.field(UserEntity.FIELDS.PASSWORD).param("123456")
.field(UserEntity.FIELDS.EMAIL).param("suninformation@163.com");
// 方式二:
Insert insert = Insert.create(UserEntity.class)
.field(Fields.create(UserEntity.FIELDS.ID,
UserEntity.FIELDS.USERNAME,
UserEntity.FIELDS.NICKNAME,
UserEntity.FIELDS.PASSWORD,
UserEntity.FIELDS.EMAIL))
.param(Params.create(UUIDUtils.UUID(),
"suninformation",
"有理想的鱼",
"123456",
"suninformation@163.com"))
SQL sql = insert.toSQL();
System.out.println("SQL: " + sql.toString());
System.out.println("参数: " + sql.params().params());
执行结果:
SQL: INSERT INTO user (id, username, nickname, password, email) VALUES (?,?,?,?,?)
参数: [28b89f37bd7d467e93f518080240ede4, suninformation, 有理想的鱼, 123456, suninformation@163.com]
示例二:批量插入
Insert insert = Insert.create(UserEntity.class)
.field(UserEntity.FIELDS.ID)
.field(UserEntity.FIELDS.USERNAME)
.field(UserEntity.FIELDS.NICKNAME)
.field(UserEntity.FIELDS.PASSWORD)
.field(UserEntity.FIELDS.EMAIL)
.addGroupParam(Params.create("1", "用户A", "昵称A", "密码A", "邮件A"))
.addGroupParam(Params.create("2", "用户B", "昵称B", "密码B", "邮件B"));
SQL sql = insert.toSQL();
System.out.println("SQL: " + sql.toString());
System.out.println("参数: " + sql.params().params());
执行结果:
SQL: INSERT INTO user (id, username, nickname, password, email) VALUES (?,?,?,?,?), (?,?,?,?,?)
参数: [1, 用户A, 昵称A, 密码A, 邮件A, 2, 用户B, 昵称B, 密码B, 邮件B]
示例三:通过SELECT结果集插入
Insert insert = Insert.create("new_table")
.select(Select.create().from("user").where(Cond.create().gtEq("age").param(20)));
SQL sql = insert.toSQL();
System.out.println("SQL: " + sql.toString());
System.out.println("参数: " + sql.params().params());
执行结果:
SQL: INSERT INTO new_table SELECT * FROM user WHERE age >= ?
参数: [20]
示例四:执行插入
int effectCount = Insert.create(UserEntity.class)
.field(UserEntity.FIELDS.ID).param(UUIDUtils.UUID())
.field(UserEntity.FIELDS.USERNAME).param("suninformation")
.field(UserEntity.FIELDS.NICKNAME).param("有理想的鱼")
.field(UserEntity.FIELDS.PASSWORD).param("123456")
.field(UserEntity.FIELDS.EMAIL).param("suninformation@163.com")
.execute();
System.out.println("本次操作影响记 录数:" + effectCount);
Update:更新语句对象
用于构建 UPDATE 数据更新语句。
示例一:常规更新
Update update = Update.create().table(UserEntity.class).field(UserEntity.FIELDS.AGE).param(18)
.where(Cond.create().lt(UserEntity.FIELDS.AGE).param(18));
SQL sql = update.toSQL();
System.out.println("SQL: " + sql.toString());
System.out.println("参数: " + sql.params().params());
执行结果:
SQL: UPDATE user SET age = ? WHERE age < ?
参数: [18, 18]
示例二:跨表更新
Update update = Update.create().table(UserEntity.class, "u").field("u", UserEntity.FIELDS.AGE).param(18)
.table("user_ext", "ue").field("ue", "country").param("CN")
.leftJoin("user_ext", "ue", Cond.create().eqField("ue.id", Fields.field("u", UserEntity.FIELDS.ID)))
.where(Cond.create().lt("u", UserEntity.FIELDS.AGE).param(18));
SQL sql = update.toSQL();
System.out.println("SQL: " + sql.toString());
System.out.println("参数: " + sql.params().params());
执行结果:(为方便阅读,此处美化了 SQL 的输出格式)
SQL: UPDATE user u, user_ext ue
LEFT JOIN user_ext ue ON ue.id = u.id
SET u.age = ?, ue.country = ? WHERE age < ?
参数: [18, CN, 18]
示例三:执行更新
int effectCount = Update.create().table(UserEntity.class).field(UserEntity.FIELDS.AGE).param(18)
.where(Cond.create().lt("u", UserEntity.FIELDS.AGE).param(18)).execute();
System.out.println("本次操作影响记录数:" + effectCount);
Delete:删除语句对象
用于构建 DELETE 数据删除语句。
示例一:单表删除
Delete delete = Delete.create()
.from(UserEntity.class)
.where(Cond.create().lt(UserEntity.FIELDS.AGE).param(18));
SQL sql = delete.toSQL();
System.out.println("SQL: " + sql.toString());
System.out.println("参数: " + sql.params().params());
执行结果:
SQL: DELETE FROM user WHERE age < ?
参数: [18]
示例二:多表数据删除
Delete delete = Delete.create()
.table("u")
.table("ue")
.from(UserEntity.class, "u")
.leftJoin("user_ext", "ue", Cond.create()
.eqField("ue.id", Fields.field("u", UserEntity.FIELDS.ID)))
.where(Cond.create().lt("u", UserEntity.FIELDS.AGE).param(18));
SQL sql = delete.toSQL();
System.out.println("SQL: " + sql.toString());
System.out.println("参数: " + sql.params().params());
执行结果:
SQL: DELETE u, ue FROM user u LEFT JOIN user_ext ue ON ue.id = u.id WHERE u.age < ?
参数: [18]
示例三:执行删除
int effectCount = Delete.create()
.from(UserEntity.class)
.where(Cond.create().lt(UserEntity.FIELDS.AGE).param(18)).execute();
System.out.println("本次操作影响记录数:" + effectCount);
Join:连接对象
用于生成 SQL 语句中的 JOIN 子句,支持 LEFT、RIGHT 和 INNER 连接方式。
示例代码:
Join join = Join.inner("user_ext").alias("ue").on(Cond.create().eqField("ue.uid", "u.id"));
Select select = Select.create("user", "u").join(join)
.where(Cond.create()
.gtEq("u", "age").param(18)
.and().isNotNull("ue", "type"));
SQL sql = select.toSQL();
System.out.println("SQL: " + sql.toString());
System.out.println("参数: " + sql.params().params());
执行结果:
SQL: SELECT * FROM user u INNER JOIN user_ext ue ON ue.uid = u.id WHERE u.age >= ? AND ue.type IS NOT NULL
参数: [18]
Union:联合对象
用于配合 SELECT 查询语句对象对 UNION 或 UNION ALL 子句的支持,其目的是将多个 SELECT 语句关联在一起,仅做为参数使用。
示例代码:
Select select = Select.create("user").where(Where.create(Cond.create().eq("dept").param("IT")))
.union(Union.create(Select.create("user").where(Where.create(Cond.create().lt("age").param(18)))).all());
SQL sql = select.toSQL();
System.out.println("SQL: " + sql.toString());
System.out.println("参数: " + sql.params().params());
执行结果:
SQL: SELECT * FROM user WHERE dept = ? UNION ALL SELECT * FROM user WHERE age < ?
参数: [IT, 18]
SQL:自定义SQL语句
它是对象查询的基础组件,为各种 SQL 语句提供执行能力。
示例代码:
SQL sql = SQL.create("select * from user where age > ? and username like ?").param(18).param("%ymp%");
// 执行查询:按指定的结果集类型返回
IResultSet<Object[]> resultSet = sql.find(IResultSetHandler.ARRAY.create());
// 计算记录数量:返回符合条件的记录数
long count = sql.count();
// 执行更新类操作:返回被影响记录数量
int effectCount = sql.execute();
BatchSQL:批量SQL语句对象
与 SQL 对象一样属于对象查询的基础组件,主要用于批量更新类 SQL 语句的执行和参数对象的封装。
示例代码一: 基本使用方法
// 构建SQL插入语句:INSERT INTO user (id, username, nickname, password, email) VALUES (?, ?, ?, ?, ?)
Insert insert = Insert.create(UserEntity.class)
.field(Fields.create(UserEntity.FIELDS.ID,
UserEntity.FIELDS.USERNAME,
UserEntity.FIELDS.NICKNAME,
UserEntity.FIELDS.PASSWORD,
UserEntity.FIELDS.EMAIL));
// 构建批处理SQL对象(此处通过Insert对象构建,也可以直接书写SQL语句)
BatchSQL batchSQL = BatchSQL.create(insert)
// 添加批参数
.addParameter(Params.create("1", "用户A", "昵称A", "密码A", "邮件A"))
.addParameter(Params.create("2", "用户B", "昵称B", "密码B", "邮件B"))
// 可以添加额外的SQL语句(注意:非预编译,即不支持使用问号'?'占位和参数值传递)
.addSQL("DELETE FROM user WHERE age > 30")
.addSQL("DELETE FROM user WHERE age < 18");
// 执行批处理并返回每条SQK受影响记录数的数组
int[] effectCounts = batchSQL.execute();
// 可以通过此方法计算实际受影响的记录总数
int effectCount = BatchUpdateOperator.parseEffectCounts(effectCounts);
示例代码二: 读取并执行SQL脚本文件
Transactions.execute(() -> {
IDatabase database = JDBC.get();
int effectCount = database.openSession(session -> {
String dialectName = session.getConnectionHolder().getDialect().getName();
String filename = String.format("db-init_%s.sql", dialectName);
List<String> scripts = BatchSQL.loadSQL(filename);
if (scripts.isEmpty()) {
scripts = BatchSQL.loadSQL("db-init.sql");
}
return BatchSQL.execSQL(database, scripts);
});
});
EntitySQL:实体参数封装对象
主要用于使用会话(Session)执行数据实体查询时的条件及参数的封装。
示例代码:
IResultSet<UserEntity> users = JDBC.get().openSession(new IDatabaseSessionExecutor<IResultSet<UserEntity>>() {
public IResultSet<UserEntity> execute(IDatabaseSession session) throws Exception {
return session.find(EntitySQL.create(UserEntity.class)
.field(Fields.create(UserEntity.FIELDS.ID, UserEntity.FIELDS.PASSWORD)),
Where.create(Cond.create()
.eq(UserEntity.FIELDS.USERNAME).param("suninformation").and()
.eq(UserEntity.FIELDS.PASSWORD).param(DigestUtils.md5Hex("654321")))
.orderByDesc(UserEntity.FIELDS.CREATE_TIME),
Page.create().pageSize(10));
}
});
Func:函数
在编写 SQL 语句时,经常会用到由数据库 提供(或根据业务自定义)的一系列函数来处理数据查询等操作,为了能够像编写 Java 代码一样在对象查询的 SQL 语句中使用函数,JDBC模块提供了一种简单的函数封装方法,同时也封装了一些比较常用的函数(目前这些函数封装主要针对 MySQL 数据库实现,其它类型数据库的支持也将在未来版本中逐步完善),其中主要包括:常规运算、数学计算、字符操作、聚合分组、日期时间和流程控制等相关函数。
常规运算函数:Func.Operators
| 函数 | 代码 | 描述 |
|---|---|---|
| brackets | Func.operators.brackets("x") | 括号 |
| quotes | Func.operators.quotes("x") | 引号 |
| addition | Func.operators.addition(n)Func.operators.addition("x")Func.operators.addition("x", n)Func.operators.addition("x", 'y') | 加法 |
| subtract | Func.operators.subtract(n)Func.operators.subtract("x")Func.operators.subtract("x", n)Func.operators.subtract(n, 'x')Func.operators.subtract("x", 'y') | 减法 |
| multiply | Func.operators.multiply(n)Func.operators.multiply("x")Func.operators.multiply("x", n)Func.operators.multiply("x", 'y') | 乘法 |
| divide | Func.operators.divide(n)Func.operators.divide("x")Func.operators.divide("x", n)Func.operators.divide(n, 'x')Func.operators.divide("x", 'y') | 除法 |
数学计算类函数:Func.Math
| 函数 | 代码 | 描述 |
|---|---|---|
| ABS | Func.math.ABS("x") | X 的绝对值 |
| ACOS | Func.math.ACOS("x") | X 的反余弦 |
| ASIN | Func.math.ASIN("x") | X 的反正弦 |
| ATAN | Func.math.ATAN("x") | X 的反正切 |
| CEILING | Func.math.CEILING("x") | 不小于 X 的最小整数值 |
| CONV | Func.math.CONV("x", 10, 2) | 进制转换 |
| COS | Func.math.COS("x") | X 的余弦 |
| COT | Func.math.COT("x") | X 的余切 |
| CRC32 | Func.math.CRC32("x") | 计算循环冗余码校验值并返回一个32比特无符号值 |
| DEGREES | Func.math.DEGREES("x") | X 由弧度被转化为度 |
| EXP | Func.math.EXP("x") | e 的 X 乘方后的值(自然对数的底) |
| FLOOR | Func.math.FLOOR("x") | 不大于 X 的最大整数值 |
| LN | Func.math.LN("x") | X 的自然对数,即 X 相对于基数 e 的对数 |
| LOG | Func.math.LOG("x")Func.math.LOG("b", x") | X 的自然对数 X 对于任意基数 B 的对数 |
| LOG10 | Func.math.LOG10("x") | |
| LOG2 | Func.math.LOG2("x") | |
| MOD | Func.math.MOD("n", "m") | N 被 M 除后的余数 |
| PI | Func.math.PI() | PI 的值(默认的显示小数位数是7位) |
| POW | Func.math.POW("y", x") | X 的 Y 乘方的结果值 |
| POWER | Func.math.POWER("y", x") | |
| RADIANS | Func.math.RADIANS("x") | 由度转化为弧度的参数 X |
| RAND | Func.math.RAND()Func.math.RAND("n") | 随机浮点值 v ,范围在 0 到1 之间 (即, 其范围为 0 ≤ v ≤ 1.0) 指定一个整数参数 N ,它被用作种子值,用来产生重复序列 |
| ROUND | Func.math.ROUND("x")Func.math.ROUND("x", d) | X 值接近于最近似的整数 X 值保留到小数点后 D 位并四舍五入 (若要直接保留 X 值小数点左边的 D 位,可将 D 设为负值) |
| SIGN | Func.math.SIGN("n") | X 值的符号(负数、零或正)对应 -1,0或1 |
| SIN | Func.math.SIN("x") | X 的正弦 |
| SQRT | Func.math.SQRT("x") | 非负数 X 的二次方根 |
| TAN | Func.math.TAN("x") | X 的正切 |
| TRUNCATE | Func.math.TRUNCATE("x")Func.math.TRUNCATE("x", d) | 舍去至小数点后 D 位的数字 X 若 D 的值为 0,则结果不带有小数点或不带有小数部分 可以将 D 设为负数,若要截去 X 小数点左起第 D 位开始后面所有低位的值 |
字符操作类函数:Func.Strings
| 函数 | 代码 | 描述 |
|---|---|---|
| ASCII | Func.strings.ASCII(str) | 字符串 str 的最左字符的数值 |
| BIN | Func.strings.BIN(n) | N 的二进制值的字符串表示 |
| BIN_LENGTH | Func.strings.BIN_LENGTH(str) | 二进制的字符串 str 长度 |
| CHAR | Func.strings.CHAR(...n) | 将 n 个整数代码所对应的字符组成的字符串 |
| CHAR_LENGTH | Func.strings.CHAR_LENGTH(str) | 字符串 str 的长度,长度的单位为字符 |
| CHARACTER_LENGTH | Func.strings.CHARACTER_LENGTH(str) | 与 CHAR_LENGTH 同 |
| CONCAT | Func.strings.CONCAT(str1, str2...n) | 连接多个 str 参数产生的字符串 |
| CONCAT_WS | Func.strings.CONCAT_WS(sp, str1, str2...n) | 与 CONCAT 同,第一个参数用于指定分隔符 |
| ELT | Func.strings.ELT(n, str1...n) | 返回第 N 个字符串 |
| FIELD | Func.strings.FIELD(str, ...n) | 返回 str 在列表中的位置索引 |
| FIND_IN_SET | Func.strings.FIND_IN_SET(x, strlist) | 返回字符串 x 在字符串列表 strlist 中的位置 |
| FORMAT | Func.strings.FORMAT(x, d) | 将数字 x 格式化为 '#,###,###.##' 格式,保留 d 位小数 |
| HEX | Func.strings.HEX(str) | 将字符串 str 转换为十六进制 |
| FROM_BASE64 | Func.strings.FROM_BASE64(str) | 将 Base64 编码的字符串解码 |
| TO_BASE64 | Func.strings.TO_BASE64(str) | 将字符串 str 编码为 Base64 |
| INSERT | Func.strings.INSERT(str, pos, len, newstr) | 在字符串 str 的 pos 位置插入 newstr,替换 len 个字符 |
| INSTR | Func.strings.INSTR(str, substr) | 字符串 str 中子字符串的第一个出现位置 |
| LEFT | Func.strings.LEFT(str, len) | 字符串 str 开始的 len 最左字符 |
| LENGTH | Func.strings.LENGTH(str) | 字符串 str 的长度,单位为字节 |
| LOAD_FILE | Func.strings.LOAD_FILE(str) | 读取文件并将这一文件按照字符串的格式返回 |
| LOCATE | Func.strings.LOCATE(substr, str)Func.strings.LOCATE(substr, str, pos) | 返回子字符串 substr 在字符串 str 中第一次出现的位置 |
| LOWER | Func.strings.LOWER(str) | 返回字符串 str,以及根据最新字符集映射转化为小写字母的字符 |
| LPAD | Func.strings.LPAD(str, len, padstr) | 返回字符串 str,其左边被字符串 padstr 填补至 len 字符长度 |
| LTRIM | Func.strings.LTRIM(str) | 返回字符串 str,起始空格字符被删去 |
| OCT | Func.strings.OCT(n) | N 的八进制值的字符串表示 |
| ORD | Func.strings.ORD(str) | 若字符串 str 的最左字符是一个多字节字符, 则返回该字符的代码 |
| QUOTE | Func.strings.QUOTE(str) | 引证一个字符串,由此产生一个在 SQL 语句中可用作完全转义数据值的结果 |
| REPEAT | Func.strings.REPEAT(str, count) | 返回一个由重复的字符串 str 组成的字符串,重复次数为 count |
| REPLACE | Func.strings.REPLACE(str, fromStr, toStr) | 替换字符串 str 中所有的 fromStr 为 toStr |
| REVERSE | Func.strings.REVERSE(str) | 返回字符串 str,顺序和字符顺序相反 |
| RIGHT | Func.strings.RIGHT(str, len) | 从字符串 str 开始,返回最右 len 字符 |
| RPAD | Func.strings.RPAD(str, len, padstr) | 返回字符串 str,其右边被字符串 padstr 填补至 len 字符长度 |
| RTRIM | Func.strings.RTRIM(str) | 返回字符串 str,结尾空格字符被删去 |
| SOUNDEX | Func.strings.SOUNDEX(str) | 从 str 返回一个 soundex 字符串 |
| SPACE | Func.strings.SPACE(n) | 返回一个由 N 间隔符号组成的字符串 |
| STRCMP | Func.strings.STRCMP(expr1, expr2) | 若所有的字符串均相同,则返回 0,若第一个参数小于第二个,则返回 -1,其它情况返回 1 |
| SUBSTRING | Func.strings.SUBSTRING(str, pos)Func.strings.SUBSTRING(str, pos, len) | 从字符串 str 返回一个子字符串,起始于位置 pos,可选长度 len |
| SUBSTRING_INDEX | Func.strings.SUBSTRING_INDEX(str, delim, count) | 在定界符 delim 以及 count 出现前,从字符串 str 返回子字符串 |
| TRIM | Func.strings.TRIM(str) | 返回字符串 str,其中所有前缀 和/或后缀都已被删除 |
| TRIM_BOTH | Func.strings.TRIM_BOTH(remstr, str) | 返回字符串 str,其中所有 remstr 前缀和后缀都已被删除 |
| TRIM_LEADIN | Func.strings.TRIM_LEADIN(remstr, str) | 返回字符串 str,其中所有 remstr 前缀都已被删除 |
| TRIM_TRAILING | Func.strings.TRIM_TRAILING(remstr, str) | 返回字符串 str,其中所有 remstr 后缀都已被删除 |
| UNHEX | Func.strings.UNHEX(str) | 执行从 HEX(str) 的反向操作,将十六进制数字转化为字符 |
| UPPER | Func.strings.UPPER(str) | 返回字符串 str,以及根据最新字符集映射转化为大写字母的字符 |
| REGEXP_INSTR | Func.strings.REGEXP_INSTR(str, pattern)Func.strings.REGEXP_INSTR(str, pattern, pos, occurrence, return_end_opt, match_type) | 返回字符串 str 中匹配正则表达式 pattern 的子串的起始位置 |
| REGEXP_LIKE | Func.strings.REGEXP_LIKE(str, pattern)Func.strings.REGEXP_LIKE(str, pattern, match_type) | 检查字符串 str 是否匹配正则表达式 pattern |
| REGEXP_REPLACE | Func.strings.REGEXP_REPLACE(str, pattern, replacement)Func.strings.REGEXP_REPLACE(str, pattern, replacement, pos, occurrence, match_type) | 替换字符串 str 中匹配正则表达式 pattern 的子串 |
| REGEXP_SUBSTR | Func.strings.REGEXP_SUBSTR(str, pattern)Func.strings.REGEXP_SUBSTR(str, pattern, pos, occurrence, match_type) | 返回字符串 str 中匹配正则表达式 pattern 的子串 |
聚合分组类函数:Func.Aggregate
| 函数 | 代码 | 描述 |
|---|---|---|
| AVG | Func.aggregate.AVG(expr)Func.aggregate.AVG(distinct, expr)Func.aggregate.AVG(expr, over) | 返回 expr 的平均值,DISTINCT 选项可用于返回不同值的平均值,支持窗口函数 |
| BIT_AND | Func.aggregate.BIT_AND(expr)Func.aggregate.BIT_AND(expr, over) | 返回 expr 中所有比特的按位与,计算执行的精确度为 64 比特 |
| BIT_OR | Func.aggregate.BIT_OR(expr)Func.aggregate.BIT_OR(expr, over) | 返回 expr 中所有比特的按位或,计算执行的精确度为 64 比特 |
| BIT_XOR | Func.aggregate.BIT_XOR(expr)Func.aggregate.BIT_XOR(expr, over) | 返回 expr 中所有比特的按位异或,计算执行的精确度为 64 比特 |
| COUNT | Func.aggregate.COUNT(expr)Func.aggregate.COUNT(distinct, expr)Func.aggregate.COUNT(expr, over) | 返回 SELECT 语句检索到的行中非 NULL 值的数目,支持窗口函数 |
| GROUP_CONCAT | Func.aggregate.GROUP_CONCAT(...expr)Func.aggregate.GROUP_CONCAT(distinct, ...expr)Func.aggregate.GROUP_CONCAT(distinct, orderBy, separator, ...expr) | 返回一个字符串结果,该结果由分组中的值连接而成,可指定排序和分隔符 |
| WM_CONCAT | Func.aggregate.WM_CONCAT(expr) | Oracle 数据库特有的聚合函数,用于连接字符串 |
| MAX | Func.aggregate.MAX(expr)Func.aggregate.MAX(distinct, expr)Func.aggregate.MAX(expr, over) | 返回 expr 的最大值,支持窗口函数 |
| MIN | Func.aggregate.MIN(expr)Func.aggregate.MIN(distinct, expr)Func.aggregate.MIN(expr, over) | 返回 expr 的最小值,支持窗口函数 |
| SUM | Func.aggregate.SUM(expr)Func.aggregate.SUM(distinct, expr)Func.aggregate.SUM(expr, over) | 返回 expr 的总数,若返回集合中无任何行则返回 NULL,支持窗口函数 |
日期时间类函数:Func.DateTime
| 函数 | 代码 | 描述 |
|---|---|---|
| ADDDATE | Func.dateTime.ADDDATE(expr, days) | 将 days 天数添加至 expr |
| ADDTIME | Func.dateTime.ADDTIME(expr, expr2) | 将 expr2 添加至 expr 然后返回结果 |
| CONVERT_TZ | Func.dateTime.CONVERT_TZ(dt, fromTz, toTz) | 将时间日期值 dt 从 fromTz 给出的时区转到 toTz 给出的时区 |
| CURDATE | Func.dateTime.CURDATE() | 将当前日期按照 'YYYY-MM-DD' 或 YYYYMMDD 格式的值返回 |
| CURTIME | Func.dateTime.CURTIME() | 将当前时间以 'HH:MM:SS' 或 HHMMSS 的格式返回 |
| DATE | Func.dateTime.DATE(expr) | 提取日期或时间日期表达式 expr 中的日期部分 |
| DATE_FORMAT | Func.dateTime.DATE_FORMAT(date, format) | 根据 format 字符串安排 date 值的格式 |
| DATEDIFF | Func.dateTime.DATEDIFF(expr, expr2) | 返回起始 时间 expr 和结束时间 expr2 之间的天数 |
| DAYNAME | Func.dateTime.DAYNAME(date) | 返回 date 对应的工作日名称 |
| DAYOFMONTH | Func.dateTime.DAYOFMONTH(date) | 返回 date 对应的该月日期,范围是从 1 到 31 |
| DAYOFWEEK | Func.dateTime.DAYOFWEEK(date) | 返回 date (1 = 周日, 2 = 周一, ..., 7 = 周六)对应的工作日索引 |
| DAYOFYEAR | Func.dateTime.DAYOFYEAR(date) | 返回 date 对应的一年中的天数,范围是从 1 到 366 |
| FROM_UNIXTIME | Func.dateTime.FROM_UNIXTIME(timestamp)Func.dateTime.FROM_UNIXTIME(timestamp, format) | 将 Unix 时间戳转换为日期格式 |
| UNIX_TIMESTAMP | Func.dateTime.UNIX_TIMESTAMP()Func.dateTime.UNIX_TIMESTAMP(date) | 返回 Unix 时间戳,或返回 date 参数以秒数的形式表示 |
| GET_FORMAT | Func.dateTime.GET_FORMAT(date, type) | 返回一个格式字符串 |
| HOUR | Func.dateTime.HOUR(time) | 返回 time 对应的小时数,范围是从 0 到 23 |
| LAST_DAY | Func.dateTime.LAST_DAY(date) | 获取一个日期或日期时间值,返回该月最后一天对应的值 |
| MAKEDATE | Func.dateTime.MAKEDATE(year, dayOfYear) | 给出年份值和一年中的天数值,返回一个日期 |
| MAKETIME | Func.dateTime.MAKETIME(hour, minute, second) | 返回由 hour、minute 和 second 参数计算得出的时间值 |
| MICROSECOND | Func.dateTime.MICROSECOND(expr) | 从时间或日期时间表达式 expr 返回微秒值,范围从 0 到 999999 |
| MINUTE | Func.dateTime.MINUTE(time) | 返回 time 对 应的分钟数,范围是从 0 到 59 |
| MONTH | Func.dateTime.MONTH(date) | 返回 date 对应的月份,范围是从 1 到 12 |
| MONTHNAME | Func.dateTime.MONTHNAME(date) | 返回 date 对应的月份名称 |
| NOW | Func.dateTime.NOW() | 返回当前日期和时间值,格式为 'YYYY-MM-DD HH:MM:SS' |
| PERIOD_ADD | Func.dateTime.PERIOD_ADD(p, n) | 为年-月组合日期 p 添加 n 个月 |
| PERIOD_DIFF | Func.dateTime.PERIOD_DIFF(p1, p2) | 返回 p1 和 p2 之间的月数 |
| QUARTER | Func.dateTime.QUARTER(date) | 返回 date 对应的一年中的季度值,范围是从 1 到 4 |
| SEC_TO_TIME | Func.dateTime.SEC_TO_TIME(seconds) | 返回被转化为小时、分钟和秒数的 seconds 参数值 |
| SECOND | Func.dateTime.SECOND(time) | 返回 time 对应的秒数,范围是从 0 到 59 |
| STR_TO_DATE | Func.dateTime.STR_TO_DATE(str, format) | DATE_FORMAT() 函数的倒转,将字符串转换为日期时间值 |
| SYSDATE | Func.dateTime.SYSDATE() | 返回当前日期和时间值 |
| TIME | Func.dateTime.TIME(expr) | 提取一个时间或日期时间表达式的时间部分 |
| TIME_FORMAT | Func.dateTime.TIME_FORMAT(time, format) | 其使用和 DATE_FORMAT() 函数相同,但仅处理时间格式 |
| TIME_TO_SEC | Func.dateTime.TIME_TO_SEC(time) | 返回已转化为秒的 time 参数 |
| TIMEDIFF | Func.dateTime.TIMEDIFF(expr, expr2) | 返回起始时间 expr 和结束时间 expr2 之间的时间差 |
| TIMESTAMP | Func.dateTime.TIMESTAMP(expr)Func.dateTime.TIMESTAMP(expr, expr2) | 将日期或日期时间表达式 expr 作为日期时间值返回 |
| TIMESTAMPDIFF | Func.dateTime.TIMESTAMPDIFF(unit, datetimeExpr1, datetimeExpr2) | 返回日期或日期时间表达式之间的整数差 |
| TO_DAYS | Func.dateTime.TO_DAYS(date) | 给定一个日期 date,返回一个天数(从年份 0 开始的天数) |
| UTC_DATE | Func.dateTime.UTC_DATE() | 返回当前 UTC 日期值,格式为 'YYYY-MM-DD' 或 YYYYMMDD |
| UTC_TIME | Func.dateTime.UTC_TIME() | 返回当前 UTC 时间值,格式为 'HH:MM:SS' 或 HHMMSS |
| UTC_TIMESTAMP | Func.dateTime.UTC_TIMESTAMP() | 返回当前 UTC 日期及时间值 |
| WEEK | Func.dateTime.WEEK(date)Func.dateTime.WEEK(date, mode) | 返回 date 对应的星期数 |
| WEEKDAY | Func.dateTime.WEEKDAY(date) | 返回 date (0 = 周一, 1 = 周二, ... 6 = 周日)对应的工作日索引 |
| WEEKOFYEAR | Func.dateTime.WEEKOFYEAR(date) | 将该日期的阳历周以数字形式返回,范围是从 1 到 53 |
| YEAR | Func.dateTime.YEAR(date) | 返回 date 对应的年份,范围是从 1000 到 9999 |
| YEARWEEK | Func.dateTime.YEARWEEK(date)Func.dateTime.YEARWEEK(date, mode) | 返回一个日期对应的年或周 |
控制流函数:Func.ControlFlow
| 函数 | 代码 | 描述 |
|---|---|---|
| CASE | Func.controlFlow.CASE(whenFn[])Func.controlFlow.CASE(value, whenFn[])Func.controlFlow.CASE(value, whenFn[], elseFn) | CASE 表达式,根据条件返回不同的值 |
| WHEN | Func.controlFlow.WHEN(expr)Func.controlFlow.WHEN(expr, result) | CASE 表达式中的 WHEN 子句 |
| ELSE | Func.controlFlow.ELSE()Func.controlFlow.ELSE(result) | CASE 表达式中的 ELSE 子句 |
| IF | Func.controlFlow.IF(expr1, expr2, expr3) | 如果 expr1 为 TRUE,则返回 expr2,否则返回 expr3 |
| IFNULL | Func.controlFlow.IFNULL()Func.controlFlow.IFNULL(expr1, expr2) | 如果 expr1 不为 NULL,则返回 expr1,否则返回 expr2 |
| NULLIF | Func.controlFlow.NULLIF()Func.controlFlow.NULLIF(expr1, expr2) | 如果 expr1 = expr2,则返回 NULL,否则返回 expr1 |
比较函数:Func.Comparison
@since 2.1.4
| 函数 | 代码 | 描述 |
|---|---|---|
| BETWEEN | Func.comparison.BETWEEN(min, max) | 检查值是否在指定范围内 |
| COALESCE | Func.comparison.COALESCE(value, ...values) | 返回参数列表中的第一个非 NULL 值 |
| EXISTS | Func.comparison.EXISTS(query) | 检查子查询是否返回任何行 |
| NOT_EXISTS | Func.comparison.NOT_EXISTS(query) | 检查子查询是否不返回任何行 |
| GREATEST | Func.comparison.GREATEST(value, ...values) | 返回参数列表中的最大值 |
| IN | Func.comparison.IN(value, ...values) | 检查值是否在指定列表中 |
| NOT_IN | Func.comparison.NOT_IN(value, ...values) | 检查值是否不在指定列表中 |
| IS | Func.comparison.IS(value) | 检查值是否为指定值(主要用于布尔值判断) |
| IS_NOT | Func.comparison.IS_NOT(value) | 检查值是否不为指定值 |
| IS_NULL | Func.comparison.IS_NULL() | 检查值是否为 NULL |
| IS_NOT_NULL | Func.comparison.IS_NOT_NULL() | 检查值是否不为 NULL |
| ISNULL | Func.comparison.ISNULL(value) | 检查值是否为 NULL(类似于 IS_NULL) |
| LEAST | Func.comparison.LEAST(value, ...values) | 返回参数列表中的最小值 |
窗口函数:Func.Window
@since 2.1.4
窗口函数用于在结果集的分区上执行计算,常用于排名、累计统计等场景。使用窗口函数时需要配合 WindowOver 对象来定义分区和排序规则。
排名窗口函数:
| 函数 | 代码 | 描述 |
|---|---|---|
| ROW_NUMBER | Func.window.ROW_NUMBER()Func.window.ROW_NUMBER(over) | 返回当前行在其分区中的行号,从 1 开始 |
| RANK | Func.window.RANK()Func.window.RANK(over) | 返回当前行在其分区中的排名,相同值的行获得相同排名,排名会有跳跃 |
| DENSE_RANK | Func.window.DENSE_RANK()Func.window.DENSE_RANK(over) | 返回当前行在其分区中的排名,相同值的行获得相同排名,排名不会有跳跃 |
| PERCENT_RANK | Func.window.PERCENT_RANK()Func.window.PERCENT_RANK(over) | 返回当前行在其分区中的相对排名(0 到 1 之间) |
| CUME_DIST | Func.window.CUME_DIST()Func.window.CUME_DIST(over) | 返回当前行在其分区中的累积分布值(0 到 1 之间) |
| NTILE | Func.window.NTILE(num_buckets)Func.window.NTILE(num_buckets, over) | 将分区中的行分成指定数量的桶,并返回当前行所在的桶号 |
偏移窗口函数:
| 函数 | 代码 | 描述 |
|---|---|---|
| LAG | Func.window.LAG(expr)Func.window.LAG(expr, offset)Func.window.LAG(expr, offset, default_value, over) | 返回分区中当前行之前第 offset 行的值 |
| LEAD | Func.window.LEAD(expr)Func.window.LEAD(expr, offset)Func.window.LEAD(expr, offset, default_value, over) | 返回分区中当前行之后第 offset 行的值 |
| FIRST_VALUE | Func.window.FIRST_VALUE(expr)Func.window.FIRST_VALUE(expr, over) | 返回分区中第一行的值 |
| LAST_VALUE | Func.window.LAST_VALUE(expr)Func.window.LAST_VALUE(expr, over) | 返回分区中最后一行的值 |
| NTH_VALUE | Func.window.NTH_VALUE(expr, n)Func.window.NTH_VALUE(expr, n, over) | 返回分区中第 n 行的值 |
窗口函数使用示例:
// 创建 WindowOver 对象
WindowOver over = WindowOver.create()
.partitionBy(UserEntity::getDept) // 按部门分区
.orderByDesc(UserEntity::getSalary); // 按薪资降序排序
// 使用 ROW_NUMBER 进行排名
Select select = Select.create(UserEntity.class)
.field(UserEntity::getId)
.field(UserEntity::getUsername)
.field(Func.window.ROW_NUMBER(over), "rank");
// 使用 LAG 获取上一行数据
Select select = Select.create(SalesEntity.class)
.field(SalesEntity::getMonth)
.field(SalesEntity::getAmount)
.field(Func.window.LAG(SalesEntity::getAmount, 1, "0",
WindowOver.create().orderByAsc(SalesEntity::getMonth)), "prev_amount");
如何自定义函数封装?
示例一:创建单个参数风格的函数封装,如:ABS(X)
IFunction ABS(String x) {
AbstractFunction func = Func.create("ABS");
func.param(x);
return func;
}
示例二:创建多个参数风格的函数封装,如:FORMAT(X, D)
IFunction FORMAT(String x, Number d) {
AbstractFunction func = Func.create("FORMAT");
func.param(x).separator().param(d);
return func;
}
示例二:创建表达式风格的函数封装,如:CASE value WHEN exp1 THEN result1 ELSE result2 END
IFunction CASE(String value, IFunction[] whenFn, String elseFn) {
return new AbstractFunction() {
@Override
public void onBuild() {
field("CASE ");
if (StringUtils.isNotBlank(value)) {
field(value).space();
}
Arrays.stream(whenFn).forEach(func -> field(func).space());
if (StringUtils.isNotBlank(elseFn)) {
field(elseFn).space();
}
field("END");
}
};
}
IFunction WHEN(String expr, String result) {
return new AbstractFunction() {
@Override
public void onBuild() {
field("WHEN ").field(expr).space().field("THEN ").field(result).space();
}
};
}
IFunction ELSE(String result) {
return new AbstractFunction() {
@Override
public void onBuild() {
field("ELSE ").field(result).space();
}
};
}
对象查询的另一种写法
由于对象查询中使用的各种类构造方法大部份都需要传递 IDatabase 和数据源名称等对象,在编写比较复杂的逻辑时,代码会很冗余,因此 JDBC 持久化模块特别提供了 QueryBuilder 类使能够参数重用和简化查询对象类的创建过程,减少代码冗余。
下面的示例是通过简单的复合查询来对比两种方式的不同之处:
IDatabase owner = JDBC.get();
String dsName = "oracledb";
// 普通写法
IResultSet<Object[]> resultSet = Select.create(owner, dsName, UserEntity.class, "u")
.join(Join.left(owner, dsName, UserExtEntity.TABLE_NAME).alias("ue")
.on(Cond.create(owner, dsName)
.eqField(Fields.field("u", UserEntity.FIELDS.ID), Fields.field("ue", UserExtEntity.FIELDS.UID))))
.field(Fields.create()
.add("u", UserEntity.FIELDS.ID)
.add("u", UserEntity.FIELDS.USERNAME)
.add("ue", UserExtEntity.FIELDS.MONEY))
.find(IResultSetHandler.ARRAY.create());
// 另一种写法
IResultSet<Object[]> resultSet = new QueryBuilder<IResultSet<Object[]>>(owner, dsName) {{
Select select = select(UserEntity.class, "u")
.join(left(UserExtEntity.TABLE_NAME).alias("ue")
.on(cond().eqField(field("u", UserEntity.FIELDS.ID), field("ue", UserExtEntity.FIELDS.UID))))
.field("u", fields(UserEntity.FIELDS.ID, UserEntity.FIELDS.USERNAME))
.field(field("ue", UserExtEntity.FIELDS.MONEY));
// 返回最终结果
build(select.find(IResultSetHandler.ARRAY.create()));
}}.build();
Lambda表达式 支持
从 v2.1.4 版本开始,JDBC 持久化模块引入了对 Lambda 表达式的支持,允许开发者使用方法引用的方式编写类型安全的查询语句,避免了硬编码字段名带来的风险,同时提高了代码的可读性和可维护性。
LambdaUtils 核心实现
Lambda 表达式支持的核心是 LambdaUtils 类,它提供了一系列函数式接口和工具方法,用于解析 Lambda 表达式获取字段名、数据库列名和其他相关信息:
函数式接口
SFunction<T, R>:序列化的函数式接口,用于支持 Lambda 表达式和方法引用EntityFunction<T extends IEntity<?>, R>:实体函数式接口,用于实体类的方法引用PkFunction<T extends IEntityPK, R>:主键函数式接口,用于复合主键类的方法引用SSupplier<T>:序列化的供应商接口,用于支持无参方法引用SBinaryFunction<T, U, R>:序列化的双函数接口,用于支持两个参数的方法引用
工具方法
getFieldName(SFunction<T, R> func):从方法引用中解析出字段名getColumnName(SFunction<T, R> func):从方法引用中解析出数据库字段名(带缓存机制)getEntityName(EntityFunction<T, R> func):从实体方法引用中获取实体名称getEntityName(Class<? extends IEntity> entityClass):获取实体名称getFullFieldName(String prefix, SFunction<T, R> func):获取带前缀的完整字段名getValue(SSupplier<T> supplier):从供应商函数中获取值getValue(SBinaryFunction<T, U, R> func, T t, U u):从双函数中获取值getTargetClass(SFunction<T, R> func):从方法引用中获取目标类getTargetClass(EntityFunction<T, R> func):从实体方法引用中获取目标类getTargetClass(PkFunction<T, R> func):从主键方法引用中获取目标类
缓存机制
LambdaUtils 类内部使用了两级缓存机制,提高解析性能:
FIELD_NAME_CACHE:缓存字段名到数据库列名的映射关系,按目标类分组LAMBDA_CACHE:缓存 Lambda 表达式的序列化信息,避免重复反射解析
这种缓存机制确保了在频繁调用时,Lambda 表达式的解析性能得到显著提升。
字段选择
使用 Lambda 表达式选择字段,支持单字段、多字段、带前缀和别名等多种方式:
// 基本字段选择
Select select = Select.create(UserEntity.class)
.field(UserEntity::getId)
.field(UserEntity::getUsername);
// 带别名的字段选择
select.field(UserEntity::getId, "user_id");
// 带前缀的字段选择
select.field("u", UserEntity::getEmail);
// 带前缀和别名的字段选择
select.field("u", UserEntity::getCreateTime, "create_date");
条件查询
使用 Lambda 表达式创建各种条件查询,支持等于、不等于、大于、小于等多种操作符:
// 等于条件
Cond cond = Cond.create()
.eq(UserEntity::getUsername, "admin")
.and().eq(UserEntity::getPassword, "123456");
// 范围条件
cond = Cond.create()
.gt(UserEntity::getAge, 18)
.and().lt(UserEntity::getAge, 30);
// 字段比较
cond = Cond.create()
.eq(UserEntity::getId, UserExtEntity::getUid);
// 带前缀的字段比较
cond = Cond.create()
.eq("u", UserEntity::getId, "ue", UserExtEntity::getUid);
// 模糊查询
cond = Cond.create()
.likeWrap(UserEntity::getUsername).param("%test%");
连接查询
使用 Lambda 表达式简化连接查询的编写:
// 内连接
Select select = Select.create(UserEntity.class, "u")
.innerJoin(UserExtEntity.class, "ue", UserEntity::getId, UserExtEntity::getUid);
// 左连接
select = Select.create(UserEntity.class, "u")
.leftJoin(UserExtEntity.class, "ue", UserEntity::getId, UserExtEntity::getUid);
// 带前缀的连接
select = Select.create(UserEntity.class, "u")
.innerJoin(UserExtEntity.class, "ue", "u", UserEntity::getId, "ue", UserExtEntity::getUid);
排序和分组
使用 Lambda 表达式创建排序和分组:
// 排序
Select select = Select.create(UserEntity.class)
.orderByAsc(UserEntity::getCreateTime)
.orderByDesc(UserEntity::getId);
// 带前缀的排序
select = Select.create(UserEntity.class, "u")
.orderByAsc("u", UserEntity::getUsername);
// 分组
select = Select.create(UserEntity.class)
.groupBy(UserEntity::getDept)
.having(Cond.create().gt(Func.aggregate.AVG(UserEntity::getSalary), 5000));
// 带前缀的分组
select = Select.create(UserEntity.class, "u")
.groupBy("u", UserEntity::getDept);
完整示例
下面是一个使用 Lambda 表达式的完整查询示例:
// 创建查询
Select select = Select.create(UserEntity.class, "u")
// 选择字段
.field("u", UserEntity::getId)
.field("u", UserEntity::getUsername)
.field("u", UserEntity::getAge)
.field("ue", UserExtEntity::getMoney, "salary")
// 左连接
.leftJoin(UserExtEntity.class, "ue", "u", UserEntity::getId, "ue", UserExtEntity::getUid)
// 查询条件
.where(Cond.create()
.gt("u", UserEntity::getAge, 18)
.and().eq("ue", UserExtEntity::getType, 1)
.and().likeWrap("u", UserEntity::getUsername).param("%test%"))
// 排序
.orderByAsc("u", UserEntity::getCreateTime)
.orderByDesc("u", UserEntity::getId);
// 执行查询
SQL sql = select.toSQL();
System.out.println("SQL: " + sql.toString());
System.out.println("参数: " + sql.params().params());
执行结果:
SQL: SELECT u.id, u.username, u.age, ue.money salary FROM user u LEFT JOIN user_ext ue ON u.id = ue.uid WHERE u.age > ? AND ue.type = ? AND u.username LIKE ? ORDER BY u.create_time ASC, u.id DESC
参数: [18, 1, %test%]
与传统查询方式的对比
| 特性 | 传统方式 | Lambda 表达式方式 |
|---|---|---|
| 类型安全 | 否,硬编码字段名 | 是,编译时检查 |
| 代码可读性 | 较低,需要手动拼接字段名 | 高,直观的方法引用 |
| 可维护性 | 低,字段名修改需要手动更新所有引用 | 高, 字段名修改自动更新所有引用 |
| 开发效率 | 较低,需要记忆字段名 | 高,IDE 自动补全支持 |
| 错误风险 | 高,容易拼写错误 | 低,编译时检查 |
性能考量
- 解析开销:Lambda 表达式的解析需要通过反射获取方法引用信息,首次调用会有一定的性能开销,但后续调用会使用缓存,性能影响很小
- 缓存机制:框架内部使用了 ConcurrentHashMap 对解析结果进行缓存,避免重复解析
- 运行时性能:生成的 SQL 语句与传统方式完全相同,运行时性能没有差异
- 内存占用:Lambda 表达式的缓存会占用一定的内存,但占用量很小,不会对系统性能造成影响
最佳实践建议
- 优先使用 Lambda 表达式:在新开发的代码中,优先使用 Lambda 表达式方式,提高代码的类型安全性和可维护性
- 合理使用缓存:框架会自动缓存解析结果,无需手动管理
- 注意方法引用的正确性:确保方法引用指向的是实体类的 getter 方法,否则会抛出 IllegalArgumentException
- 结合 QueryBuilder 使用:与 QueryBuilder 结合使用,可以进一步简化代码,提高开发效率
- 注意泛型类型:确保 Lambda 表达式的泛型类型正确,避免类型转换错误
- 复杂查询的处理:对于非常复杂的查询,可以根据实际情况选择传统方式或 Lambda 表达式方式
Lambda 表达式与 QueryBuilder 结合使用
将 Lambda 表达式与 QueryBuilder 结合使用,可以进一步简化代码:
IDatabase owner = JDBC.get();
String dsName = "oracledb";
IResultSet<Object[]> resultSet = new QueryBuilder<IResultSet<Object[]>>(owner, dsName) {{
Select select = select(UserEntity.class, "u")
.field("u", UserEntity::getId)
.field("u", UserEntity::getUsername)
.field("ue", UserExtEntity::getMoney)
.leftJoin(UserExtEntity.class, "ue", "u", UserEntity::getId, "ue", UserExtEntity::getUid)
.where(cond()
.gt("u", UserEntity::getAge, 18)
.and().eq("ue", UserExtEntity::getType, 1))
.orderByAsc("u", UserEntity::getCreateTime);
build(select.find(IResultSetHandler.ARRAY.create()));
}}.build();
通过 Lambda 表达式的支持,JDBC 持久化模块提供了更加现代化、类型安全的查询方式,使开发者能够更加高效地编写和维护数据库查询代码。
存储器(Repository)
为了能够更方便的维护和执行 SQL 语句,JDBC模块提供了存储器的支持,可以通过 @Repository 注解自定义 SQL 语句或从配置文件中加载 SQL 语句并自动执行。
@Repository
| 配置项 | 描述 |
|---|---|
| dsName | 数据源名称,默认为空 |
| item | 从资源文件中加载 item 指定的配置项,默认为空 |
| configFile | 资源文件路径名称,默认为空 |
| value | 自定义 SQL 配置,默认为空 |
| update | 是否为更新操作,默认为 false |
| page | 是否分页查询,默认为 false |
| useFilter | 是否调用方法过滤,默认为 false |
| dbType | 指定当前存储器适用的数据库类型,默认为全部,否则将根据数据库类型进行存储器加载 |
| resultClass | 指定结果集类型,若设置的类型为实体类型则使用 EntityResultSetHandler 否则使用 BeanResultSetHandler ,默认使用 ArrayResultSetHandler 处理结果集 |
- 存储器类通过声明
@Repository注解被框架自动扫描并加载; - 与其它被容器管理的
@Bean一样支持拦截器、事务、缓存等注解; - 当
useFilter=true时,存储器类方法的参数至少有一个参数(方法有多个参数时,采用最后一个参数)用于接收SQL执行结果; - 当
useFilter=false时,存储器方法体将不会被执行; - 当
page=true时,若useFilter=false则存储器类方法的最后一个参数必须是Page类型,否则Page参数必须位于结果集参数之前; - 查询类型 SQL 的执行结果数据类型默认为
IResultSet<Object[]>(取决于resultClass参数设置),而更新类型 SQL(即update=true时)的执行结果必须为int类型; - 用于接收 SQL 执行结果的方法参数支持变长类型,如:
IResultSet<Object[]> results和IResultSet<Object[]>... results的效果是一样的; - 读取配置文件中的 SQL 配置时,配置项名称必须全部采用小写字符,如:
demo_query; - 框架将优先加载以当前数据源连接的数据库类型名称作为后缀的配置项,如:
demo_query_mysql、demo_query_oracle,若找不到则加载默认名称,即:demo_query;
示例一:执行自定义SQL语句
@Repository
public class DemoRepository implements IRepository {
@Repository(value = "select * from user where type = ${type}", page = true)
public IResultSet<Object[]> execQuery(Integer type, Page page, IResultSet<Object[]> results) throws Exception {
// 此处代码将不会执行,因为注解配置中:useFilter=false
return results;
}
}
示例二:执行配置文件中的SQL语句
新增配置文件 demo.repo.xml,内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<properties>
<category name="default">
<property name="custom_query">
<value><![CDATA[select * from user where type = ${type}]]></value>
</property>
</category>
</properties>
在存储器中加载配置文件有两种方式,如下:
方式一: 通过 @Repository 注解的 configFile 属性指定具体配置文件路径名称(方法上的 @Repository 注解优先于类上的)。
@Repository(configFile = "cfg/demo.repo.xml")
public class DemoRepository implements IRepository {
@Repository(item = "custom_query", useFilter = true)
public List<UserEntity> execQuery(Integer type, IResultSet<Object[]>... results) throws Exception {
// 以下代码仅为了演示如何对结果集的再处理过程
final List<UserEntity> returnValues = new ArrayList<>();
if (results != null && results.length > 0) {
ResultSetHelper.bind(results[0]).forEach((wrapper, row) -> {
returnValues.add(wrapper.toEntity(new UserEntity()));
return true;
});
}
return returnValues;
}
}
方式二: 通过 IRepository 存储器接口提供的 getConfig 方法指定具体配置文件对象(其优先于注解方式)。
新增配置类用于加载 demo.repo.xml 配置文件:
@Configuration(value = "cfgs/demo.repo.xml")
public class DemoRepoConfig extends DefaultConfiguration {
}
重写存储器接口方法:
@Repository
public class DemoRepository implements IRepository {
@Inject
private DemoRepoConfig demoRepoConfig;
@Override
public IConfiguration getConfig() {
return demoRepoConfig;
}
@Repository(item = "custom_query", useFilter = true, resultClass = UserEntity.class)
public List<UserEntity> execQuery(Integer type, IResultSet<UserEntity>... results) throws Exception {
// 以下代码仅为了演示如何对结果集的再处理过程
final List<UserEntity> returnValues = new ArrayList<>();
if (results != null && results.length > 0) {
ResultSetHelper.bind(results[0]).forEach((wrapper, row) -> {
returnValues.add(wrapper.toEntity(new UserEntity()));
return true;
});
}
return returnValues;
}
}
示例三:执行动态SQL语句及数据过滤
目前, JDBC 持久化模块的存储器默认支持基于 JavaScript 和 Groovy 脚本语言实现的动态 SQL 语句拼装和数据过滤。
若使用 Groovy 脚本,需求在工程中添加如下依赖包配置:
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
<version>3.0.17</version>
<type>pom</type>
</dependency>
也可以通过 IRepositoryScriptProcessor 脚本处理器接口实现自定义脚本语言并通过 SPI 机制向模块注册(自定义脚本处理器接口实现类必须使用 @RepositoryScriptProcessor 注解指定脚本语言名称),接口结构及方法说明如下:
@RepositoryScriptProcessor(value = "lua")
public class LuaRepositoryScriptProcessor implements IRepositoryScriptProcessor {
/**
* 初始化脚本处理器
*
* @param scriptStatement 脚本代码段
* @throws Exception 可能产生的任何异常
*/
public void initialize(String scriptStatement) throws Exception {
// 此处编写自定义脚本引擎的初始化逻辑
}
/**
* 是否已初始化
*
* @return 返回true表示已初始化
*/
public boolean isInitialized() {
return true;
}
/**
* 执行处理器
*
* @param name 方法名称
* @param params 参数集合
* @return 返回最终预执行SQL语句
* @throws Exception 可能产生的任何异常
*/
@Override
public String process(String name, Object... params) throws Exception {
// 此处调用脚本中的具体方法实现SQL的动态拼装
return null;
}
/**
* 判断是否支持结果数据过滤
*
* @return 返回true表示支持结果数据过滤
*/
@Override
public boolean isFilterable() {
// 请根据不同的脚本语言特性判断是否支持,比如freemarker模板语言就不支持动态脚本的执行
return filterable;
}
/**
* 执行结果数据过滤
*
* @param results 待过滤结果对象
* @return 返回过滤后的结果对象
*/
@Override
public Object filter(Object results) {
// 此处调用脚本中的具体方法实现数据过滤
return null;
}
}
以 JavaScript 脚本语言为例,修改配置文件 demo.repo.xml 内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<properties>
<category name="default">
<!-- 动态SQL也支持通过JavaScript脚本处理, 如下所示: -->
<property name="custom_query" language="javascript">
<value><![CDATA[
// 方法名称要与name属性名称一致
function custom_query(type) {
var sqlStr = "select * from user";
if (type) {
sqlStr += " where type = ${type}";
}
// 方式一:直接返回拼装后的SQL字符串
// return sqlStr;
// 方式二:添加回调方法用于过滤查询结果集
return {
"sql": function() { return sqlStr },
"filter": function(results) {
var List = Java.type("java.util.ArrayList");
var result = new List();
if (results && results.isResultsAvailable) {
for (i = 0; i < results.resultData.length; i++) {
if (i % 2 == 0) {
result.add(results.resultData.get(i))
}
}
}
// 注意:返回的结果类型需与Java接口方法参数类型匹配
return result;
}
}
}
]]></value>
</property>
</category>
</properties>
JavaScript 翻译成 Groovy 脚本如下:import net.ymate.platform.core.persistence.IResultSet
def custom_query(int type) {
String sqlStr = "select * from user"
if (type) {
sqlStr += " where type = ${type}"
}
return [
sql: { return sqlStr },
filter: (IResultSet results) -> {
List result = new ArrayList<>()
if (results && results.isResultsAvailable()) {
List resultData = results.getResultData()
for (i = 0; i < resultData.size(); i++) {
if (i % 2 == 0) {
result.add(resultData.get(i))
}
}
}
return result
}
]
}
调整存储器类,此处请注意 execQuery 方法的最后一个参数的类型是 List,其与 JavaScript 脚本中指定的 filter 过滤器返回的类型须一致,否则将发生类型转换异常,代码如下:
@Repository(configFile = "cfg/demo.repo.xml")
public class DemoRepository implements IRepository {
@Repository(item = "custom_query", useFilter = true, resultClass = UserEntity.class)
public List<UserEntity> execQuery(Integer type, List<UserEntity>... results) throws Exception {
// 以下代码仅为了演示如何对结果集的再处理过程
final List<UserEntity> returnValues = new ArrayList<>();
if (results != null && results.length > 0) {
results[0].stream()
.filter(user -> BlurObject.bind(user.getAge()).toIntValue() > 18)
.forEach(returnValues::add);
}
return returnValues;
}
}
示例四:按数据库类型加载存储器实例
本例中 IDemoRepository 业务存储器接口类分别有 MySQL 和 Oracle 两种实现类,通过 @Repository 注解的 dsName 和 dbType 属性可以让框架在初始化时的自动扫描程序根据当前存储器实现所指定的数据源连接的数据库类型来判断是否加载,若数据源名称未设置则使用默认数据源,若数据库类型未设置则默认为支持全部数据库类型。
// 业务存储器接口类
public interface IDemoRepository extends IRepository {
......
}
// 基于MySQL数据库的业务存储器接口实现类
@Repository(dbType = Type.DATABASE.MYSQL)
public class DemoMySQLRepository implements IDemoRepository {
......
}
// 基于Oracle数据库的业务存储器接口实现类
@Repository(dsName = "oracledb", dbType = Type.DATABASE.ORACLE)
public class DemoOracleRepository implements IDemoRepository {
......
}