持久层、@Table、@JoinTable、多数据源、MyBatis
引言
持久层实体类采用 @Table注解配置,自动生成增删改通用SQL,不需要在mapper.xml里写重复又费时的SQL,遇见复杂的情况下支持扩展。而报表统计分析的情况下又能支持mybatis原生写法,在写sql的时候,又能调用之前实体配置的一些参数。从而减少开发和后期维护成本。
众多的持久层框架@Column注解定义都是分布到get或属性上,或者干脆直接使用属性作为字段名,这在JeeSite是不推荐的,JeeSite的实体不仅仅是物理实体,它是与Model实体结合的一个产物。综合考虑,将@Column所有定义到类头,而不是分布到各个属性或方法上,主要是有以下三点原因:
- 可一览熟知该实体类对应的物理表结构是什么样,引领开发者思维从物理表结构到对象的映射转换,都是基于物理表结构的,@Column中的name指定物理字段名,而不是指定类上的属性名,也是这个原因;
- 自动化生成的SQL和查询条件,是有序的,可方便核查定义,有利于优化查询性能;
- 方便@JoinTable关联表和其它扩展信息的设置,如果分布到类的属性上就需要来回滚动屏幕查找,不利于管理字段列。
简单举例
以定义员工实体举例,配置如下:(注意代码上的注释)
@Table(name="${_prefix}sys_employee", alias="a", columns={
// 支持Include,如:自动导入status、create_by、create_date等字段
@Column(includeEntity=BaseEntity.class),
@Column(includeEntity=DataEntity.class),
// 支持设置主键PK字段,调用get方法时自动加入主键唯一条件
@Column(name="emp_code", label="员工编码", isPK=true),
// 支持设置查询字段类型,如LIKE自动在查询值前后加 % 符号。
@Column(name="emp_name", label="名称", queryType=QueryType.LIKE),
@Column(name="emp_name_en", label="英文名", queryType=QueryType.LIKE),
// 字段名到Java属性名的转换,采用驼峰命名法规则自动进行转换
@Column(name="emp_no", label="工号"),
// 驼峰命名法转换不了的,支持设置特殊对象属性,如mapper.xml的sql中 a.office_code AS "office.officeCode" 的写法
@Column(name="office_code", attrName="office.officeCode", label="机构编码"),
@Column(name="office_name", attrName="office.officeName", label="机构名称", queryType=QueryType.LIKE),
@Column(name="company_code", attrName="company.companyCode", label="公司编码"),
@Column(name="company_name", attrName="company.companyName", label="公司名称", queryType=QueryType.LIKE),
@Column(name="sex", label="性别"),
@Column(name="birthday", label="生日"),
// 支持设置非查询字段,添加查询条件时忽略该字段
@Column(name="photo", label="员工照片", isQuery=false),
@Column(name="email", label="电子邮件"),
@Column(name="mobile", label="手机号码"),
@Column(name="phone", label="办公电话"),
@Column(name="fax", label="传真号码"),
@Column(name="qq", label="QQ号"),
@Column(name="weixin", label="微信号"),
@Column(name="stations", label="岗位"),
},
// 支持联合查询,如左右连接查询,支持设置查询自定义关联表的返回字段列
joinTable={
@JoinTable(type=Type.LEFT_JOIN, entity=Office.class, alias="o",
on="o.office_code = a.office_name",
columns={@Column(includeEntity=Office.class)}),
@JoinTable(type=Type.LEFT_JOIN, entity=Company.class, alias="c",
on="c.company_code = a.company_name",
columns={@Column(includeEntity=Company.class)}),
},
// 支持扩展Column、Form、Where等,主要用于该注解实现不了的复杂情况,扩展SQL写法,这里设置的是sqlMap的key
extWhereKeys="dsfOffice, dsfCompany",
// 自动设置默认排序
orderBy="a.update_date DESC"
)
public class Employee extends DataEntity<Employee> {
private static final long serialVersionUID = 1L;
private String empCode; // 员工编码
private String empName; // 名称
private String empNameEn; // 英文名
private String empNo; // 工号
private Office office; // 机构编码
private Company company; // 公司编码
private String sex; // 性别
private Date birthday; // 生日
private String photo; // 员工照片
private String email; // 电子邮件
private String mobile; // 手机号码
private String phone; // 办公电话
private String fax; // 传真号码
private String qq; // QQ号
private String weixin; // 微信号
private String stations; // 岗位
/// 省略 get set 方法
}
请仔细看上面的代码和注释,其以上之外,还支持是否为插入字段,是否为更新字段等等。
再举一个例子,扩展上面介绍的Employee表,与用户表联合查询单独定义实体,用户员工实体:
@Table(name="${_prefix}sys_user", alias="a", columns={
@Column(includeEntity=User.class),
}, joinTable={
@JoinTable(type=Type.JOIN, entity=Employee.class, alias="e",
on="e.emp_code = a.ref_code AND a.user_type=#{USER_TYPE_EMPLOYEE}",
columns={@Column(includeEntity=Employee.class)}),
@JoinTable(type=Type.LEFT_JOIN, entity=Office.class, alias="o",
on="o.office_code = a.office_name", attrName="employee.office",
columns={@Column(includeEntity=Office.class)}),
@JoinTable(type=Type.LEFT_JOIN, entity=Company.class, alias="c",
on="c.company_code = a.company_name", attrName="employee.company",
columns={@Column(includeEntity=Company.class)}),
}, extWhereKeys="dsfOffice, dsfCompany", orderBy="a.update_date DESC"
)
public class EmpUser extends User {
private static final long serialVersionUID = 1L;
public EmpUser() {
this(null);
}
public EmpUser(String id){
super(id);
}
@Valid
public Employee getEmployee(){
Employee employee = (Employee)super.getRefObj();
if (employee == null){
employee = new Employee();
}
return employee;
}
public void setEmployee(Employee employee){
super.setRefObj(employee);
}
}
注解配置完成了,下面看看如何使用
如何使用
贴了这么多配置代码,下面介绍下用法。
你的Dao只需要继承CrudDao即可享受便捷体验,是不是特Easy,如下:
/**
* 员工管理DAO接口
* @author ThinkGem
*/
@MyBatisDao(entity = Employee.class)
public interface EmployeeDao extends CrudDao<Employee> {
}
EmployeeDao继承CrudDao后,里面的方法你都可以调用,如下方法:
/**
* DAO实现增删改接口
* @author ThinkGem
*/
public interface CrudDao<T> extends QueryDao<T> {
/**
* 插入数据
*/
public int insert(T entity);
/**
* 批量插入数据
*/
public int insertBatch(List<T> entityList);
/**
* 更新数据 By PK
*/
public int update(T entity);
/**
* 更新数据 By Entity
* 调用此方法前,请务必检查whereEntity的数据准确性,否则可能会造成不想要更新的数据被更新
*/
public int updateByEntity(T entity, T whereEntity);
/**
* 更新状态数据 By PK
*/
public int updateStatus(T entity);
/**
* 更新状态数据 By Entity
* 调用此方法前,请务必检查whereEntity的数据准确性,否则可能会造成不想要更新的数据被更新
*/
public int updateStatusByEntity(T entity, T whereEntity);
/**
* 删除数据 By PK(如果有status字段,则为逻辑删除,更新status字段为1,否则物理删除)
*/
public int delete(T whereEntity);
/**
* 删除数据 By Entity(如果有status字段,则为逻辑删除,更新status字段为1,否则物理删除)
* 调用此方法前,请务必检查whereEntity的数据准确性,否则可能会造成不想要更新的数据被更新
*/
public int deleteByEntity(T whereEntity);
/**
* 物理数据 By PK
*/
public int phyDelete(T whereEntity);
/**
* 物理数据 By Entity
* 调用此方法前,请务必检查whereEntity的数据准确性,否则可能会造成不想要更新的数据被更新
*/
public int phyDeleteByEntity(T whereEntity);
/**
* 获取单条数据
* @param entity
* @return entity
*/
public T get(T entity);
/**
* 获取单条数据
* @param entity
* @return entity
*/
public T getByEntity(T entity);
/**
* 查询数据列表,如果需要分页,请设置分页对象,如:entity.setPage(new Page<T>(pageNo, pageSize));
* @param entity
* @return
*/
public List<T> findList(T entity);
/**
* 查询数据总数
* @param entity
* @return
*/
public long findCount(T entity);
}
调用举例:
// 查询一条,更新
Employee employee = new Employee();
employee.setEmpCode('E001');
employee = employeeDao.get(employee);
employee.setMobile('18666666666');
employeeDao.update(employee);
// 列表查询、统计
Employee employee = new Employee();
employee.setEmpName('小王');
employee.setPage(new Page(1, 20)); // 分页查询
List<Employee> list = employeeDao.findList(employee);
Long count = employeeDao.findCount(employee);
// 批量插入
employeeDao.insertBatch(list);
是不是有种事半功倍的感觉,小小的配置,可以实现几乎可以完成原来需要写代码的80%时间。
也许你会觉着配置复杂,难以理解,只要你用上了相信你就会爱不释手。
还有一个惊喜,这些配置也可以通过代码生成工具快速生成,喜欢不喜欢。
嗯!基本增删改查,批量操作,按实体属性查询,按实体属性更新,以及统计都有了 ↓↓↓ 可是 ↓↓↓
日期范围查询
可是,这么多还是还不够,比如,我们想实现,日期范围查询怎么办?某个实体属性,实现双重查询(如那么既能eq又能like)怎么办?想实现or、is null,括号查询怎么办?这些都么关系,已经替你考虑了,如下:
////////// 日期范围查询,gte,lte ////////////////
public Date getCreateDate_gte(){
return sqlMap.getWhere().getValue("create_date", QueryType.GTE);
}
public void setCreateDate_gte(Date createDate){
createDate = DateUtils.getOfDayFirst(createDate); // 将日期的时间改为0点0分0秒
sqlMap.getWhere().and("create_date", QueryType.GTE, createDate);
}
public Date getCreateDate_lte(){
return sqlMap.getWhere().getValue("create_date", QueryType.LTE);
}
public void setCreateDate_lte(Date createDate){
createDate = DateUtils.getOfDayLast(createDate); // 将日期的时间改为23点59分59秒
sqlMap.getWhere().and("create_date", QueryType.LTE, createDate);
}
双重字段查询
////////// 双重字段查询,支持eq,支持like ////////////////
public String getTableName() {
return StringUtils.lowerCase(tableName);
}
public void setTableName(String tableName) {
this.tableName = tableName;
}
public String getTableName_like() {
return sqlMap.getWhere().getValue("table_name", QueryType.LIKE);
}
public void setTableName_like(String tableName) {
sqlMap.getWhere().and("table_name", QueryType.LIKE, tableName);
}
扩展括号查询
////////// 支持 or、is null,括号 ////////////////
public String getParentTableName_isNull() {
return this.getParentTableName();
}
public void setParentTableName_isNull(String parentTableName) {
if (StringUtils.isBlank(parentTableName)){
sqlMap.getWhere().andBracket("parent_table_name", QueryType.IS_NULL, null, 2)
.or("parent_table_name", QueryType.EQ_FORCE, "", 3).endBracket();
this.setParentTableName(null);
}else{
this.setParentTableName(parentTableName);
}
}
支持 IN 条件查询
////////// 支持 in、not in 条件查询 ////////////////
public String[] getCategoryCode_in(){
return sqlMap.getWhere().getValue("category_code", QueryType.IN);
}
public void setCategoryCode_in(String[] codes){
sqlMap.getWhere().and("category_code", QueryType.IN, codes);
}
扩展自定义列
还有一种情况,如所有的配置都配置好了,我只需要在sql返回值里加一个简单的统计数,多返回一列,你可以这样写:
// 实体类定义
@Table(name="${_prefix}gen_table", alias="a", columns={
// @Column 。。。此处省略 。。。
},
// 扩展Column里指定一个Key名字,类里并定义一个需要返回的属性和get set
extColumnKeys="extColumn"
)
public class GenTable extends DataEntity<GenTable> {
private Long childNum; // 子表个数
public Long getChildNum() {
return childNum;
}
public void setChildNum(Long childNum) {
this.childNum = childNum;
}
}
// Service 里,通过sqlMap设置你刚定义的Key即可,如下
public Page<GenTable> findPage(Page<GenTable> page, GenTable genTable) {
// 添加扩展列,查询子表个数(子查询)
String extColumn = "(SELECT count(1) FROM "+MapperHelper.getTableName(genTable)
+" WHERE parent_table_name=a.table_name) AS \"childNum\"";
genTable.getSqlMap().add("extColumn", extColumn);
return super.findPage(page, genTable);
}
多表联合查询
如果你的表里只存储了code编码,未存储name名称,你需要通过code来联合查询出name。但实体里联合查询的表不是以实体存在的,而定义在了当前实体中,这时候应该指定JoinTable的attrName为this,代表将数据映射到当前实体,举例如下:
@Table(name="${_prefix}test", alias="a", columns={
// 此处省略...
@Column(name="test_code", attrName="testCode", label="编码(外键)"),
},
// 联合查询出外键编码的名称数据(attrName="this",指定this代表,当前实体)
joinTable={
@JoinTable(type=Type.LEFT_JOIN, entity=Test2.class, attrName="this", alias="b",
on="b.code = a.test_code",
columns={
@Column(name="name", attrName="testName", label="名称"),
}),
},
orderBy="a.update_date DESC"
)
public class Test extends DataEntity<Test> {
private static final long serialVersionUID = 1L;
// 此处省略...
private String testCode; // 编码(外键,Test2关联表的主键)
private String testName; // 名称(改属性是关联表字段,不是本表字段)
/// 省略 get set 方法
}
复杂语句查询
如果以上仍得不到你的满足,怎么办,那你可以写Mapper.xml了,比如EmployeeDao.xml一些通用的字段、条件,你就不需要在xml再写一遍了,你只需要补充SQL即可(相同id,如id=”findList”则会自动覆盖默认设置):
<select id="findList" resultType="Employee">
SELECT ${sqlMap.column.toSql('a')}
FROM ${_prefix}sys_employee a
<where>
${sqlMap.where.toSql('a')}
</where>
ORDER BY ${sqlMap.order.toSql('a')}
</select>
覆写自带 Mapper 的语句
平台支持 mapper xml 的 select/update/delete 设置 weight 权重属性,指定相同的 key 权重高的优先加载,权重低的将被忽略(4.1.3+)。
例如,你想覆写自带的 UserUtils.getByLoginCode 查询语句如下:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.jeesite.modules.sys.dao.UserDao">
<!-- 根据登录名查询用户(不区分大小写),指定 weight 即可覆写 -->
<select id="getByLoginCode" resultType="User" weight="100">
SELECT ${sqlMap.column.toSql()}
FROM ${sqlMap.table.toSql()}
WHERE a.status != #{STATUS_DELETE}
AND upper(a.login_code) = upper(#{loginCode})
</select>
</mapper>
指定 typeHandler
有些情况可能需要复杂类型,这时候你就用到了 typeHandler 个性化类型:
@Table(name="m_banner", alias="a", columns={
@Column(name="bind_items", attrName="bindItems", label="绑定的数据", isQuery=false,
javaType=java.util.List.class, typeHandler=JsonTypeHandler.class),
}
以上方法适应于 “插入、更新、查询条件” 的参数类型设置,那么,如何给查询结果集指定 typeHeader?
你可以使用扩展 Mapper XML 来实现,如下:
<resultMap type="com.jeesite.modules.banner.entity.Banner" id="bannerDetail">
<result column="bindItems" property="bindItems"
javaType="java.util.List" typeHandler="com.jeesite.common.JsonTypeHandler"/>
</resultMap>
<select id="get" resultMap="bannerDetail">
SELECT ${sqlMap.column.toSql()}
FROM ${sqlMap.table.toSql()}
<where>
${sqlMap.where.toSql()}
</where>
ORDER BY ${sqlMap.order.toSql()}
</select>
这样的Dao,你满意吗?编码原来如此简单,提高了效率,又不损失灵活,是不是很有趣呢。
附:API
@Table
/**
* 指定实体的物理表属性
* @author ThinkGem
*/
public @interface Table {
/**
* 物理表名
*/
String name() default "";
/**
* 当前表别名
*/
String alias() default "a";
/**
* 表列定义
*/
Column[] columns();
/**
* 查询,关联表
*/
JoinTable[] joinTable() default {};
/**
* 指定排序
*/
String orderBy() default "";
/**
* 表说明
*/
String comment() default "";
/**
* 扩展ColumnSQL,在这里指定sqlMap的key。<br>
* 例如:\@Table(extColumnKeys="dataScopeColumn");<br>
* Service里设置:sqlMap.put("extColumn", "column_name AS \"columnName\"");<br>
* 在执行查询的时候,该语句放到自动会加到Select最后并执行。<br>
* <b>注意:</b>如果设置,必须后台代码中设置,否则可能造成sql注入漏洞<br>
*/
String extColumnKeys() default "";
/**
* 扩展FromSQL,在这里指定sqlMap的key。<br>
* 例如:\@Table(extFromKeys="dataScopeFrom");<br>
* Service里设置:sqlMap.put("dataScopeFrom", "JOIN table_name t on t.pk=a.pk");<br>
* 在执行查询的时候,该语句放到自动会加到From最后并执行。<br>
* <b>注意:</b>如果设置,必须后台代码中设置,否则可能造成sql注入漏洞<br>
*/
String extFromKeys() default "";
/**
* 扩展WhereSQL,在这里指定sqlMap的key。<br>
* 例如:\@Table(extWhereKeys="dataScopeWhere");<br>
* Service里设置:sqlMap.put("dataScopeWhere", "AND column_name='value'");<br>
* 在执行查询的时候,该语句放到自动会加到Where最后并执行。<br>
* <b>注意:</b>如果设置,必须后台代码中设置,否则可能造成sql注入漏洞<br>
*/
String extWhereKeys() default "";
}
@Column
/**
* 定义物理表列属性(不继承父类注解)
* @author ThinkGem
*/
public @interface Column {
/**
* 字段名(例如:config_key)
*/
String name() default "";
/**
* java类型 V4.0.6+
*/
Class<?> javaType() default void.class;
/**
* jdbc类型 V4.0.6+
*/
JdbcType jdbcType() default JdbcType.UNDEFINED;
/**
* 类型处理器注册器 V4.0.6+
*/
Class<? extends TypeHandler<?>> typeHandler() default UnknownTypeHandler.class;
/**
* 属性名,若不指定,则根据name()字段名进行驼峰命名法转换(例如:config_key 转换为 configKey)
*/
String attrName() default "";
/**
* 标签名
*/
String label() default "";
/**
* 字段备注
*/
String comment() default "";
/**
* 是否主键(update、delete时的条件)
*/
boolean isPK() default false;
/**
* 是否插入字段
*/
boolean isInsert() default true;
/**
* 是否更新字段
*/
boolean isUpdate() default true;
/**
* 是否更新字段(强制更新)V4.0.5+
*/
boolean isUpdateForce() default false;
/**
* 是否是查询字段
*/
boolean isQuery() default true;
/**
* 查询类型
*/
QueryType queryType() default QueryType.EQ;
/**
* 包含嵌入一个实体
*/
Class<?> includeEntity() default Class.class;
}
@JoinTable
/**
* 指定实体的物理表的关联表属性
* @author ThinkGem
*/
public @interface JoinTable {
/**
* 连接类型
*/
Type type() default Type.JOIN;
public enum Type{
JOIN("JOIN"), // INNER JOIN
LEFT_JOIN("LEFT JOIN"),
RIGHT_JOIN("RIGHT JOIN");
private final String value;
Type(String value) { this.value = value; }
public String value() { return this.value; }
}
/**
* 连接的表,指定实体Class
*/
Class<?> entity();
/**
* 当前表别名
*/
String alias();
/**
* 连接表条件
*/
String on();
/**
* 对应主表中对应的属性名,若不指定,则根据entity()进行首字母小写得到属性名(例如:Config 转换为 config)
*/
String attrName() default "";
/**
* 连接表,返回的列,若不指定,则读取entity()的所有列。
*/
Column[] columns() default {};
}
QueryType
/**
* 查询类型
* @author ThinkGem
*/
public enum QueryType{
EQ("="),
NE("!="),
GT(">"),
GTE(">="),
LT("<"),
LTE("<="),
IN("IN"), // 接受 Object[] 或 List 参数
NOT_IN("NOT IN"),
LIKE("LIKE", "%", "%"),
LEFT_LIKE("LIKE", "%", ""),
RIGHT_LIKE("LIKE", "", "%"),
IS_NULL("IS NULL"),
IS_NOT_NULL("IS NOT NULL"),
// 强制条件,不管值是不是空字符串都加载这个查询条件
EQ_FORCE("=", true),
NE_FORCE("!=", true),
;
}
多数据源
在 JeeSite 中使用多数据源是比较简单的,只需在配置文件中配置附加数据源即可,附加数据源同样支持其它类型数据库、连接池、用户名、密码加密等。
配置方法
# 数据库连接
jdbc:
# Mysql 数据库配置(主数据源,必须保留)
type: mysql
driver: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/jeesite?useSSL=false&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull
username: root
password: 123456
testSql: SELECT 1
# 连接信息加密
encrypt:
# 加密连接用户名
username: false
# 加密连接密码
password: true
# 数据库连接池配置
pool:
# 初始化连接数
init: 1
# 最小连接数
minIdle: 3
# 最大连接数
maxActive: 20
# 多数据源名称列表,多个用逗号隔开,使用方法:@MyBatisDao(dataSourceName="ds2")
dataSourceNames: ds2,ds3
# 多数据源配置:ds2(附加数据源2)
ds2:
type: mysql
driver: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/jeesite2?useSSL=false&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull
username: root
password: 123456
testSql: SELECT 1
encrypt:
username: false
password: true
pool:
init: 1
minIdle: 3
maxActive: 20
# 多数据源配置:ds3(附加数据源3)
ds3:
type: oracle
driver: oracle.jdbc.driver.OracleDriver
url: jdbc:oracle:thin:@127.0.0.1:1521/orcl
username: jeesite
password: jeesite
testSql: SELECT 1 FROM DUAL
encrypt:
username: false
password: true
pool:
init: 1
minIdle: 3
maxActive: 20
注意:yml格式要求比较严格,注意缩进,前方空格个数。
使用方法:
1)静态分配数据源:
@MyBatisDao(dataSourceName="ds2")
public interface TestDataDao extends BaseDao {
}
@MyBatisDao(dataSourceName="ds3")
public interface TestDataDao extends BaseDao {
}
2)动态分配数据源:
dao:
@MyBatisDao(dataSourceName=DataSourceHolder.EMPTY)
public interface TestDataDao extends BaseDao {
}
service:
// 动态设置数据源
DataSourceHolder.setDataSourceName(dataSourceName);
// 恢复默认数据源
DataSourceHolder.clearDataSourceName();