02.MyBatis

MyBatis

MyBatis是支持普通SQL查询,存储过程和高级映射的优秀持久层框架,它提供一种半自动化的ORM实现。这里的“半自动化”,是相对Hibernate等提供了全面的数据库封装机制的“全自动化”ORM实现而言,全自动ORM实现了POJO和数据库表之间的映射,以及SQL的自动生成和执行。MyBatis消除了几乎所有的JDBC代码和参数的手工设置以及对结果集的检索。MyBatis可以使用简单的XML或注解用于配置和原始映射,将接口和JavaPOJO(Plain Old Java Objects,普通的Java对象)映射成数据库中的记录。

MyBatis允许将SQL写在XML中,便于统一的管理与优化,并且与程序代码解耦合。同时MyBatis提供了映射标签,支持对象与数据库ORM字段关系映射,支持编写动态SQL。不过MyBatis也存在不少的痛点,由于XML里标签ID必须唯一,导致DAO中的方法不支持重载,并且DAO层过于简单,仍然需要大量对象组装的工作量。同时字段映射标签和对象关系映射标签仅仅是对映射关系的描述,具体实现仍然依赖于SQL;譬如配置了一对多的Collection标签之后,如果SQL中没有Join子查询或者查询子表的话,查询后返回的对象是不具备对象关系的,即Collection的对象为null

快速开始

新建Spring Boot项目,在pom.xml中引入MyBatisStarter以及MySQL Connector依赖,具体如下:

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.1</version>
</dependency>

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

关于mybatis-spring-boot-starter的版本需要注意:

  • 2.1.x版本适用于:MyBatis 3.5+、Java 8+、Spring Boot 2.1+
  • 2.0.x版本适用于:MyBatis 3.5+、Java 8+、Spring Boot 2.0/2.1
  • 1.3.x版本适用于:MyBatis 3.4+、Java 6+、Spring Boot 1.5

同之前介绍的使用jdbc模块和jpa模块连接数据库一样,在application.properties中配置mysql的连接配置:

spring.datasource.url=jdbc:mysql://localhost:3306/test
spring.datasource.username=root
spring.datasource.password=
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

Mysql中创建一张用来测试的表,比如:User表,其中包含id(BIGINT)、age(INT)、name(VARCHAR)字段。

CREATE TABLE `User` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `name` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL,
  `age` int DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci

创建User表的映射对象User

@Data
@NoArgsConstructor
public class User {

    private Long id;

    private String name;
    private Integer age;

    public User(String name, Integer age) {
        this.name = name;
        this.age = age;
    }
}

创建User表的操作接口:UserMapper。在接口中定义两个数据操作,一个插入,一个查询,用于后续单元测试验证。

@Mapper
public interface UserMapper {

    @Select("SELECT * FROM USER WHERE NAME = #{name}")
    User findByName(@Param("name") String name);

    @Insert("INSERT INTO USER(NAME, AGE) VALUES(#{name}, #{age})")
    int insert(@Param("name") String name, @Param("age") Integer age);

}

创建Spring Boot主类:

@SpringBootApplication
public class Application {

	public static void main(String[] args) {
		SpringApplication.run(Application.class, args);
	}

}

创建单元测试。具体测试逻辑如下:

  • 插入一条name=AAAage=20的记录,然后根据name=AAA查询,并判断age是否为20
  • 测试结束回滚数据,保证测试单元每次运行的数据环境独立
@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
public class ApplicationTests {

    @Autowired
    private UserMapper userMapper;

    @Test
    @Rollback
    public void test() throws Exception {
        userMapper.insert("AAA", 20);
        User u = userMapper.findByName("AAA");
        Assert.assertEquals(20, u.getAge().intValue());
    }

}

注解配置说明

下面通过几种不同传参方式来实现前文中实现的插入操作,来学习一下MyBatis中常用的一些注解。

  • 使用@Param

在之前的整合示例中我们已经使用了这种最简单的传参方式,如下:

@Insert("INSERT INTO USER(NAME, AGE) VALUES(#{name}, #{age})")
int insert(@Param("name") String name, @Param("age") Integer age);

这种方式很好理解,@Param中定义的name对应了SQL中的#{name}age对应了SQL中的#{age}

  • 使用Map

如下代码,通过 Map<String, Object> 对象来作为传递参数的容器:

@Insert("INSERT INTO USER(NAME, AGE) VALUES(#{name,jdbcType=VARCHAR}, #{age,jdbcType=INTEGER})")
int insertByMap(Map<String, Object> map);

对于Insert语句中需要的参数,我们只需要在map中填入同名的内容即可,具体如下面代码所示:

Map<String, Object> map = new HashMap<>();
map.put("name", "CCC");
map.put("age", 40);
userMapper.insertByMap(map);
  • 使用对象

除了Map对象,我们也可直接使用普通的Java对象来作为查询条件的传参,比如我们可以直接使用User对象:

@Insert("INSERT INTO USER(NAME, AGE) VALUES(#{name}, #{age})")
int insertByUser(User user);

这样语句中的#{name}#{age}就分别对应了User对象中的nameage属性。

增删改查

MyBatis针对不同的数据库操作分别提供了不同的注解来进行配置,在之前的示例中演示了@Insert,下面针对User表做一组最基本的增删改查作为示例:

public interface UserMapper {

    @Select("SELECT * FROM user WHERE name = #{name}")
    User findByName(@Param("name") String name);

    @Insert("INSERT INTO user(name, age) VALUES(#{name}, #{age})")
    int insert(@Param("name") String name, @Param("age") Integer age);

    @Update("UPDATE user SET age=#{age} WHERE name=#{name}")
    void update(User user);

    @Delete("DELETE FROM user WHERE id =#{id}")
    void delete(Long id);
}

在完成了一套增删改查后,不妨我们试试下面的单元测试来验证上面操作的正确性:

@Transactional
@RunWith(SpringRunner.class)
@SpringBootTest
public class ApplicationTests {

	@Autowired
	private UserMapper userMapper;

	@Test
	@Rollback
	public void testUserMapper() throws Exception {
		// insert一条数据,并select出来验证
		userMapper.insert("AAA", 20);
		User u = userMapper.findByName("AAA");
		Assert.assertEquals(20, u.getAge().intValue());
		// update一条数据,并select出来验证
		u.setAge(30);
		userMapper.update(u);
		u = userMapper.findByName("AAA");
		Assert.assertEquals(30, u.getAge().intValue());
		// 删除这条数据,并select验证
		userMapper.delete(u.getId());
		u = userMapper.findByName("AAA");
		Assert.assertEquals(null, u);
	}
}

返回结果绑定

对于增、删、改操作相对变化较小。而对于“查”操作,我们往往需要进行多表关联,汇总计算等操作,那么对于查询的结果往往就不再是简单的实体对象了,往往需要返回一个与数据库实体不同的包装类,那么对于这类情况,就可以通过@Results@Result注解来进行绑定,具体如下:

@Results({
    @Result(property = "name", column = "name"),
    @Result(property = "age", column = "age")
})
@Select("SELECT name, age FROM user")
List<User> findAll();

在上面代码中,@Result中的property属性对应User对象中的成员名,column对应SELECT出的字段名。在该配置中故意没有查出id属性,只对User对应中的nameage对象做了映射配置,这样可以通过下面的单元测试来验证查出的idnull,而其他属性不为null

@Test
@Rollback
public void testUserMapper() throws Exception {
	List<User> userList = userMapper.findAll();
	for(User user : userList) {
		Assert.assertEquals(null, user.getId());
		Assert.assertNotEquals(null, user.getName());
	}
}

数据类型

无论是MyBatis在预处理语句(PreparedStatement)中设置一个参数时,还是从结果集中取出一个值时,都会用类型处理器将获取的值以合适的方式转换成Java类型。从3.4.5开始,MyBatis默认支持JSR-310(日期和时间API)

 JDBCType            JavaType
  CHAR                String
  VARCHAR             String
  LONGVARCHAR         String
  NUMERIC             java.math.BigDecimal
  DECIMAL             java.math.BigDecimal
  BIT                 boolean
  BOOLEAN             boolean
  TINYINT             byte
  SMALLINT            short
  INTEGER             int
  BIGINT              long
  REAL                float
  FLOAT               double
  DOUBLE              double
  BINARY              byte[]
  VARBINARY           byte[]
  LONGVARBINARY               byte[]
  DATE                java.sql.Date
  TIME                java.sql.Time
  TIMESTAMP           java.sql.Timestamp
  CLOB                Clob
  BLOB                Blob
  ARRAY               Array
  DISTINCT            mapping of underlying type
  STRUCT              Struct
  REF                 Ref
  DATALINK            java.net.URL

使用XML配置

第一步:在应用主类中增加mapper的扫描包配置:

@MapperScan("com.didispace.chapter36.mapper")
@SpringBootApplication
public class Chapter36Application {

	public static void main(String[] args) {
		SpringApplication.run(Chapter36Application.class, args);
	}

}

第二步:在第一步中指定的Mapper包下创建User表的Mapper定义:

public interface UserMapper {

    User findByName(@Param("name") String name);

    int insert(@Param("name") String name, @Param("age") Integer age);

}

第三步:在配置文件中通过mybatis.mapper-locations参数指定xml配置的位置:

mybatis.mapper-locations=classpath:mapper/*.xml

第四步:在第三步中指定的xml配置目录下创建User表的mapper配置:

<?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.didispace.chapter36.mapper.UserMapper">
    <select id="findByName" resultType="com.didispace.chapter36.entity.User">
        SELECT * FROM USER WHERE NAME = #{name}
    </select>

    <insert id="insert">
        INSERT INTO USER(NAME, AGE) VALUES(#{name}, #{age})
    </insert>
</mapper>

到这里从注解方式的MyBatis使用方式就改为了XML的配置方式了,为了验证是否运行正常,可以通过下面的单元测试来尝试对数据库的写和读操作:

@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
@Transactional
public class Chapter36ApplicationTests {

    @Autowired
    private UserMapper userMapper;

    @Test
    @Rollback
    public void test() throws Exception {
        userMapper.insert("AAA", 20);
        User u = userMapper.findByName("AAA");
        Assert.assertEquals(20, u.getAge().intValue());
    }

}

Links