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.3</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 | 数据库引用标识符 | 
数据源(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使用,可选值:tablemarkdowncsv,默认为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));
// 或者插入前判断记录是否已存在,若已存在则执行记录更新操作
user.saveOrUpdate();
// 或者插入前判断记录是否已存在,若已存在则执行记录更新操作时仅更新指定的字段
user.saveOrUpdate(Fields.create(UserEntity.FIELDS.NICKNAME, UserEntity.FIELDS.EMAIL));
更新(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;
    }
});
查询(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 接口文档和相关源码。
结果集(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]