本文为稀土掘金技术社区首发签约文章,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 点:
- 保持程序中代码的一致性,我个人觉得 MyBaits 注解和 XML 映射器的混用会让代码看起来非常“混乱”,不便于管理;
- 一旦使用场景变得复杂,MyBatis 注解对实现动态 SQL 语句的支持并不是十分友好,处理起来会比较麻烦;
- 就第 2 点而言,如果使用 MyBatis 注解实现动态 SQL 语句,SQL 语句的可读性就回变的很差,这点可以在我们的例子中有明显的感受。
到这里,关于 MyBatis 中使用常用的注解实现 SQL 语句的内容就全部完成了,除了用于实现 SQL 语句和结果集映射的方法外,MyBatis 还提供了一些其它的注解,这些就留给感兴趣的小伙伴自行探索了。