概述

以下为MyBatis官网对于框架本身的介绍。

MyBatis 是一款优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以使用简单的 XML 或注解来配置和映射原生信息,将接口和 Java 的 POJOs(Plain Old Java Objects,普通的 Java对象)映射成数据库中的记录。

示例

以下以在SpringBoot 中使用 MyBatis 为例,并以注解为配置方式展示Mybatis框架的简单使用。

依赖

关于Spring Boot 的工程创建,网上相关资料比较多,此处不再赘述。

数据库以mysql 为例,首先先引入相关依赖,lombak 和 junit4 依赖引入是为了减少model类编码和方便测试。

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>1.3.2</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

数据库准备

启动一个mysql数据库服务,建一个名称为DEMO的数据库,并创建表结构并插入数据如下。

CREATE TABLE `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
  `sex` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
  `create_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
  `update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

INSERT INTO `user` VALUES (1, 'Tom', 'MALE', '2019-01-31 13:57:22', '2019-01-31 13:57:26');
INSERT INTO `user` VALUES (2, 'Lucy', 'FEMALE', '2019-01-31 13:57:55', '2019-01-31 13:57:57');

CREATE TABLE `order` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` int(11) DEFAULT NULL,
  `create_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
  `update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

INSERT INTO `order` (id, user_id, create_time, update_time) VALUES (1, 1, '2019-01-31 16:57:07', '2019-01-31 16:57:09');
INSERT INTO `order` (id, user_id, create_time, update_time) VALUES (2, 1, '2019-01-31 16:57:39', '2019-01-31 16:57:41');

工程准备

创建实体类如下:

@Setter
@Getter
@ToString
public class User {

    private Long id;
    private String username;
    private UserSex userSex;
    private List<Order> orders;

    public User() {
    }
}

@Getter
@Setter
@ToString
public class Order {

    private Long id;
    private Long userId;
    private Date createTime;
    private Date updateTime;
    private User user;

    public Order() {
    }
}

public enum UserSex {
    MALE,
    FEMALE,
    OTHER;
}

向application.yml文件中添加数据库JDBC配置和Mybatis配置。

spring:
  application:
    name: mybatis-demo
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/test?useLegacyDatetimeCode=false&useSSL=false&characterEncoding=UTF-8
    username: root
    password: 12345678

mybatis:
  type-aliases-package: per.zepon.mybatisdemo.entity
  configuration:
    map-underscore-to-camel-case: true
    default-fetch-size: 100
    default-statement-timeout: 30

CRUD

查询

新建一个名为 UserMapper 的接口,并在方法findAll()上定义查询。其中@Select定义了查询的SQL语句,@Results定义了查询结果转化为POJO的过程。如果表字段名和实体属性不一致(如多词下的下划线分词和驼峰分词),需要指定清楚对应关系和类型。

@Mapper
@Repository
public interface UserMapper {

    @Select("SELECT * FROM user")
    @Results({
            @Result(property = "createTime", column = "create_time", javaType = Date.class, jdbcType = JdbcType.TIMESTAMP),
            @Result(property = "updateTime", column = "update_time", javaType = Date.class, jdbcType = JdbcType.TIMESTAMP)
    })
    List<User> findAll();
}

@Select定义的SQL语句中带参数时,id=#{id}指定了从方法名上读取参数名为id的参数值。

public interface UserMapper {

    @Select("SELECT * FROM user WHERE id = #{id}")
    @Results({
            @Result(property = "createTime", column = "create_time", javaType = Date.class, jdbcType = JdbcType.TIMESTAMP),
            @Result(property = "updateTime", column = "update_time", javaType = Date.class, jdbcType = JdbcType.TIMESTAMP)
    })
    User findById(Long id);
}

还可以像如下方法定义动态查询的方法。定义SelectProvider还有将该表的查询语句聚合到一个类中的作用。

public interface UserMapper {

    @SelectProvider(type = UserDaoProvider.class, method = "findByUsernameAndGender")
    @Results({
            @Result(property = "createTime", column = "create_time", javaType = Date.class, jdbcType = JdbcType.TIMESTAMP),
            @Result(property = "updateTime", column = "update_time", javaType = Date.class, jdbcType = JdbcType.TIMESTAMP)
    })
    User findById(Long id);

    class UserDaoProvider {

        public String findByUsernameAndGender(User user) {
            return new SQL() {{
                SELECT("*");
                FROM("user");
                if (user.getUsername() != null) {
                    WHERE("username=#{username}");
                }
                AND();
                if (user.getGender() != null) {
                    WHERE("gender=#{gender}");
                }
            }}.toString();
        }
    }
}

查询结果还可以映射为指定字段为key,实体为值的Map。@MapKey("id")指定了key取值为主键id,假如指定字段不唯一会导致覆盖。

public interface UserMapper {

    @MapKey("id")
    @Select("select * from user")
    @Results({
            @Result(property = "createTime", column = "create_time", javaType = Date.class, jdbcType = JdbcType.TIMESTAMP),
            @Result(property = "updateTime", column = "update_time", javaType = Date.class, jdbcType = JdbcType.TIMESTAMP)
    })
    Map<String, User> findUserMap();
}

在处理一些一对多,一对一,多对一的表关系上,实体可以定义一个字段,执行二次查询,将关联结果映射成实体一并查出。以下以多对一,一对多展示。

public interface UserMapper {

    @Select("SELECT * FROM user WHERE id=#{id}")
    @Results({
            @Result(property = "id", column = "id"),
            @Result(property = "orders", column = "id", javaType = List.class, many = @Many(select = "per.zepon.mybatisdemo.mybatis.OrderMapper.findByUserId"))
    })
    User findWithOrdersById(Long id);
}

public interface OrderMapper {

    @Select("SELECT * FROM `order` WHERE user_id=#{userId}")
    List<Order> findByUserId(Long userId);
}

调用findWithOrdersById(Long id)查询指定的User表记录的同时将order表与之关联订单记录一起查询得到。下面例子则相反,查询指定ID订单并得到与之关联的用户信息。

public interface OrderMapper {

    @Select("SELECT * FROM `order` WHERE id=#{id}")
    @Results({
            @Result(property = "userId", column = "user_id"),
            @Result(property = "user", column = "user_id", one = @One(select = "per.zepon.mybatisdemo.mybatis.UserMapper.findById"))
    })
    Order findWithUserById(Long id);
}

public interface UserMapper {

    @Select("SELECT * FROM user WHERE id = #{id}")
    User findById(Long id);
}

注: 由于SQL执行结果的列"user_id"被用于二次查询,因此假如不写@Result(property = "userId", column = "user_id") 将列"user_id"再利用一次,最后映射后的实体的userId字段将为null。

插入&更新&删除

与查询大同小异,写操作的SQL也是通过在如@Insert等注解中写SQL,并指定插入参数。以下三个简单的插入、更新及删除的例子。

public interface UserMapper {

    @Insert("INSERT INTO user(username,gender,create_time,update_time) VALUES(#{username}, #{gender}, #{createTime}, #{updateTime})")
    void insert(User user);

    @Update("UPDATE user SET username=#{username},gender=#{gender},update_time=#{updateTime} WHERE id =#{id}")
    void update(User user);

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

总结

MyBatis与使用过的spring data jdbc 相比较而言,支持动态SQL这一点是后者所没有,在一些查询和结果集的映射也会比后者好一些,但Mybatis是一个面向数据库的数据层访问框架,还没有像基于hibernate之类ORM框架的Spring data JPA那么面向对象。但面向数据库编写意味则可以写一些姿势奇怪的SQL,较为灵活。具体使用哪个DAO层框架还是应该根据项目大小和场景的业务需求选择适合的框架辅助开发(废话)。