# 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;

# 分页排序支持

已经实现了自动物理分页。

在方法里传PageSort即可。如:

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>
    
  • 运行插件