# Mybatis Jpa
# 入门
# mybatis jpa
# import package with maven
<dependency>
<groupId>com.alilitech</groupId>
<artifactId>mybatis-jpa</artifactId>
<version>2.0.0</version>
</dependency>
# use mybatis jpa
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// load jpa
new MybatisJpaBootstrap(sqlSessionFactory.getConfiguration()).start();
sqlSession = sqlSessionFactory.openSession();
# mybatis jpa spring
# import package with maven
<dependency>
<groupId>com.alilitech</groupId>
<artifactId>mybatis-jpa-spring</artifactId>
<version>2.0.0</version>
</dependency>
# use mybatis jpa spring config
@ImportAutoConfiguration(MyBatisJpaConfiguration.class)
@Configuration
public class MyConfig {
@Bean
public DataSource dataSource() throws IOException {
//to load datasource...
}
@Bean(name = "sqlSessionFactory")
public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource) {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dataSource);
factoryBean.setDatabaseIdProvider(new DatabaseIdProviderImpl());
return factoryBean;
}
}
and then init spring context with this configuration.
# mybatis jpa spring boot
# import package with maven
<dependency>
<groupId>com.alilitech</groupId>
<artifactId>mybatis-jpa-spring-boot-starter</artifactId>
<version>2.0.0</version>
</dependency>
# use mybatis jpa spring boot
@SpringBootApplication
public class TestAppStart {
public static void main(String[] args) {
SpringApplication.run(TestAppStart.class, args);
}
}
# mybatis jpa features
mybatis本身为我们做了很多事情,又不失灵活性,我们可以完全自主的方便的自定义sql。但有些通用的表数据增删改查也要我们来提供sql。为此市面上出了很多generator,代码生成器可以帮我们解决部分需求,但在表字段增加、修改、删减后又要重新执行一遍。如果我们已经有很多定制的开发了,则无法重新执行sql生成。为此我们为mybatis提供了增强,保留了原来的灵活性,更为开发者减少了工作量。由于此次增强实现了部分JPA规范,固命名为mybatis-jpa。
# 文件配置mapper扫描路径
首先增强了mybatis配置,传统的mybatis都需要搞个configuration类,然后写个@mapperScan
, 感觉多此一举,这样也不能用统一的启动类。所以定义了在配置文件里配置mapper扫描
mybatis:
mapper-locations: classpath*:com/mapping/*.xml
type-aliases-package: com.**.domain
mapper-scan:
basePackages: "com.**mapper,org.**mapper"
TIP
mapper-scan.basePackages 多个路径,逗号隔开,支持通配符
# Crud自动加载SQL
通用的crud再也不用我们自己手写或生成sql了,框架为我们提供了自动装载sql。
实现了Mapper
的接口或实现了Mapper
的子接口(如CrudMapper
等)的接口可以自动了生成对应的sql statement,无需重复编写。举例:
定义mapper
// 泛型一定要定义,第一个指实体类的类型,第二指主键类型
public interface UserMapper extends CrudMapper<User, Long> {
}
定义实体类
@Table(name = "t_user") //表名
public class User {
@Id //指定主键
@GeneratedValue(GenerationType.IDENTITY) //指定主键生成的规则
private Long userId;
//...
// 字段名会自动映射为库里的dept_no,也就是驼峰(代码)转换成下划线(数据库)
private String deptNo;
// 也可强制指定表里的字段名称
@Column(name = "memo1")
private String memo1;
}
# 方法名称定义查询
如果需要根据条件进行查询,可根据jpa规范实现,用方法名称定义查询条件,无需编写sql。如:
/**
* 框架为你装载如下sql:
* select ... from xxx where name = #{name} and age = #{age} order by name desc
* 您只需要定义方法名称,无需写sql
*/
List<User> findByNameAndAgeOrderByNameDesc(String name, Integer age)
目前提供了以下几种查询:
关键字 | 查询效果 |
---|---|
IsBetween;Between | xx between val1 and val2 |
IsNotBetween;NotBetween | xx not between val1 and val2 |
IsNotNull; NotNull | xx is not null |
IsNull; Null | xx is null |
IsLessThan; LessThan | xx < val |
IsLessThanEqual; LessThanEqual | xx <= val |
IsGreaterThan; GreaterThan | xx > val |
IsGreaterThanEqual; GreaterThanEqual | xx >= val |
IsBefore; Before | xx < val |
IsAfter; After | xx > val |
IsNotLike; NotLike | xx not like %val% |
IsLike; Like | xx like %val% |
IsStartingWith; StartingWith; StartsWith | xx like val% |
IsEndingWith; EndingWith; EndsWith | xx like %val |
IsNotContaining; NotContaining; NotContains | xx not like %val% |
IsContaining; Containing; Contains | xx like %val% |
IsTrue; True | xx is true |
IsFalse; False | xx is false |
IsNot; Not | xx <> val |
Is; Equals | xx = val |
In(传入list/set/array) | xx in (?, ?, ? ....) |
NotIn(传入list/set/array) | xx not in (?, ?, ? ....) |
排序(在条件后面加OrderBy,可多个字段):
findByXXOrderByXXXAsc
findByXXOrderByXXXDescAndXXX
查询条件扩展
find..By.. findByNameOrDeptNo
get..By.. getByNameOrDeptNo
query..By.. queryByNameOrDeptNo
count..By.. //根据条件查询数量,返回数量 countByNameOrDeptNo
exists..By.. //根据条件判断是否存在,返回boolean existsByNameOrDeptNo
delete..By.. //根据条件删除,返回删除的数量 deleteByNameOrDeptNo
# 方法名称定义查询(动态条件)
在传统jpa里,若是实用jpa规范的接口,不能根据条件不同自定义不同条件的查询。但实际使用过程中,经常有若条件是空的,则查全部的。这个时候如果还是用条件匹配是不适合的。
故为解决此问题,定义的注解@IfTest
。举例:
/**
*
* 此注解相当于mybatis里的动态sql标签
* <if test="name != null and name != ''">and name = #{name}</if>
* <if test="age > 0">and age = #{age}</if>
* <if test="deptNo != null and deptNo != ''">and dept_no = #{deptNo}</if>
*/
@IfTest(notEmpty = true)
List<TestUser> findPageByNameAndAgeOrDeptNo(String name, @IfTest(notEmpty = true, conditions = {"> 0"})Integer age, String DeptNo);
请注意
参数上定义了注解,则使用参数定义的注解。参数上没有定义的,则使用方法上的注解。
@IfTest详解
notEmpty
- 传入参数非集合时表示
xx != null and xx != ''
- 传入参数是集合时表示
xx != null and xx.size() > 0
- 传入参数是数组时表示
xx != null and xx.length > 0
- 传入参数非集合时表示
notNull
转义成
xx != null
其它条件
必须只写判断条件,不用写元素,比如
> 0
# 自动关联查询
在查询时,往往会关联多表查询。但在使用jpa的时候,可以定义关联关系。通过自定义关联关系,可自动关联查询。mybatis-jpa也提供了关联查询,您只需要在实体类上定义关联关系即可,举例如下:
@Table(name = "t_user")
public class User {
@Id
private Long userId;
//...
private String deptNo
@ManyToOne
@JoinColumn(name = "deptNo", referencedColumnName = "deptNo")
private Dept dept;
}
@Table(name = "t_dept")
public class Detp {
@Id
private Long deptId;
private String deptNo;
//...
@OneToMany(mappedBy = "dept")
private List<User> users;
}
目前提供三种关联
OneToOne
public class User { @Id private Long userId; //... //User表的userId和UserInfo表的userId一对一关联 @OneToOne @JoinColumn(name = "userId", referencedColumnName = "userId") private UserInfo userInfo; } public class UserInfo { @Id private Long userInfoId; private Long userId; //... }
ManyToOne
public class Dict { @Id private Long dictId; //... //Dict表的doctId与DictVal表里的dictId关联 @OneToMany(mappedBy = "dict") private List<DictVal> dictVals; } public class DictVal { @Id private Long dictValId; private String dictId; //... //DictVal的dictId与Dict主键关联 @ManyToOne private Dict dict; }
ManyToMany
public class User { @Id private Long userId; //... //定义中间表user_role //定义User表的userId与中间表的userId字段关联 //定义Role表的roleId与中间表的roleId字段关联 @ManyToMany @JoinTable(name = "user_role", joinColumns = @JoinColumn(name = "userId", referencedColumnName = "userId"), inverseJoinColumns = @JoinColumn(name = "roleId", referencedColumnName = "roleId")) List<Role> roles; } public class Role { @Id private Long roleId; //... @ManyToMany(mappedBy = "roles") private List<User> users; }
请注意
目前只有一层关联,并且只是做查询关联,并未做更新关联。
定义关联字段
通过
@JoinColumn
注解可定义关联字段,若未定义,则默认是当前字段和关联类的主键关联定义关联表
当是ManyToMany的时候,才需要定义关联表,使用
@JoinTable
注解
# 关联查询优化
若每次查询都需要关联查询,有时候消耗较大,会影响性能(N+1问题)。我们可以通过注解来实现,哪些需要,哪些不需要从而优化部分性能。如:
public class User {
@Id
private Long id;
//...
private String deptNo
@MappedStatement(exclude = {"findById"}) //findById方法时不关联查询Dept
@ManyToOne
@JoinColumn(name = "deptNo", referencedColumnName = "deptNo")
private Dept dept;
}
TIP
@MappedStatement
有include(哪些需要关联),exclude(哪些不需要关联)。填入的是对应的mapper的方法名称。
若同时存在exclude,include。以exclude为准。
TIP
有一种情况,就是只是定义关联关系,并不是真正需要关联查询。可以只是加上注解@MappedStatement
,并且不提供任何注解的值,这个时候就表示不会有任何查询去关联查子表。
# 关联查询(子查询)自定义
子查询时有时候需要定义排序,或者需要自定义部分字段过滤,比如有些删除是逻辑删除,子查询的时候我们不希望把已经删除的查出来。参考:
public class User {
@Id
private Long id;
//...
private String deptNo;
// 关联查Dept的时候加条件 deleted = 0 order by dept_no
@ManyToOne
@JoinColumn(name = "deptNo")
@SubQuery(
predicates = @SubQuery.Predicate(property = "deleted",condition = "= 0"),
orders = @SubQuery.Order(property = "deptNo"))
private Dept dept;
}
TIP
通过注解@SubQuery
可实现自定义子查询条件(固定条件,非动态),子查询排序(固定,非动态)
# 代码构建复杂查询
如果一直通过Mapper的方法来直接定义查询条件和排序,有时候会让方法变地太长,这样对于维护其实是不利的,我们提供了代码级构造复杂查询,通过继承SpecificationMapper
接口,可以利用Specification
构建复杂条件查询。参考:
// WHERE ( dept_no = ? AND ( age > ? AND name like ?) ) order by name ASC
userMapper.findSpecification(Specifications.and()
.equal("deptNo", "002")
.nested(builder -> {
builder.and()
.greaterThan("age", 18)
.like("name", "Jack");
})
.order().asc("name").build());
//page and order
testUserMapper.findPageSpecification(page, Specifications.and()
.equal("deptNo", "002")
.order().asc("name").build());
或者自定义构建
userMapper.findSpecification((cb, query) -> {
PredicateExpression expression = cb.and(cb.in("deptNo", "002", "003"), cb.isNull("createTime"));
PredicateExpression expression1 = cb.or(cb.lessThan("age", 18), expression);
query.where(cb.equal("name", "Jack"), expression1);
query.orderBy(cb.asc("deptNo"), cb.desc("id"));
return null;
});
# 主键支持
目前支持四种主键类型:自增(IDENTITY)、序列(SEQUENCE)、UUID(32位),SNOWFLAKE(雪花)。可如下定义:
public class User {
@GeneratedValue(GenerationType.IDENTITY)
@Id
private Long id;
//...
}
# 雪花算法主键
关于雪花算法的主键配置如下:
mybatis:
snowflake:
group-id: 1 # 组id
worker-id: 1 # 工作id
time-callback-strategy: waiting # 时间回拨策略(等待,备用工作id,修改偏移量)
max-back-time: 1 # 最大可回拨时间,超出则报异常,表示生成id失败
extra-worker-id: 2 # 备用工作id 只有回拨策略是备用工作id时,必须配置此属性
offset: # 默认时间偏移量
雪花算法的主键为时间回拨提供了3种处理方式
WAITING
等待回拨时间,此策略适用于客户端允许时间等待的情况
备用workerId
如果workerId有多余的情况,可以启用备用id。备用id只是在时间段重叠部分换了备用workerId,这样备用workerId变成了主workerId,原来的主workerId变成备用workerId。注意此策略不适应用于回拨时间段在同一区间的,若两次回拨的时间区间重叠了,这时候两个workerId都被使用过了,会造成主键重复
修改偏移量
我们设计时间偏移量,在取得当前时间后,会减去偏移量,从而达到较小数值,从而让ID的生成时间更久。时间回拨,无非就是让最终生成的ID变小了,导致ID重复,我们可以考虑修改偏移量来达到ID变大。让偏移量减去回拨时间区间,从而让最终的ID变成和原来的轨迹。
注意
时间偏移量的修改只是在内存中修改了。如果项目重启了,请确认项目的重启间隔大于回拨时间。
由于现在很多项目使用云原生,会导致项目不间断,也就是说没有重启间隔时间。我们提供了外部存储时间偏移量接口
OffsetRepository
,在实现接口后让spring管理,示例如下:@Component public class MyOffsetRepository implements OffsetRepository, EnvironmentAware { private Environment env; @Autowired private RedisTemplate<Object, Object> redisTemplate; @Override public boolean saveOffset(Class<?> entityClass, Long offset) { // 由于是多应用,我们可以采用应用名称来区分不同项目,也可以采用`groupId+workerId`来区分 String applicationName = env.getProperty("spring.application.name"); redisTemplate.opsForValue().set(applicationName + "." + entityClass.getName(), offset); return true; } @Override public Long getOffset(Class<?> entityClass) { String applicationName = env.getProperty("spring.application.name"); Object o = redisTemplate.opsForValue().get(applicationName + "." + entityClass.getName()); return (Long) o; } @Override public void setEnvironment(Environment env) { this.env = env; } }
# 自定义主键生成规则
现阶段生成id的方式特别多,特别是基于分布式的情况,所以提供了扩展给使用者,让使用者自定义id生成规则。
通过实现KeyGenerator#generate
,然后在定义id生成规则的时候指定generatorClass:
// 定义主键生成类
public class MyGenerator implements KeyGenerator {
@Override
public String generate() {
return System.currentTimeMillis() + "";
}
}
// 指定用这个类生成这个主键,在插入的时候会自动调用此类的generate方法
@Id
@GeneratedValue(generatorClass = MyGenerator.class)
private String id;
# 分页排序支持
已经实现了自动物理分页。
在方法里传Page
, Sort
即可。如:
List<TestUser> findPageByName(Page page, Sort sort, String name);
返回的total也在此对象里,拿到即可。
排序传入Sort
对象,Sort
对象可以构造多个排序
- 如果只是分页,不查询总数,可设置
page.setSelectCount(false)
- 为应对多数据源,可手动设置DatabaseType:
page.setDatabaseType(databaseType)
- 如果想自动根据不同数据源设置不同的type,可在配置里设置
mybatis.page.autoDialect=true
- 如果不想分页查询,则可以传入page=null
注意
若使用传入参数排序,则不要用接口定义的方式定义排序。只能选一种。
传入参数排序可动态排序,根据传入的参数不同排序方式不一样; 接口定义的方式排序只能固定排序。
分页中,只对框架生成的sql提供了count sql优化,自定义的sql暂未做解析优化。
# 默认值触发
默认值触发是指类似于触发器,在我们插入或更新时候指定某些字段的默认值。而不需要我们每次处理的时候去设置值。
默认值分两种:java代码、系统函数。java代码可直接定义,也可定义类和方法。系统函数分直接替换占位。
可使用@TriggerValue
注解实现。如:
@TriggerValue(triggers = @Trigger(triggerType = SqlCommandType.INSERT, valueType = TriggerValueType.DatabaseFunction, value = "sysdate"))
private Date createTime;
@TriggerValue(triggers = @Trigger(triggerType = SqlCommandType.UPDATE, valueType = TriggerValueType.JavaCode, valueClass = TestSeqService.class, methodName = "getDate"))
private Date updateTime;
# 自定义数据库扩展
不可能实现所有的关系型数据库,故将数据库的扩展功能交给使用者。
对于spring项目可以通过实现MybatisJpaConfigurer#addDatabase
方法,添加定制化数据库,同时需要定义分页PaginationDialect
。若有序列生成id的方法,也需实现KeySqlGenerator
@Override
public void addDatabase(DatabaseRegistry databaseRegistry) {
databaseRegistry.addDatabase(DatabaseType.valueOf("CustomDatabaseId"))
.paginationDialect(new MyPaginationDialect())
.keySqlGenerator(new MyPaginationDialect());
}
这个MybatisJpaConfigurer应被注册为spring的bean
# Pageable入参解析
对于spring mvc项目,让排序传入更简单更优雅。若集成swagger,则可以通过swagger-ui查看具体的参数信息
# MybatisJpaStartedEvent
对于spring项目,提供一个Event,在jpa加载完成后发布一个事件。可以利用此事件执行一些后置方法。
比如我们需要在框架启动后去调用jpa构建的statement,不然会报找不到statement的异常
# code-generator
这是一个maven插件,方便生成实体类和mapper,使用方式如下:
在需要生成代码的项目或模块里,定义一个xml文件
generate.xml
:<?xml version="1.0" encoding="UTF-8"?> <config> <datasource> <url>jdbc:mysql://localhost:3306/test?characterEncoding=utf-8</url> <driverName>com.mysql.jdbc.Driver</driverName> <username>root</username> <password>root</password> </datasource> <properties> <projectPath>src/main/java</projectPath> <packageName>com.alilitech</packageName> </properties> <tables> <table tableName="t_user" domainName="User" /> <!-- 可以多个table --> </tables> </config>
TIP
将此文件置于classpath下,注意此项目本身要有相关的数据库连接驱动,否则无法连接数据库。
table标签属性配置如下:
domainName 可以不填,默认是tableName转换成大写驼峰
overrideDomain 文件存在时是否覆盖针对Domain,默认为true
overrideMapper 文件存在时是否覆盖针对Mapper,默认为false
ignoreNoStandardUnderscore 忽略非标准的下划线,一般针对有数字的字段属性。默认为false,不忽略,也就是在字段上需要加上@Column字段
配置插件
<build> <plugins> <plugin> <groupId>com.alilitech</groupId> <artifactId>boot-plus-generator</artifactId> <version>1.3.8</version> <dependencies> <dependency> <groupId>com.alilitech</groupId> <artifactId>boot-plus-mybatis-jpa</artifactId> <version>1.3.8</version> </dependency> </dependencies> </plugin> </plugins> </build>
运行插件