持久层、@Table、@JoinTable、多数据源、MyBatis
# 引言
持久层实体类采用 JeeSite 独创的 @Table 注解配置,自动生成增删改通用 SQL,支持多表联合查询,不需要在 mapper.xml 里写重复又费时的 SQL,遇见复杂的情况下支持扩展。而报表统计分析的情况下又能支持 mybatis 原生写法,在写 sql 的时候,又能调用之前实体配置的一些参数。从而减少开发和后期维护成本。
当前众多同类的持久层框架 @Column 注解定义都是分布到 get 或属性上,或者干脆直接使用属性作为字段名,这在 JeeSite 是不推荐的,JeeSite 的实体不仅仅是物理实体,它是与Model实体结合的一个产物。综合考虑,将@Column所有定义到类头,而不是分布到各个属性或方法上,主要是有以下三点原因:
- 可一览熟知该实体类对应的物理表结构是什么样,引领开发者思维从物理表结构到对象的映射转换。思想是基于物理表结构的,@Column中的name指定物理字段名,而不是指定类上的属性名,也是这个原因
- 自动化生成的SQL和查询条件,是有序的,可方便核查定义,有利于优化查询性能
- 方便@JoinTable关联表和其它扩展信息的设置,如果分布到类的属性上就需要来回滚动屏幕查找,不利于管理字段列
关于列表查询条件可以通过实体自动生成,不用您写各式各样的 *Wrapper、手写字段名、if判断查询条件是否为空等。
这一套 @Table 注解是为了简化 80% 的通用场景而设计的,不是为了实现更复杂的功能。如果太复杂,就无形增加了更多的学习及维护成本,如 JPA、Hibernate。当然,不是说只支持简单的功能,而是可以在简单的配置之上,可自由扩展和支持任意复杂的业务场景的设计思想。
# 简单举例
以定义员工实体举例,配置如下:(注意代码上的注释)
@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_code", attrName="office",
columns={@Column(includeEntity=Office.class)}),
@JoinTable(type=Type.LEFT_JOIN, entity=Company.class, alias="c",
on="c.company_code = a.company_code", attrName="company",
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 方法
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
请仔细看上面的代码和注释,其以上之外,还支持是否为插入字段,是否为更新字段等等。
再举一个例子,扩展上面介绍的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);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
注解配置完成了,下面看看如何使用
# 如何使用
贴了这么多配置代码,下面介绍下用法。
您的Dao只需要继承CrudDao即可享受便捷体验,是不是特Easy,如下:
/**
* 员工管理DAO接口
* @author ThinkGem
*/
@MyBatisDao(entity = Employee.class)
public interface EmployeeDao extends CrudDao<Employee> {
}
2
3
4
5
6
7
8
EmployeeDao继承CrudDao后,里面的方法您都可以调用,如下方法:
/**
* DAO实现增删改接口
* @author ThinkGem
*/
public interface CrudDao<T> extends QueryDao<T> {
/**
* 插入数据
*/
int insert(T entity);
/**
* 批量插入数据
*/
int insertBatch(List<T> entityList);
/**
* 批量插入数据(防止数据库一次性接受不了太长的sql,可传递 batchSize 进行批次发送)v4.5.0+ v5.0.2+
* @param batchSize 每次发送的条数(未设置,则指定默认值:mybatis.defaultBatchSize=500)
*/
long insertBatch(List<T> entityList, Integer batchSize);
/**
* 更新数据(仅使用 主键 作为更新条件)
*/
int update(T entity);
/**
* 批量更新数据 By PK v4.5.0+ v5.0.2+
*/
long updateBatch(List<T> entityList);
/**
* 批量更新数据 By PK(按设定批次大小发送给数据库) v4.5.0+ v5.0.2+
* @param batchSize 每次发送的条数(未设置,则指定默认值:mybatis.defaultBatchSize=500)
*/
long updateBatch(List<T> entityList, Integer batchSize);
/**
* 更新数据(使用实体类的 全部属性 作为更新条件)
* 调用此方法前,请务必检查whereEntity的数据准确性,否则可能会造成不想要更新的数据被更新
*/
int updateByEntity(T entity, T whereEntity);
/**
* 更新状态数据(仅使用 主键 作为更新条件)
*/
int updateStatus(T entity);
/**
* 更新状态数据(使用实体类的 全部属性 作为更新条件)
* 调用此方法前,请务必检查whereEntity的数据准确性,否则可能会造成不想要更新的数据被更新
*/
int updateStatusByEntity(T entity, T whereEntity);
/**
* 删除数据(仅使用 主键 作为删除条件)
* 如果有status字段,则为逻辑删除,更新status字段为1,否则物理删除)
*/
int delete(T whereEntity);
/**
* 删除数据(使用实体类的 全部属性 作为删除条件)
* (如果有status字段,则为逻辑删除,更新status字段为1,否则物理删除)
* 调用此方法前,请务必检查whereEntity的数据准确性,否则可能会造成不想要更新的数据被更新
*/
int deleteByEntity(T whereEntity);
/**
* 物理数据(仅使用 主键 作为删除条件)
*/
int phyDelete(T whereEntity);
/**
* 物理数据(使用实体类的 全部属性 作为删除条件)
* 调用此方法前,请务必检查whereEntity的数据准确性,否则可能会造成不想要更新的数据被更新
*/
int phyDeleteByEntity(T whereEntity);
/**
* 执行批量语句 v5.2.1
* dao.executeBatch((dao) -> { 执行批量语句... })
*/
long executeBatch(Consumer<BaseDao> consumer);
/**
* 开始批量更新数据(请放进 try finally 里)v4.5.0+ v5.0.2+ 例如:<br>
* try{ startBatch(); 执行批量语句... flush(); } finally { endBatch(); }
*/
void startBatch();
/**
* 结束批量更新数据(请放进 try finally 里)v4.5.0+ v5.0.2+ 例如:<br>
* try{ startBatch(); 执行批量语句... flush(); } finally { endBatch(); }
*/
void endBatch();
/**
* 批量语句提交到数据库(只返回更新数)v4.5.0+ v5.0.2+
*/
long flush();
/**
* 批量语句提交到数据库(返回更新详情)v4.5.0+ v5.0.2+
*/
List<BatchResult> flushStatements();
/**
* 获取单条数据(仅使用 主键 作为查询条件)
* @param entity
* @return entity
*/
T get(T entity);
/**
* 获取单条数据(使用实体类的 全部属性 作为查询条件)
* @param entity
* @return entity
*/
T getByEntity(T entity);
/**
* 查询数据列表,如果需要分页,请设置分页对象,如:entity.setPage(new Page<T>(pageNo, pageSize));
* @param entity
* @return
*/
List<T> findList(T entity);
/**
* 查询数据总数
* @param entity
* @return
*/
long findCount(T entity);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
调用举例:
// 1)查询一条,更新
Employee employee = new Employee();
employee.setEmpCode('E001');
employee = employeeDao.get(employee);
employee.setMobile('18666666666');
employeeDao.update(employee);
// 2)列表查询、统计
Employee employee = new Employee();
employee.setEmpName('小王');
employee.setPage(new Page(1, 20)); // 分页查询
List<Employee> list = employeeDao.findList(employee);
Long count = employeeDao.findCount(employee);
// 3)分页批量插入数据(防止太长的sql数据库无法接受)
ListUtils.pageList(list, 100, new MethodCallback() {
@SuppressWarnings("unchecked")
public Object execute(Object... objs) {
return employeeDao.insertBatch((List<RoleMenu>)objs[0]);
}
});
// 4)批量插入或更新(第一个参数是插入或更新 list 集合数据,第二个参数是 batchSize)v4.5.0+ v5.0.2+
// 如果每次发送的条数 batchSize 未设置,则获取默认参数值:mybatis.defaultBatchSize=500
employeeDao.insertBatch(list, null);
employeeDao.updateBatch(list, null);
// 5)自定义批量执行语句 v4.5.0+ v5.0.2+
try{
// 批量开始
employeeDao.batchStart();
// 执行批量 insert 或 update 等...
for (Employee employee : list) {
employeeDao.insert(employee);
}
// 将批量刷新到数据库
employeeDao.flush();
} finally {
// 批量完成
employeeDao.batchEnd();
}
// 6)自定义 ExecutorType 执行类型 v4.5.0+ v5.0.2+
try{
// 设置 MyBatis 执行类型
ExecutorHolder.setSimpleExecutorType();
ExecutorHolder.setReuseExecutorType();
ExecutorHolder.setBatchExecutorType();
// 业务代码,省略...
// 将批量刷新到数据库
employeeDao.flushStatements();
} finally {
// 清理 MyBatis 执行类型
ExecutorHolder.clearExecutorType
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
是不是有种事半功倍的感觉,小小的配置,可以实现几乎可以完成原来需要写代码的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);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 多重字段查询
////////// 双重字段查询,支持eq,ne,like等 ////////////////
// 等于 =
public String getTableName() {
return StringUtils.lowerCase(tableName);
}
public void setTableName(String tableName) {
this.tableName = tableName;
}
// 不等于 !=
public String getTableName_ne() {
return sqlMap.getWhere().getValue("table_name", QueryType.NE);
}
public void setTableName_ne(String tableName) {
sqlMap.getWhere().and("table_name", QueryType.NE, tableName);
}
// 模糊查询 like
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);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 扩展括号查询
////////// 支持 or、is null,括号 ////////////////
public String getParentTableName_isNull() {
return this.getParentTableName();
}
public void setParentTableName_isNull(String parentTableName) {
if (StringUtils.isBlank(parentTableName)){
// 最后一个参数 2、3 是索引号,当列名和操作符定义重复调用时,加索引号进行区分
sqlMap.getWhere().andBracket("parent_table_name", QueryType.IS_NULL, null, 2)
.or("parent_table_name", QueryType.EQ_FORCE, "", 3).endBracket();
// SQL 输出结果示例:WHERE ( a.parent_table_name IS NULL OR a.parent_table_name = ? )
this.setParentTableName(null);
}else{
this.setParentTableName(parentTableName);
}
}
public String getOfficeCode_inChildren() {
return sqlMap.getWhere().getValue("office_code", QueryType.EQ, 3);
}
public void setOfficeCode_inChildren(String officeCode) {
// 最后一个参数 3 是索引号,当列使用括号时,可加相同的索引号进行匹配
sqlMap.getWhere().andBracket("office_code", QueryType.EQ, officeCode, 3)
.or("parent_codes", QueryType.LIKE, officeCode, 3).endBracket(3);
}
////////// 条件嵌套查询,可替代 andBracket、orBracket、endBracket v5.2.1+ ////////////////
// 提示:最后一个参数 1、2、3 为索引号,当列名重复调用的时候使用,如果不重复,可以不设置
sqlMap.getWhere()
.and("name", QueryType.EQ, "abc", 1)
.and((w) -> w
.or("name", QueryType.EQ, "def", 2)
.or("name", QueryType.EQ, "ghi", 3))
.and((w) -> w
.or((w2) -> w2
.and("name", QueryType.EQ, "jkl", 4)
.and("name", QueryType.EQ, "", 5)
.and("name", QueryType.EQ_FORCE, "", 6))
.or((w2) -> w2
.and("name", QueryType.EQ, "mno", 7)
.and("name", QueryType.EQ, "pqr", 8)));
// SQL 输出结果示例:(设置的空值自动忽略,EQ_FORCE 可强制增加空值条件)
WHERE a.name = 'abc'
AND ( a.name = 'def' OR a.name = 'ghi' )
AND (
( a.name = 'jkl' AND a.name = '' )
OR ( a.name = 'mon' AND a.name = 'pqr' )
)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# 支持 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);
}
2
3
4
5
6
7
8
9
10
# 多表联合查询
如果您的表里只存储了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 方法
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 扩展查询列
还有一种情况,如所有的配置都配置好了,我只需要在sql返回值里加一个简单的统计数,多返回一列,您可以这样写:
// 实体类定义
@Table(name="${_prefix}test_table", alias="a", columns={
// @Column 。。。此处省略 。。。
},
// 扩展Column里指定一个Key名字,类里并定义一个需要返回的属性和get set
extColumnKeys="extColumn"
)
public class TestTable extends DataEntity<TestTable> {
private Long childNum; // 子表个数
// 属性、get、set 此处省略 。。。
}
// Service 里,通过sqlMap设置您刚定义的Key即可,如下
public Page<TestTable> findPage(Page<TestTable> page, TestTable testTable) {
// 添加扩展列,查询子表个数(子查询)
String extColumn = "(SELECT count(1) FROM " + MapperHelper.getTableName(testTable)
+ " WHERE parent_table_name=a.table_name) AS \"childNum\"";
testTable.sqlMap().add("extColumn", extColumn); // 旧版将 sqlMap() 替换为 getSqlMap()
return super.findPage(page, testTable);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 扩展查询表
// 实体类定义
@Table(name="${_prefix}test_data", alias="a", columns={
// @Column 。。。此处省略 。。。
},
// 扩展Form里指定一个Key名字,类里并定义一个需要返回的属性和get set
extFormKeys="extForm"
)
public class TestData extends DataEntity<TestData> {
// 属性、get、set 此处省略 。。。
}
// Service 里,通过sqlMap设置您刚定义的Key即可,如下
public Page<TestData> findPage(Page<TestData> page, TestData testData) {
String extForm = "JOIN js_test_data_detail b ON b.id = a.id";
testData.sqlMap().add("extForm", extForm); // 旧版将 sqlMap() 替换为 getSqlMap()
return super.findPage(page, testData);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 扩展查询条件
// 实体类定义
@Table(name="${_prefix}test_data", alias="a", columns={
// @Column 。。。此处省略 。。。
},
// 扩展Where里指定一个Key名字,类里并定义一个需要返回的属性和get set
extWhereKeys="extWhere"
)
public class TestData extends DataEntity<TestData> {
// 属性、get、set 此处省略 。。。
}
// Service 里,通过sqlMap设置您刚定义的Key即可,如下
public Page<TestData> findPage(Page<TestData> page, TestData testData) {
String extWhere = "AND a.test_input = '123456'";
testData.sqlMap().add("extWhere", extWhere); // 旧版将 sqlMap() 替换为 getSqlMap()
return super.findPage(page, testData);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 复杂情况写 Mapper SQL
如果以上仍得不到您的满足,怎么办,那您可以写 Mapper.xml 了,比如 EmpUserDao.xml 一些通用的字段、条件,您就不需要在 xml 再写一遍了,您只需要补充 SQL 即可(相同 id,如 id="findList" weight="100" 则会自动覆盖默认设置):
<select id="findList" resultType="EmpUser" weight="100">
SELECT ${sqlMap.column.toSql()}
FROM ${sqlMap.table.toSql()}
<if test="roleCode != null and roleCode != ''">
JOIN ${_prefix}sys_user_role ur ON ur.user_code = a.user_code
</if>
<where>
${sqlMap.where.toSql()}
<if test="roleCode != null and roleCode != ''">
AND ur.role_code = #{roleCode}
</if>
</where>
ORDER BY ${sqlMap.order.toSql()}
</select>
2
3
4
5
6
7
8
9
10
11
12
13
14
提示:${}
是否有 SQL 注入风险?请放心 sqlMap
和 _prefix
均为程序内部调用并生成结果,不会有安全隐患。在 JeeSite 中,所有通过浏览器或接口接受的参数,是通过 #{}
参数传递方式提交给数据库,个别属性如 orderBy
也会通过 EncodeUtils.sqlFilter()
安全过滤后才会被数据库解析执行。
MyBatis 官方 Mapper 语法中文文档:
- 文档1:https://mybatis.org/mybatis-3/zh/sqlmap-xml.html (opens new window)
- 文档2:https://mybatis.org/mybatis-3/zh/dynamic-sql.html (opens new window)
# 覆写自带 Mapper SQL
平台支持 mapper xml 的 select/update/delete 设置 weight 权重属性,指定相同的 key 权重高的优先加载,权重低的将被忽略(4.1.3+)。
例如,您想覆写自带的 UserUtils.getByLoginCode 查询order
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://jeesite.com/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>
2
3
4
5
6
7
8
9
10
11
12
13
# 指定 typeHandler
有些情况比如JSON复杂类型字段存取、手机号身份证加密字段等,这时候您就用到了 typeHandler 个性化类型:
@Table(name="${_prefix}sys_banner", alias="a", columns={
@Column(name = "id", attrName = "id", label = "主键", isPK = true)
@Column(name = "bind_items", attrName = "bindItems", label = "绑定项目", isQuery = QueryType.EQ,
javaType = List.class, typeHandler = JsonTypeHandler.class),
}
2
3
4
5
以上方法适应于 “插入、更新、查询条件” 的参数类型设置,那么,如何给查询结果集指定 typeHandler?
有两种方法,一种是编写 Results 注解完成,另外一种是写 Mapper XML 实现。
值得注意的是,只需编写 typeHandler 的字段即可,如下:
@Results({
@Result(column = "bindItems", property = "bindItems",
javaType = List.class, typeHandler = JsonTypeHandler.class)
})
@SelectProvider(type = SelectSqlProvider.class, method = "findList")
List<Banner> findList(Banner entity);
2
3
4
5
6
或者:
<resultMap id="bannerResult" type="com.jeesite.modules.banner.entity.Banner">
<result column="bindItems" property="bindItems" javaType="java.util.List"
typeHandler="com.jeesite.common.mybatis.type.JsonTypeHandler"/>
</resultMap>
<select id="findList" resultMap="bannerResult">
SELECT ${sqlMap.column.toSql()}
FROM ${sqlMap.table.toSql()}
<where>
${sqlMap.where.toSql()}
</where>
ORDER BY ${sqlMap.order.toSql()}
</select>
2
3
4
5
6
7
8
9
10
11
12
为什么 result 的 column="bindItems" 而不是 column="bind_items" 因为在 @Column 中指定的是 attrName = "bindItems" ,所以在生成 SQL 为 bind_items AS "bindItems",返回列名即为 bindItems。
# 手机号加密脱敏(国密)
内置包括加密类型如下几个,可自己扩展其它加密方式:
- EncryptTypeHandler.class:对数据进行 AES 或 SM4 数据加密解密
- AesTypeHandler.class:对数据进行 AES 数据加密解密
- SM4TypeHandler.class:对数据进行 SM4 数据加密解密
其中 EncryptTypeHandler 的加密算法,有 encrypt.smAlgorithm 参数决定,是采用国密 SM4 算法,还是 AES 算法,还可设置 encrypt.storeBase64 参数,改变存储方式为 Hex 还是 Base64。
- EmpUser.java
@Table(name="${_prefix}sys_user", alias="a", label="员工信息", columns={
// 指定 手机号字段,加密解密 typeHandler,用来存储 mobile 字段自动加密
@Column(name = "mobile", attrName = "mobile", label = "手机号码", queryType = QueryType.EQ,
javaType = String.class, typeHandler = EncryptTypeHandler.class),
// .... 下面省略 ....
}
)
2
3
4
5
6
7
8
9
- EmpUserDao.java
public interface EmpUserDao extends CrudDao<EmpUser> {
// 实现 empUserService.get(entity) 查询方法中的 mobile 字段自动解码
@Override
@Results({
@Result(column = "mobile", property = "mobile",
javaType = String.class, typeHandler = EncryptTypeHandler.class)
})
@SelectProvider(type = SelectSqlProvider.class, method = "get")
EmpUser get(EmpUser entity);
// .... 下面省略 ....
}
2
3
4
5
6
7
8
9
10
11
12
13
14
- EmpUserDao.xml
<mapper namespace="com.jeesite.modules.sys.dao.EmpUserDao">
<!-- 实现 empUserService.findList(entity) 查询方法中的 mobile 字段自动解码
因为该方法在 Mapper 里重写过了,所以不能使用 @Results 注解,即在这里定义 -->
<!-- 结果集映射 -->
<resultMap id="empUserResult" type="EmpUser">
<result column="mobile" property="mobile" javaType="java.lang.String"
typeHandler="com.jeesite.common.mybatis.type.EncryptTypeHandler"/>
</resultMap>
<!-- 查询数据 -->
<select id="findList" resultMap="empUserResult">
SELECT ${sqlMap.column.toSql()}
FROM ${sqlMap.table.toSql()}
<!-- .... 下面省略 .... -->
</select>
</mapper>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 多数据库类型语法
使用 @Table 无需关注多数据库的语法,如果自己写 Mapper SQL 的时候, 可能需要用到多数据库类型下的语法判断,下面就举例如何应用:
Dao 接口方法定义:
List<Log> findList(Log log);
注意:Log 实体类需要继承 DataEntity 或 BaseEntity,如果没有继承需要写一个 get 方法,如下:
public Global getGlobal() {
return Global.getInstance();
}
2
3
Mapper 定义:
<!-- 判断数据库类型例子 -->
<select id="findList" resultType="Log">
SELECT a.*
FROM ${_prefix}sys_log a
<where>
<if test="logTitle != null and logTitle != ''">
AND a.log_title LIKE
<if test="global.dbName == 'mysql'">CONCAT('%', #{title}, '%')</if>
<if test="global.dbName == 'oracle'">'%' || #{title} || '%'</if>
<if test="global.dbName == 'mssql'">'%' + #{title} + '%'</if>
<if test="global.dbName == 'postgresql'">'%' || #{title} || '%'</if>
</if>
</where>
ORDER BY a.create_date DESC
</select>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Map 参数和返回值
Service:
public Page<Map<String, Object>> findPage(Map<String, Object> params) {
// 演示Map参数和返回值,支持分页
Page<Map<String, Object>> pageMap = (Page)params.get("page");
if (pageMap == null) {
pageMap = new Page<>();
params.put("page", pageMap);
}
// 设置查询条件
params.put("testInput", "123");
// 查询数据返回结果
pageMap.setList(dao.findListForMap(params));
// 输出结果(测试)
System.out.println(pageMap.getList());
System.out.println(pageMap.getCount());
return pageMap;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Dao:
public List<Map<String, Object>> findListForMap(Map<String, Object> params);
Mapper:
<!-- 演示Map参数和返回值,支持分页 -->
<select id="findListForMap" resultType="map">
SELECT * FROM test_data a
<where>
<if test="testInput != null and testInput != ''">
AND a.test_input = #{testInput}
</if>
</where>
<if test="page != null and page.orderBy != null and page.orderBy != ''">
ORDER BY ${page.orderBy}
</if>
</select>
2
3
4
5
6
7
8
9
10
11
12
# 多 @Table 自定义
public Page<TestData> findPage(TestData testData) {
// 重新定义注解
@Table(name="test_data", alias="a", columns={
@Column(name="id", attrName="id", label="编号", isPK=true),
@Column(name="test_input", attrName="testInput", label="单行文本", queryType=QueryType.LIKE),
@Column(name="test_textarea", attrName="testTextarea", label="多行文本", queryType=QueryType.LIKE),
}, orderBy="a.update_date DESC")
class TestDataQuery extends TestData {
private static final long serialVersionUID = 1L;
public TestDataQuery(TestData entity){
BeanUtils.copyProperties(entity, this);
}
}
// 实例化条件,查询并返回结果
return super.findPage(new TestDataQuery(testData));
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 动态表名
有时候可能需要给 @Table 指定动态表名,name 属性支持使用简单的 EL 表达式。
1、_prefix 是内置属性,含义是表的前缀,数值来自 application.yml 里的 jdbc.tablePrefix 属性。
2、支持获取 application.yml、jeesite-xxx.yml 中的属性值作为表名。v5.7.1+
3、支持获取当前对象的属性数据作为表名,使用 customTableName 属性,举例如下:
@Table(name="${_prefix}${customTableName}", alias="a", columns={
// 此处省略...
},
orderBy="a.update_date DESC"
)
public class Test extends DataEntity<Test> {
private static final long serialVersionUID = 1L;
private String customTableName; // 属性名自定义,可以给该属性赋值,设置动态表名
/// 省略 属性 get set 方法
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
这样的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("dataScopeColumn", "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 "";
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# @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+
* 若想全局设置可调用:sqlMap.updateNullValue(); V4.1.8
*/
boolean isUpdateForce() default false;
/**
* 是否是查询字段
*/
boolean isQuery() default true;
/**
* 查询类型
*/
QueryType queryType() default QueryType.EQ;
/**
* 包含嵌入一个实体(只嵌套实体里的@Column,不嵌套@JoinTable,嵌套太多不利于后期维护)
*/
Class<?> includeEntity() default Class.class;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# @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 {};
/**
* 是否懒加载,当指定加载别名 sqlMap.loadJoinTableAlias(aliases) 多个用逗号,才进行加载对应关联表 v5.3.1
* @return
*/
boolean lazy() default false;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# QueryType
/**
* 查询类型
* @author ThinkGem
*/
public enum QueryType{
EQ("="),
NE("!="),
GT(">"),
GTE(">="),
LT("<"),
LTE("<="),
IN("IN"), // 接受 Object[] 或 List 参数
NOT_IN("NOT IN"), // 接受 Object[] 或 List 参数
LIKE("LIKE", "%", "%"),
LEFT_LIKE("LIKE", "%", ""),
RIGHT_LIKE("LIKE", "", "%"),
NOT_LIKE("NOT LIKE", "%", "%"), // v4.1.7
LEFT_NOT_LIKE("NOT LIKE", "%", ""), // v4.1.7
RIGHT_NOT_LIKE("NOT LIKE", "", "%"),// v4.1.7
IS_NULL("IS NULL"),
IS_NOT_NULL("IS NOT NULL"),
// 强制条件,不管值是不是空字符串都加载这个查询条件
EQ_FORCE("=", true),
NE_FORCE("!=", true),
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# QueryAndor
/**
* Sql操作符
* @author ThinkGem
*/
public enum QueryAndor{
AND("AND"),
AND_BRACKET("AND ("),
OR("OR"),
OR_BRACKET("OR ("),
END_BRACKET(")"),
}
2
3
4
5
6
7
8
9
10
11
12
13
# QueryWhere
/**
* 添加一个 AND 条件
* @param columnName 列名
* @param queryType 关系操作符
* @param value 查询值
*/
public QueryWhere and(String columnName, QueryType queryType, Object value){
return add(QueryAndor.AND, columnName, queryType, value, 1);
}
/**
* 添加一个 AND 条件,多个相同列时,指定 num 索引号
* @param columnName 列名
* @param queryType 关系操作符
* @param value 查询值
* @param num 区分标示符,多个相同列时,指定 num 索引号
*/
public QueryWhere and(String columnName, QueryType queryType, Object value, Integer num){
return add(QueryAndor.AND, columnName, queryType, value, num);
}
/**
* 添加一个 AND 条件,带括号
*/
public QueryWhere andBracket(String columnName, QueryType queryType, Object value){
return add(QueryAndor.AND_BRACKET, columnName, queryType, value, 1);
}
/**
* 添加一个 AND 条件,带括号,多个相同列时,指定 num 索引号
*/
public QueryWhere andBracket(String columnName, QueryType queryType, Object value, Integer num){
return add(QueryAndor.AND_BRACKET, columnName, queryType, value, num);
}
/**
* 添加一个 OR 条件
*/
public QueryWhere or(String columnName, QueryType queryType, Object value){
return add(QueryAndor.OR, columnName, queryType, value, 1);
}
/**
* 添加一个 OR 条件,多个相同列时,指定 num 索引号
*/
public QueryWhere or(String columnName, QueryType queryType, Object value, Integer num){
return add(QueryAndor.OR, columnName, queryType, value, num);
}
/**
* 添加或者条件,并添加一个左括号(必须和endBracket组合使用)
*/
public QueryWhere orBracket(String columnName, QueryType queryType, Object value){
return add(QueryAndor.OR_BRACKET, columnName, queryType, value, 1);
}
/**
* 添加或者条件,并添加一个左括号(必须和endBracket组合使用),多个相同列时,指定 num 索引号
*/
public QueryWhere orBracket(String columnName, QueryType queryType, Object value, Integer num){
return add(QueryAndor.OR_BRACKET, columnName, queryType, value, num);
}
/**
* 添加一个右括号(必须和 andBracket 或 orBracket 组合使用)
*/
public QueryWhere endBracket(){
return add(QueryAndor.END_BRACKET, null, null, null, 1);
}
/**
* 添加一个右括号(必须和 andBracket 或 orBracket 组合使用),多个相同列时,指定 num 索引号
*/
public QueryWhere endBracket(Integer num){
return add(QueryAndor.END_BRACKET, null, null, null, num);
}
/**
* 添加一个 AND 条件,内嵌的条件使用括号包裹(可替代andBracket和andBracket) v5.2.1+
*/
public QueryWhere and(Consumer<QueryWhere> nestedWhere){
return nested(nestedWhere, QueryAndor.AND);
}
/**
* 添加一个 OR 条件,内嵌的条件使用括号包裹(可替代orBracket和orBracket) v5.2.1+
*/
public QueryWhere or(Consumer<QueryWhere> nestedWhere){
return nested(nestedWhere, QueryAndor.OR);
}
/**
* 添加一个内嵌条件,内嵌的条件使用括号包裹 v5.2.1+
*/
private QueryWhere nested(Consumer<QueryWhere> nestedWhere, QueryAndor nestedAndor){
QueryWhere queryWhere = new QueryWhere(this.entity, paramPrefix, nestedAndor);
nestedWhere.accept(queryWhere);
return this;
}
/**
* 添加一个查询条件
* @param andor 逻辑运算符 AND OR
* @param columnName 列名
* @param queryType 关系运算符 = > >= < <= !=
* @param value 设置值
* @param num 字段标识号,多个相同列时,指定 num 索引号
*/
private QueryWhere add(QueryAndor andor, String columnName, QueryType queryType, Object value, Integer num){
if (andor == null){
return this;
}
String key = StringUtils.replace(columnName, ".", "#") + "#" + queryType + num;
sqlMap.where.put(key, new Entity(paramPrefix + key, andor, columnName, queryType, value));
return this;
}
/**
* 获取用户填写的查询条件值
* @param columnName 物理表字段名,例如:config_key
* @param queryType 关系运算符
*/
public <E> E getValue(String columnName, QueryType queryType) {
return (E)getValue(columnName, queryType, 1);
}
/**
* 获取用户填写的查询条件值,多个相同列时,指定 num 索引号
* @param field 物理表字段名,例如:config_key
* @param queryType 关系运算符
* @param num 字段标识号,多个相同列时,指定 num 索引号
*/
public <E> E getValue(String columnName, QueryType queryType, Integer num) {
Object qwe = sqlMap.where.get(columnName + "#" + queryType + num);
if (qwe != null){
return (E) qwe;
}
return null;
}
/**
* 禁用自动添加租户代码条件(corp_code = #{entity.corpCode})
*/
public QueryWhere disableAutoAddCorpCodeWhere();
/**
* 禁用自动添加状态条件(status != #{STATUS_DELETE}),但在调用findList之前setStatus("")
*/
public QueryWhere disableAutoAddStatusWhere();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
# SqlMap
/**
* 执行更新时,不去验证字段是否为空(相当于Column.isUpdateForce的全局设置)v4.0.8+
*/
public SqlMap updateNullValue();
/**
* 设置强制更新的列,如 Column 中定义 isUpdate 为 false 的也允许被更新 V5.8.1+
*/
public SqlMap forceUpdateColumns(String forceUpdateColumns);
/**
* 最大联表个数( -1 为不限制,n 为 JoinTable n张表,0 为 不进行 JoinTable 查询)v4.3.2+
*/
public SqlMap maxJoinTableNum(int maxJoinTableNum);
/**
* 指定 lazy 的 JoinTable 要加载的别名 v5.3.1
* @param aliases 字符串,多个别用逗号隔开
* @return
*/
public SqlMap loadJoinTableAlias(String aliases);
/**
* 执行 delete 逻辑删除的时候,同时标记 id 为删除 v5.4.0+
*/
public SqlMap markIdDelete();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 主键、PK、ID 字段
PK 和 ID 表示的都是唯一主键,主键支持一个字段和多个字段的复合主键,可通过 @Column 的 isPK 属性指定
主键支持名称自定义,如 office_code,当主键成不为 id 时候,也可通过实体中的 getId() 和 setId() 方法进行操作主键
当主键为复合主键的时候,getId() 和 setId() 获取和设置的主键是复合主键的组合值,采用 “#” 分隔
id 属性,巧妙的满足了通用业务,不必考虑业务表的主键名是什么,可直接通过 id 操作的场景
# 支持 bigint 主键
个别业务表可能会强制要求主键使用长整型,你可直接将物理表的主键设置为 bigint,代码生成器会自动识别并生成对应代码。
# 支持自增主键
虽说 JeeSite 不推荐使用自增字段(因为会有各方面的分布式、高并发、数据迁移和同步、扩展性问题等等)
但有些旧系统遗留或第三方系统数据表不能更改的情况,所以,JeeSite 还是提供了这方面的支持
下面咱们以 MySql 为例,修改自带的 TestData 功能,展示自增字段的配置方法
1、将原字段修改为长整型并为自增字段:
ALTER TABLE `test_data` MODIFY COLUMN `id` bigint(64) NOT NULL AUTO_INCREMENT COMMENT '编号' FIRST;
2、修改 TestData 实体类,如下:
/**
* 测试数据Entity
* @author ThinkGem
* @version 2018-04-22
*/
@Table(name="test_data", alias="a", columns={
// @Column(name="id", attrName="id", label="编号", isPK=true), // 注释掉这一行
@Column(name="id", attrName="tid", label="编号", isPK=true), // 添加自增字段
// 原来的代码,此处省略...
}
)
public class TestData extends DataEntity<TestData> {
private static final long serialVersionUID = 1L;
private Long tid; // 增加表自增主键存储属性
// 原来的代码,此处省略...
/**
* 重载插入前执行的方法,setId 为 null,使用数据自增
*/
@Override
public void preInsert() {
super.preInsert();
setId(null);
}
/**
* 重载默认方法,主键类型互转,方便前端操作
* 如果需要在 insert 后返回 自增ID,请设置 mybatis-config.xml 的 useGeneratedKeys="true"
*/
@Override
public String getId() {
return ObjectUtils.toString(getTid());
}
/**
* 重载默认方法,主键类型互转,方便前端操作
*/
@Override
public void setId(String id) {
setTid(StringUtils.isNotBlank(id) ? ObjectUtils.toLong(id) : null);
}
/**
* 自增字段的 get
*/
public Long getTid() {
return tid;
}
/**
* 自增字段的 set
*/
public void setTid(Long tid) {
this.tid = tid;
}
/* v4.2.0 之前版本添加,之后版本无需添加该方法(获取当前主键字段名和属性名)
public String getIdColumnName() {
if (super.getIdColumnName() == null){
super.getId();
}
return super.getIdColumnName();
}
public String getIdAttrName() {
if (super.getIdAttrName() == null){
super.getId();
}
return super.getIdAttrName();
}
*/
// 原来的代码,此处省略...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
自增字段返回主键
@MyBatisDao
public interface TestDataDao extends CrudDao<TestData> {
@Override
@InsertProvider(type = InsertSqlProvider.class, method = "insert")
@Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id")
long insert(T entity);
}
2
3
4
5
6
7
# 多数据源使用
特点:深入 MyBatis 底层事务,从 DAO 层切换多数据源,方便快捷,彻底的解决必须从 Controller 里切换数据源的麻烦。
在 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")
#(注意:此为必填属性,请不要使用 default、dynamic、empty 关键词作为数据源名)
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
# JTA 分布式事务,建议启用多数据源的时候开启(v4.0.4+)
jta:
enabled: true
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
注意:yml格式要求比较严格,注意缩进,前方空格个数。
# 方式二:界面配置
该方式的优点是通过菜单即可快速添加和维护数据源,相对方式一的 application.yml 中配置私密性弱一些。
进入菜单:数据管理 -> 数据源管理
1)新增或编辑:
填写完成数据库连接参数后,点击保存按钮,后台会进行连接参数的正确性进行验证,如果连接失败,界面给予提示原因。连接成功后系统自动加入到当前数据源池中,方可后面使用该数据源。
2)连接类型:支持自定义扩展(可配置一些连接参数默认值)
3)连接池类型:支持自定义扩展(实现 AddDataSource 接口类)
4)修改记录:修改数据源时,需要填写修改原因提交,系统会自动进行修改前后数据比较留痕。可点击“修改记录”按钮,快速查看修改日志。
# 多数据源使用方法
方法一:静态分配数据源(注解配置):
@MyBatisDao(dataSourceName="ds2")
public interface TestDataDao extends BaseDao {
}
2
3
@MyBatisDao(dataSourceName="ds3")
public interface TestDataDao extends BaseDao {
}
2
3
方法二:动态分配数据源(接口调用):
dao层:
@MyBatisDao(dataSourceName=DataSourceHolder.EMPTY)
public interface TestDataDao extends BaseDao {
}
2
3
service层:(在调用dao接口前后切换和恢复数据源)
// 动态设置数据源
DataSourceHolder.setDataSourceName(dataSourceName);
// 恢复默认数据源
DataSourceHolder.clearDataSourceName();
2
3
4
方法三:通过yml配置切换(v4.3.0+)
1、数据源映射(Dao类名 = 数据源名称),优先于 @MyBatisDao(dataSourceName="ds2") 设置 v4.3.0
2、Dao类名,不仅支持某个具体 Dao类名,还支持 Dao 里的某个方法指定数据源名称,还支持包路径指定数据源等
3、数据源名指定 {empty} 时支持动态,相当于 @MyBatisDao(dataSourceName=DataSourceHolder.EMPTY)
4、数据源支持指定变量 {corpCode}、 {userCode}、{userCache中的Key名}、{yml或sys_config中的Key名}
5、从上到下,先匹配先受用规则,默认数据源名为 default 扩展数据源为 dataSourceNames 列表里自定义的名字
# 数据库连接
jdbc:
mybatisDaoAndDataSourceMappings: |
TestDataChildDao = ds2
EmpUserDao.findList = ds2
com.jeesite.modules.sys = default
com.jeesite.modules.filemanager = ds2
2
3
4
5
6
7
# 自定义数据源
可以自定义数据源实现,替换默认的 Druid 连接池。
方案一:替换默认数据源
@Configuration
public class DataSourceConfig {
@Bean
public DataSource dataSource(RoutingDataSource routingDataSource) throws SQLException {
// 创建默认数据源,配置文件中指定数据库类型:jdbc.type=mysql
MysqlDataSource bean = new MysqlDataSource();
bean.setUrl("jdbc:mysql://127.0.0.1:3306/jeesite2?zeroDateTimeBehavior=CONVERT_TO_NULL");
bean.setUser("root");
bean.setPassword("123456");
routingDataSource.setDefaultDataSource(bean);
return routingDataSource.getDefaultDataSource();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
方案二:使用 JNDI 数据源举例
@Configuration
public class DataSourceConfig {
@Bean
public RoutingDataSource routingDataSource() throws Exception {
RoutingDataSource bean = new RoutingDataSourceSupport() {
@Override
public void afterPropertiesSet() throws Exception {
// 创建默认数据源,配置文件中指定数据库类型:jdbc.type=mysql
JndiObjectFactoryBean bean = new JndiObjectFactoryBean();
bean.setJndiName("java:/comp/env/jeesite");
bean.setProxyInterface(DataSource.class);
bean.setLookupOnStartup(false);
bean.afterPropertiesSet();
this.setDefaultDataSource((DataSource)bean.getObject());
// 多数据源支持,添加 ds2 数据源,类型:jdbc.ds2.type=mysql
JndiObjectFactoryBean bean2 = new JndiObjectFactoryBean();
bean2.setJndiName("java:/comp/env/jeesite2");
bean2.setProxyInterface(DataSource.class);
bean2.setLookupOnStartup(false);
bean2.afterPropertiesSet();
this.addDataSource("ds2", (DataSource)bean2.getObject());
}
};
return bean;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 动态操作数据源
支持通过 API 维护数据源,可自己添加界面操作数据源。
@Autowired
private RoutingDataSource routingDataSource;
// 操作的数据源名称
String dataSourceName = "jeesite";
// 动态添加数据源(数据源配置信息来自 yml 的 jdbc.数据源名.xxx 的配置
DataSource dataSource = routingDataSource.createDataSource(dataSourceName);
routingDataSource.addDataSource(dataSourceName, dataSource);
// 如果不是通过 routingDataSource.createDataSource 创建的数据源
// 您还需要添加数据源类型(在分页查询的时候用到)
PropertiesUtils.getInstance().getProperties()
.put("jdbc." + dataSourceName + ".type", "mysql");
// 动态删除数据源(只移除资源,请调用前请手动调用数据源close释放资源)
routingDataSource.removeDataSource(dataSourceName);
// 获取指定数据源
routingDataSource.getDataSource(dataSourceName);
// 获取所有数据源
routingDataSource.getDataSourceMap();
// 获取默认数据源
routingDataSource.getDefaultDataSource();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
根据数据源配置参数,动态追加数据源(4.4.0+、5.0.1+ )
// 添加数据源
DataSourceConfig dataSourceConfig = new DataSourceConfig(dataSourceName);
dataSourceConfig.setType("mysql");
dataSourceConfig.setDriver("com.mysql.cj.jdbc.Driver");
dataSourceConfig.setUrl("jdbc:mysql://127.0.0.1:3306/jeesite2?zeroDateTimeBehavior=CONVERT_TO_NULL");
dataSourceConfig.setUsername("root");
dataSourceConfig.setPassword("123456");
routingDataSource.addDataSource(dataSourceConfig);
// 销毁数据源
routingDataSource.getDataSource(dataSourceName)?.close();
// 移除数据源
routingDataSource.removeDataSource();
// 切换数据源
DataSourceHolder.setDataSourceName(dataSourceName);
// 恢复数据源
DataSourceHolder.clearDataSourceName();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
联系售后技术人员获取 DynamicDataSourceTest 实例完整代码