JDBC
JDBC 持久化模块针对关系型数据库(RDBMS)数据存取的一套简单解决方案,主要关注数据存取的效率、易用性、稳定和透明,其具备以下功能特征:
- 基于 JDBC 框架 API 进行轻量封装,结构简单、便于开发、调试和维护;
- 优化批量数据更新、标准化结果集、预编译 SQL 语句处理;
- 支持单实体 ORM 操作,无需编写 SQL 语句;
- 提供脚手架工具,快速生成数据实体类,支持链式调用;
- 支持通过存储器注解自定义 SQL 语句或从配置文件中动态加载 SQL 并自动执行;
- 支持结 果集与值对象的自动装配,支持自定义装配规则;
- 支持多数据源,默认支持 C3P0、DBCP、Druid、HikariCP、JNDI 连接池配置,支持数据源扩展;
- 支持基于
IDataSourceRouter的数据源路由(分库),既可通过openSession手动指定,也可通过@DataSourceRouter注解在类/方法/包上声明式配置,并内置路由实例与元数据缓存以提升性能;与基于IShardingRule/IShardingable的数据分片(分表)能力可组合使用; - 支持多种数据库(如:Oracle、MySQL、SQLServer、SQLite、H2、PostgreSQL 等);
- 支持面向对象的数据库查询封装,有助于减少或降低程序编译期错误;
- 支持数据库事务嵌套;
- 支持数据库视图和存储过程;
Maven包依赖
<dependency>
<groupId>net.ymate.platform</groupId>
<artifactId>ymate-platform-persistence-jdbc</artifactId>
<version>2.1.4</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
# 是否对输出的SQL参数值进行脱敏(屏蔽字符串与二进制类型的参数值),默认值: false
ymp.configs.persistence.jdbc.ds.default.mask_sql_params=
# 是否开启堆栈跟踪, 默认值: 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 语句 |
| maskSqlParams | 是否对输出的 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")
})
数据源路由(IDataSourceRouter)
在多数据源环境下,除了通过数据源名称直接获取连接持有者(database.getConnectionHolder(name))之外,还可以通过实现 net.ymate.platform.core.persistence.IDataSourceRouter 接口来动态决定当前会话操作所使用的数据源,这是实现「分库」路由的基础。
IDataSourceRouter 接口仅定义了一个方法:
/**
* 获取数据源名称
*
* @return 返回数据源名称
*/
String getDataSourceName();
将路由器对象传递给 JDBC.openSession(IDataSourceRouter, IDatabaseSessionExecutor) 方法,即可让整个会话内的所有数据库操作都路由到 getDataSourceName() 所返回的数据源上;此外,JDBC.openSession(IDataSourceRouter) 亦可返回一个绑定到指定数据源的会话对象。
示例: 根据用户 ID 取模选择数据源,实现水平分库。
// 1. 自定义数据源路由器
public class UserDataSourceRouter implements IDataSourceRouter {
private final String uid;
public UserDataSourceRouter(String uid) {
this.uid = uid;
}
@Override
public String getDataSourceName() {
// 根据用户ID取模选择数据源,实现水平分库
return Integer.parseInt(uid) % 2 == 0 ? "ds0" : "ds1";
}
}
// 2. 使用路由器开启会话
IDatabase database = JDBC.get();
List<UserEntity> result = database.openSession(new UserDataSourceRouter("1001"), session -> {
// 该会话内的所有数据库操作均路由到 ds1 数据源
return session.find(UserEntity.builder().build());
});
说明: 若
getDataSourceName()返回的数据源名称不存在,会话在获取连接时将抛出异常。请确保其返回的名称已在上文「多数据源连接」中正确配置。
通过 @DataSourceRouter 注解声明路由
除手动创建 IDataSourceRouter 实例并传递给 openSession 外,还可以通过 @DataSourceRouter 注解在 类、方法或包 上声明要使用的路由实现类,框架会在运行时按优先级自动解析并选取数据源。该方式常用于存储器(Repository)方法,使其无需关心会话开启逻辑即可动态路由:
// 1. 自定义无状态的数据源路由器
public class UserDataSourceRouter implements IDataSourceRouter {
@Override
public String getDataSourceName() {
// 根据实际业务上下文(如当前登录用户、请求参数等)动态返回数据源名称
return ...;
}
}
// 2. 在存储器类(或方法、包)上声明该路由器
// 方法级注解优先于类级、类级优先于包级
@Repository
@DataSourceRouter(UserDataSourceRouter.class)
public class DemoRepository implements IRepository {
@Repository(value = "select * from user where type = ${type}")
@DataSourceRouter(OtherDataSourceRouter.class) // 方法级路由优先级最高
public IResultSet<Object[]> execQuery(Integer type) throws Exception {
return null;
}
}
@DataSourceRouter 注解(net.ymate.platform.core.persistence.annotation.DataSourceRouter)说明:
- 作用目标:
TYPE(类)、METHOD(方法)、PACKAGE(包); - value:
Class<? extends IDataSourceRouter>类型,指定路由实现类,要求该实现类为无状态对象(相同实现类全局仅实例化一次,实例在类级别缓存中共享); - 解析优先级:
方法级→类级→包级,命中第一个非空注解即生效; - 缓存机制:框架内置多级缓存以提升性能——路由实现类实例以实现类为键全局共享;注解元素(类/方法/包)到路由 实例的映射采用弱引用键,避免强引用目标类导致类加载器无法回收;对于「未声明注解或实例化失败」的元素使用空哨兵标记,避免重复扫描与重复实例化尝试;
- 静态工具方法:
IDataSourceRouter.getDataSourceRouter(DataSourceRouter)、IDataSourceRouter.getDataSourceRouter(AnnotatedElement...)、IDataSourceRouter.getDataSourceName(AnnotatedElement...)已封装上述缓存与解析逻辑,各模块可直接复用。
说明: 在存储器中,数据源的最终选取顺序为:方法级
@Repository#dsName()→ 方法级@DataSourceRouter→ 类级@Repository#dsName()→ 类级@DataSourceRouter,以上均未指定时回退到默认数据源(详见下文「存储器(Repository)」章节)。
连接池配置
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();
}
}
}
}
数据源反注册(unregisterDataSource)
动态关闭并移除指定非默认数据源(释放连接池资源 + 移除配置),适用于运行时按需卸载数据源,例如插件卸载或租户下线。
示例:
// 反注册名称为 "oracledb" 的数据源(默认数据源 default 受保护,不可反注册)
JDBC.get().unregisterDataSource("oracledb");
- 当
dataSourceName为空或等于默认数据源名称(ds_default_name)时,将抛出IllegalArgumentException; - 内部关闭该数据源的连接池并释放资源,不再从
DriverManager全局注册表注销 JDBC 驱动(驱动为同一类加载器下的共享资源,注销会影响其他共用同驱动的数据源),因此反注册单个数据源不会影响其它数据源; - 该数据源将从运行时缓存与配置中一并移除,后续按该名称访问将视为不存在(不会被再次惰性重建);
- 反注册时请确保该数据源当前无活跃会话/事务持有其连接,否则
close()可能中断进行中的操作。
数据实体(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 |
@ShardingRule
声明数据实体类所使用的数据分片(分表)规则类,用于实现水平分表。
| 配置项 | 描述 |
|---|---|
| value | 数据分片(分表)规则实现类,需实现 net.ymate.platform.core.persistence.IShardingRule 接口 |
数据分片(Sharding)
数据分片用于解决单表数据量过大或单库性能瓶颈的问题,YMP 的 JDBC 持久化模块提供了「分库」与「分表」两套互补的能力:
- 分库(数据源路由):通过
IDataSourceRouter接口(详见上文「数据源路由」章节)将操作路由到不同的数据源; - 分表(数据分片):通过
IShardingRule/IShardingable接口在运行时根据分片参数动态计算实际的数据表名称。
参与分表计算的核心接口与类位于 net.ymate.platform.core.persistence 包中:
IShardingRule:分片(表)规则计算接口,负责根据分片参数计算最终的数据表名称;IShardingable:分片(表)能 力接口,负责提供用于分片规则计算的参数对象;ShardingElement<T>/ShardingList<T>:分片元素与分片列表包装类,用于批量跨分片操作。
1. 定义分片规则(IShardingRule)
IShardingRule 仅定义一个方法,框架在执行 SQL 时会调用它来替换原始表名:
/**
* 计算分片(表)名称
*
* @param originalName 原始分片(表)名称
* @param shardingParam 分片(表)辅助计算参数
* @return 返回分片(表)名称
*/
String getShardName(String originalName, Object shardingParam);
示例: 根据用户 ID 取模将 user 表拆分为 user_0、user_1、user_2、user_3 四张表。
public class UserTableShardingRule implements IShardingRule {
@Override
public String getShardName(String originalName, Object shardingParam) {
// shardingParam 通常为分片键(如用户ID)
int suffix = Math.abs(String.valueOf(shardingParam).hashCode()) % 4;
return originalName + "_" + suffix;
}
}