MyBaits中使用注解实现SQL语句

602 阅读13分钟

本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!
大家好,我是王有志,一个分享硬核 Java 技术的金融摸鱼侠,欢迎大家加入 Java 人自己的交流群“共同富裕的 Java 人”。

有些时候,我们应用程序中的表结构会非常简单,可能只是一张由 Key-Value 组成配置表,而且这张表关联的业务逻辑也非常简单,只需要提供简单的查询功能即可,甚至不需要实现新增,修改和删除功能,那么对于一张这样简单的表来说,我们再去使用 XML 文件构建映射器就会显得非常“笨重”,那么 MyBatis 中有没有什么简便的方式去实现查询功能呢?

有的,MyBatis 提供了能够实现 XML 映射器功能的注解,让我们能够直接在 Mapper 接口的方法上编写 SQL 语句,从而减少 XML 配置。

Tips:我个人是不推荐使用 MyBatis 的注解实现 SQL 语句的,但是作为 MyBatis 中的一部分,我们还是可以来学习了解下 MyBatis 注解实现 SQL 语句的用法。

前期准备

数据库表

准备一张非常简单的配置表,并且准备一些初始化数据,SQL 语句如下:

create table property(
  id             int auto_increment primary key,
  property_type  varchar(20)  not null,
  property_key   varchar(50)  not null,
  property_value varchar(500) not null
);

INSERT INTO property (id, property_type, property_key, property_value)
VALUES (1, 'type-1', 'key-1', 'value-1');
INSERT INTO property (id, property_type, property_key, property_value)
VALUES (2, 'type-2', 'key-2', 'value-2');
INSERT INTO property (id, property_type, property_key, property_value)
VALUES (3, 'type-3', 'key-3', 'value-3');

Java 持久化对象

实现数据库表对应的 Java 的持久化对象,代码如下:

@Getter
@Setter
public class PropertyDO implements Serializable {

  @Serial
  private static final long serialVersionUID = -603679783492897168L;

  private Integer id;

  private String propertyType;

  private String propertyKey;

  private String propertyValue;
}

Mapper 接口

创建一个空的 PropertyMapper 接口,代码如下:

public interface PropertyMapper {
}

MyBatis 核心配置文件

配置 MyBatis 的核心配置文件 mybatis-config.xml,环境和数据源的配置都与我们之前的配置相同。具体的差异体现在映射器的配置上,之前我们在 mapper 元素中配置的是 XML 映射器文件,现在我们需要配置的是 Mapper 接口,完整的配置如下:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">

<configuration>
  <properties resource="mysql-config.properties"/>
  <settings>
    <setting name="logImpl" value="STDOUT_LOGGING"/>
    <setting name="mapUnderscoreToCamelCase" value="true"/>

  </settings>

  <environments default="MySQL_environment">
    <environment id="MySQL_environment">
      <transactionManager type="JDBC"/>
      <dataSource type="POOLED">
        <property name="driver" value="${mysql.driver}"/>
        <property name="url" value="${mysql.url}"/>
        <property name="username" value="${mysql.username}"/>
        <property name="password" value="${mysql.password}"/>
      </dataSource>
    </environment>
  </environments>

  <mappers>
    <mapper class="com.wyz.mapper.PropertyMapper"/>
  </mappers>
</configuration>

注意我们在配置 Mapper 接口时 mapper 元素使用了 class 属性,而不是之前配置映射器时使用的的 resource 属性。

测试文件

准备一个测试文件,提前做好 SqlSessionFactory 实例,SqlSession 实例和 PropertyMapper 实例的初始化工作,代码如下:

public class AnnotationTest {

  private static SqlSession sqlSession;

  private static PropertyMapper propertyMapper;

  @BeforeClass
  public static void init() throws IOException {
    Reader mysqlReader = Resources.getResourceAsReader("mybatis-config.xml");
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(mysqlReader);
    sqlSession = sqlSessionFactory.openSession();
    propertyMapper = sqlSession.getMapper(PropertyMapper.class);
  }
}

至此,我们就完成了前期的准备工作了,接下来我们就一起学习如何在 MyBatis 中使用常见的注解进行 SQL 语句的开发。

使用注解实现简单的 SQL 语句

这一部分,我们一起来学习如何使用 MyBatis 提供的 @Select 注解,@Insert 注解,@Update 注解和 @Delete 注解实现数据库的增删改查功能。这些注解的使用方式非常简单,只需要在注解中编写相应功能的 SQL 语句即可,而且在注解中编写的 SQL 语句与在 XML 映射器中编写的 SQL 语句完全一致。

使用 @Select 注解实现查询语句

使用 @Select 注解实现通过 id 查询 property 表数据的功能,代码如下:

@Select("select * from property where id = #{id, jdbcType=INTEGER}")
PropertyDO selectById(Integer id);

单元测试代码如下:

@Test
public void testSelectById() {
  PropertyDO property = propertyMapper.selectById(1);
  System.out.println(JSON.toJSONString(property));
}

使用 @Insert 注解实现新增语句

使用 @Insert 注解来实现向 property 表中插入数据的功能,代码如下:

@Insert("insert into property(id, property_type, property_key, property_value) " +
        "value (#{id, jdbcType=INTEGER}, #{propertyType, jdbcType=VARCHAR}, #{propertyKey, jdbcType=VARCHAR}, #{propertyValue, jdbcType=VARCHAR})")
int insert(PropertyDO property);

单元测试代码如下:

@Test
public void testInsert() {
  PropertyDO property = new PropertyDO();
  property.setId(99);
  property.setPropertyType("type-99");
  property.setPropertyKey("key-99");
  property.setPropertyValue("value-99");
  propertyMapper.insert(property);
  sqlSession.commit();
}

插入时使用自增主键

在编写插入语句时,我们可以使用 @Options 注解使用自增主键,代码如下:

@Insert("insert into property(, property_type, property_key, property_value) " +
        "value (#{propertyType, jdbcType=VARCHAR}, #{propertyKey, jdbcType=VARCHAR}, #{propertyValue, jdbcType=VARCHAR})")
@Options(useGeneratedKeys = true, keyProperty = "id")
int insertByAutoIncrement(PropertyDO property);

Tips:实际上,即便不使用 @Options 注解,MySQL 也可以实现同样的功能。

插入时使用非自增主键

如果我们不使用自增主键,还可以使用 @SelectKey 注解实现自定义主键,代码如下:

@Insert("insert into property(id, property_type, property_key, property_value) " +
        "value (#{id, jdbcType=INTEGER}, #{propertyType, jdbcType=VARCHAR}, #{propertyKey, jdbcType=VARCHAR}, #{propertyValue, jdbcType=VARCHAR})")
@SelectKey(statement = "select 10000 ",
           keyProperty = "id",
           resultType = Integer.class,
           before = true)
int insertByCustomizeKey(PropertyDO property);

解释一下 @SelectKey 注解中使用的 4 个属性:

  • statement,用于声明获取主键的 SQL 语句;
  • keyProperty,声明主键在 Java 持久化对象中的字段;
  • resultType,声明生成主键的 Java 类型;
  • before,声明生成主键的 SQL 语句是在插入语句之前执行还是在之后执行,默认为 false,即在插入语句之后执行。

Tips:以上两种设置主键的方式大家了解即可,通常我们在生产应用程序中不会使用这种方式。

使用 @Update 注解实现修改语句

使用 @Update 注解来实现通过 Id 更新 property 表数据的功能,代码如下:

@Update("update property set property_type = #{propertyType, jdbcType=VARCHAR}, property_key = #{propertyKey, jdbcType=VARCHAR}, property_value = #{propertyValue, jdbcType=VARCHAR} " +
        "where id = #{id, jdbcType=INTEGER}")
int updateById(PropertyDO property);

单元测试代码如下:

@Test
public void testUpdateById() {
  PropertyDO property = new PropertyDO();
  property.setId(99);
  property.setPropertyType("type-99-1");
  property.setPropertyKey("key-99-1");
  property.setPropertyValue("value-99-1");
  propertyMapper.updateById(property));
  sqlSession.commit();
}

使用 @Delete 注解实现删除语句

最后,我们使用 @Delete 注解来实现根据 Id 删除 property 表中数据的功能,代码如下:

@Delete("delete from property where id = #{id, jdbcType=INTEGER}")
int deleteById(Integer id);

单元测试代码如下:

@Test
public void testDeleteById() {
  System.out.println(propertyMapper.deleteById(99));
  sqlSession.commit();
}

可以看到我们在使用注解实现简单的增删改查功能会非常简单,而且代码非常简洁,这是使用 MyBatis 注解实现 SQL 语句带来的优势。

Tips:尽管网络上给出了使用 MyBatis 注解编写 SQL 语句能够带来的各种各样的好处,但我个人认为,这种方式唯一的优点只有简洁,同时我也不太推荐使用 MyBatis 注解的方式编写 SQL 语句,如果非要使用的话,就在这种 SQL 语句非常简单的功能中使用。

使用注解定义数据库结果集与 Java 持久化对象之间的映射关系

上面的内容非常简单,接下来我们来上点强度(实际上强度也不大),我们来学习如使用 MyBatis 中的注解实现结果集映射和动态 SQL 语句。

使用 @Results 注解实现结果集映射

同样的,我们可能遇到数据库表中的字段与 Java 持久化对象中的字段命名不一致的情况,这种情况下 MyBatis 的插件 mapUnderscoreToCamelCase 就失效了,这时我们需要通过自定义映射规则来实现数据库结果接与 Java 持久化对象之间的映射关系。

MyBatis 中提供了 @Results 注解来实现这种映射关系,使用方式如下:

@Results(id = "BaseResultMap", value = {
  @Result(property = "id", column = "id", jdbcType = JdbcType.INTEGER),
  @Result(property = "propertyType", column = "property_type", jdbcType = JdbcType.VARCHAR),
  @Result(property = "propertyKey", column = "property_key", jdbcType = JdbcType.VARCHAR),
  @Result(property = "propertyValue", column = "property_value", jdbcType = JdbcType.VARCHAR)
})
@Select("select * from property where id = #{id, jdbcType=INTEGER}")
PropertyDO selectById(Integer id);

基本上与我们在 XML 映射器中使用 resultMap 元素定义映射关系的使用方式一致。

你可以注释掉 mybatis-config.xml 中的 mapUnderscoreToCamelCase 插件再来测试,看一下控制台输出的数据,字段间的映射关系是否正确。

关于@Results 注解你还需要注意,在它的声明上 @Results 注解只能够作用在方法上,@Results 注解的部分源代码如下:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Results {
  // 省略
}

Tips:除了自定义映射规则外,使用数据库别名依旧是能够实现数据库结果集与 Java 持久化对象字段之间的映射的。

使用 @ResultMap 注解实现结果集映射

你可能会有疑惑,如果 @Results 注解只能作用在方法上,是不是涉及到自定义映射关系的方法都需要重复定义呢?

在早期的 MyBatis 版本(MyBatis 3.3.0 版本及之前的版本)中确实是这样的,但是经过后来的改进,我们只需要使用 @ResultMap 注解并结合 @Results 注解的 id 属性,就能够复用 @Results 注解定义的映射关系了

我们再来实现一个查询所有 proprerty 表中数据的方法,并使用 @ResultMap 注解引用使用 @Results 注解定义的映射规则,代码如下:

@ResultMap("BaseResultMap")
@Select("select * from property")
List<PropertyDO> selectAll();

单元测试代码如下:

@Test
public void testSelectAll() {
  List<PropertyDO> properties = propertyMapper.selectAll();
  System.out.println(JSON.toJSONString(properties));
}

当然了,@ResultMap 注解也可以引用 XML 映射器文件中定义的映射规则。

我们先来定义映射器文件 PropertyMapper.xml,并在其中定义映射规则,代码如下:

<?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.wyz.mapper.PropertyMapper">
  <resultMap id="XMLBaseResultMap" type="com.wyz.entity.PropertyDO">
    <id column="id" jdbcType="INTEGER" property="id"/>
    <result column="property_type" jdbcType="VARCHAR" property="propertyType"/>
    <result column="property_key" jdbcType="VARCHAR" property="propertyKey"/>
  </resultMap>
</mapper>

为了体现与之前使用 @Results 注解定义的映射规则区分,我这里定义的映射规则“XMLBaseResultMap”中并没有定义 property 表中 property_value 字段与 Java 持久化对象 PropertyDO 中 propertyValue 字段的映射关系。

接着我们将 PropertyMapper.xml 注册到 MyBatis 的应用程序中,mybatis-config.xml 中的配置如下:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">

<configuration>
  <!-- 省略部分配置-->

  <mappers>
    <mapper class="com.wyz.mapper.PropertyMapper"/>
    <mapper resource="mapper/PropertyMapper.xml"/>
  </mappers>
</configuration>

接着我们修改PropertyMapper#selectAll方法,代码如下:

@ResultMap("XMLBaseResultMap")
@Select("select * from property")
List<PropertyDO> selectAll();

最后我们来执行上面的单元测试,通过控制台的输出,我们可以看到通过数据库查询到的结果集成功的映射到了 PropertyDO 对象上,并且 PropertyDO 对象的 propertyValue 字段并没有映射上数据。

使用注解实现动态 SQL 语句

使用脚本在注解中实现动态 SQL 语句

首先是一种“挂羊头卖狗肉”的方式,使用前面提到的注解,并使用 script 元素拼接我们在 XML 映射器中编写的 SQL 语句即可,例如,我们来实现一个查询语句,在条件子句中动态拼接参数,代码如下:

@ResultMap("BaseResultMap")
@Select("<script>" +
        "       select * from property\n" +
        "        <where>\n" +
        "            <if test=\"id != null\">\n" +
        "                and id = #{id}\n" +
        "            </if>\n" +
        "            <if test=\"propertyType != null\">\n" +
        "                and property_type = #{propertyType}\n" +
        "            </if>\n" +
        "            <if test=\"propertyKey != null\">\n" +
        "                and property_key = #{propertyKey}\n" +
        "            </if>\n" +
        "            <if test=\"propertyValue != null\">\n" +
        "                and property_value = #{propertyValue}\n" +
        "            </if>\n" +
        "        </where>" +
        "</script>")
List<PropertyDO> selectByProperty(PropertyDO property);

可以看到,我在注解 @Select 中使用了 script 元素,并在 script 元素中使用 XML 映射器中的元素完成的动态 SQL 语句的编写(实际上 SQL 语句就是慰我从 XML 映射器中粘过来的,一点都没改)。

另外,这么使用的话 IDEA 是会有报错的提示的,如下图所示:

这个报错提示没有任何影响,并不会耽误 SQL 语句的执行。

当然,如果你使用这种方式在 MyBatis 的注解中实现动态 SQL 语句,那还不如直接用 XML 映射器去实现呢,而且这也不是 MyBaits 中使用注解实现动态 SQL 语句的“正统”方式。

使用 4 种 Provider 注解实现动态 SQL 语句

既然使用脚本的方式不是 MyBatis 注解实现动态 SQL 语句的“正统”,那么“正统”是什么?

答案是使用 4 种 Provider 注解:

  • @SelectProvider 注解,用于实现动态的查询语句;
  • @InsertProvider 注解,用于实现动态的插入语句;
  • @UpdateProvider 注解,用于实现动态的更新语句;
  • @DeleteProvider 注解,用于实现动态的删除语句。

这里以使用 @SelectProvider 注解实现动态的查询语句为例,我们来实现一个与PropertyMapper#selectByProperty方法功能一样的动态。

使用之前,我们先来简单了解下 @SelectProvider 注解,部分源码如下:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Repeatable(SelectProvider.List.class)
public @interface SelectProvider {
  /**
   * Specify a type that implements an SQL provider method.
   * @return a type that implements an SQL provider method
   */
  Class<?> value() default void.class;

  /**
   * Specify a type that implements an SQL provider method.
   * This attribute is alias of {@link #value()}.
   * @return a type that implements an SQL provider method
   */
  Class<?> type() default void.class;

  /**
   * Specify a method for providing an SQL.
   * @return a method name of method for providing an SQL
   */
  String method() default "";
}

@SelectProvider 注解中主要的字段有两个(value 字段与 type 字段作用相同):

  • type(value)字段:指定实现 SQL 语句的 Java 类;
  • method 字段:指定该 Java 类中实现 SQL 语句的方法。

接下来我们就按照 @SelectProvider 注解的要求来实现一个用于提供 SQL 语句的 Java 类和方法,由于是为 PropertyMapper 提供 SQL 语句的所以这里我将它命名为“PropertyMapperProvider”,并且实现一个提供 SQL 语句的方法,代码如下:

import com.wyz.entity.PropertyDO;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.jdbc.SQL;

public class PropertyMapperProvider {

  public String selectByPropertyUseProvider(PropertyDO property) {
    SQL sql = new SQL();
    sql.SELECT("*").FROM("property");
    if (property.getId() != null) {
      sql.WHERE("id = #{id, jdbcType=INTEGER}");
    }
    if (StringUtils.isNotBlank(property.getPropertyType())) {
      sql.WHERE("property_type = #{propertyType, jdbcType=VARCHAR}");
    }
    if (StringUtils.isNotBlank(property.getPropertyKey())) {
      sql.WHERE("property_key = #{propertyKey, jdbcType=VARCHAR}");
    }
    if (StringUtils.isNotBlank(property.getPropertyValue())) {
      sql.WHERE("property_value = #{propertyValue, jdbcType=VARCHAR}");
    }
    return sql.toString();
  }
}

PropertyMapperProvider#selectByPropertyUseProvider方法的实现非常简单,借助 MyBatis 提供的 SQL 类动态组装了一条 SQL 语句。注意,MyBatis 中要求提供 SQL 语句的方法的返回值类型必须为 String。

下面我们就在 PropertyMapper 中定义新的方法,并使用 @SelectProvider 注解,代码如下:

@ResultMap("BaseResultMap")
@SelectProvider(type = PropertyMapperProvider.class, method = "selectByPropertyUseProvider")
List<PropertyDO> selectByPropertyUseProvider(PropertyDO property);

最后你可以自己写一个单元测试,来测试你的代码,这里我再偷个懒~~

我们回过头来看用于提供 SQL 语句的 PropertyMapperProvider 类中的方法,前面我们说过,MyBatis 对提供 SQL 语句的方法的要求只有一点(返回的 SQL 语句的正确性这种要求就不算了),那就是返回值类型必须为 String,也就是说我们不使用 MyBatis 提供的 SQL,直接组装字符串也是可以的,这里我再实现一个直接使用字符串组装动态 SQL 语句的方法,代码如下:

public String selectByPropertyUseProviderCustomize(PropertyDO property) {
  String sql = "select * from property";
  List<String> wheres = new ArrayList<>();
  if (property.getId() != null) {
    wheres.add("id = #{id, jdbcType=INTEGER}");
  }
  if (StringUtils.isNotBlank(property.getPropertyType())) {
    wheres.add("property_type = #{propertyType, jdbcType=VARCHAR}");
  }
  if (StringUtils.isNotBlank(property.getPropertyKey())) {
    wheres.add("property_key = #{propertyKey, jdbcType=VARCHAR}");
  }
  if (StringUtils.isNotBlank(property.getPropertyValue())) {
    wheres.add("property_value = #{propertyValue, jdbcType=VARCHAR}");
  }
  StringBuilder where = new StringBuilder(" where ");
  for (int i = 0; i < wheres.size(); i++) {
    if (i == 0) {
      where.append(wheres.get(i));
    } else {
      where.append(" and ").append(wheres.get(i));
    }
  }
  return sql + where;
}

大家可以替换掉PropertyMapperProvider#selectByPropertyUseProvider方法,再来执行下我们刚才编写的单元测试。

至于 @InsertProvider 注解,@UpdateProvider 注解和 @DeleteProvider 注解,在使用方式上与 @SelectProvider 注解完全相同,这里我就不再赘述了。

就我个人而言,尽管在简单的场景下使用 MyBatis 的注解实现 SQL 语句会让程序看起来比较简洁,但是我依旧不会使用 MyBatis 的注解来实现 SQL 语句,我考虑的主要有 3 点:

  1. 保持程序中代码的一致性,我个人觉得 MyBaits 注解和 XML 映射器的混用会让代码看起来非常“混乱”,不便于管理;
  2. 一旦使用场景变得复杂,MyBatis 注解对实现动态 SQL 语句的支持并不是十分友好,处理起来会比较麻烦;
  3. 就第 2 点而言,如果使用 MyBatis 注解实现动态 SQL 语句,SQL 语句的可读性就回变的很差,这点可以在我们的例子中有明显的感受。

到这里,关于 MyBatis 中使用常用的注解实现 SQL 语句的内容就全部完成了,除了用于实现 SQL 语句和结果集映射的方法外,MyBatis 还提供了一些其它的注解,这些就留给感兴趣的小伙伴自行探索了。


尾图.png