掘金 后端 ( ) • 2024-04-03 13:22

highlight: a11y-dark

1. SQL 语句

SQL标签的属性列表:

属性 描述 id 在命名空间中唯一的标识符,可以被用来引用这条语句。 parameterType 将会传入这条语句的参数类的完全限定名或别名。这个属性是可选的,因为 MyBatis 可以通过类型处理器(TypeHandler) 推断出具体传入语句的参数,默认值为未设置(unset)。该参数一般不需要写。 parameterMap 这是引用外部parameterMap的已经被废弃的方法。请使用内联参数映射和 parameterType 属性。 resultType 从这条语句中返回的期望类型的类的完全限定名或别名。 注意如果返回的是集合,那应该设置为集合包含的类型,而不是集合本身。可以使用resultTyperesultMap,但不能同时使用。 resultMap 外部resultMap的命名引用。结果集的映射是 MyBatis 最强大的特性,如果你对其理解透彻,许多复杂映射的情形都能迎刃而解。可以使用resultMapresultType,但不能同时使用。 flushCache 将其设置为true后,只要语句被调用,都会导致本地缓存和二级缓存被清空,默认值:false。 useCache 将其设置为true后,将会导致本条语句的结果被二级缓存缓存起来,默认值:对select元素为true。 timeout 这个设置是在抛出异常之前,驱动程序等待数据库返回请求结果的秒数。默认值为未设置(unset)(依赖驱动)。 fetchSize 这是一个给驱动的提示,尝试让驱动程序每次批量返回的结果行数和这个设置值相等。 默认值为未设置(unset)(依赖驱动)。 statementType 值为STATEMENTPREPAREDCALLABLE中的一个。这会让 MyBatis 分别使用 StatementPreparedStatementCallableStatement,默认值:PREPARED。 resultSetType 值为FORWARD_ONLYSCROLL_SENSITIVESCROLL_INSENSITIVEDEFAULT(等价于unset) 中的一个,默认值为未设置(unset)(依赖驱动)。 databaseId 如果配置了数据库厂商标识(databaseIdProvider),MyBatis 会加载所有的不带 databaseId或匹配当前databaseId的语句;如果带或者不带的语句都有,则不带的会被忽略。 resultOrdered 这个设置仅针对嵌套结果select语句适用:如果为true,就是假设包含了嵌套结果集或是分组,这样的话当返回一个主结果行的时候,就不会发生有对前面结果集的引用的情况。 这就使得在获取嵌套的结果集的时候不至于导致内存不够用。默认值:false。 resultSets 这个设置仅对多结果集的情况适用。它将列出语句执行后返回的结果集并给每个结果集一个名称,名称是逗号分隔的。 useGeneratedKeys (仅对insertupdate有用)这会令 MyBatis 使用JDBCgetGeneratedKeys 方法来取出由数据库内部生成的主键(比如:像MySQLSQL Server这样的关系数据库管理系统的自动递增字段),默认值:false。 keyProperty (仅对insertupdate有用)唯一标记一个属性,MyBatis 会通过getGeneratedKeys的返回值或者通过insert语句的selectKey子元素设置它的键值,默认值为未设置(unset)。如果希望得到多个生成的列,也可以是逗号分隔的属性名称列表。 keyColumn (仅对insertupdate有用)通过生成的键值设置表中的列名,这个设置仅在某些数据库(像PostgreSQL)是必须的,当主键列不是表中的第一列的时候需要设置。如果希望使用多个生成的列,也可以设置为逗号分隔的属性名称列表。

1.1. SELECT

<select
  id="selectPerson"
  parameterType="int"
  parameterMap="deprecated"
  resultType="hashmap"
  resultMap="personResultMap"
  flushCache="false"
  useCache="true"
  timeout="10"
  fetchSize="256"
  statementType="PREPARED"
  resultSetType="FORWARD_ONLY">
<select id="selectPerson" resultType="hashmap">
  SELECT * FROM Person WHERE id = #{id}
</select>

1.2. INSERT

<insert
  id="insertAuthor"
  parameterType="domain.blog.Author"
  flushCache="true"
  statementType="PREPARED"
  keyProperty=""
  keyColumn=""
  useGeneratedKeys=""
  timeout="20">
<insert id="insertAuthor">
  INSERT INTO Author (id, username, password, email, bio)
  VALUES (#{id}, #{username}, #{password}, #{email}, #{bio})
</insert>

1.2.1. 返回自增主键

如果数据库支持自动生成主键(比如MySQLSQL Server),可以设置useGeneratedKeys="true",然后再把 keyProperty设置到目标属性上,就可以实现将新增记录的主键值设置到参数对象的对应属性上。假设Author表已经对id列设置了自增主键。

单条插入:

<insert id="insertAuthor" useGeneratedKeys="true" keyProperty="id">
  INSERT INTO Author (username, password, email, bio)
  VALUES (#{username}, #{password}, #{email}, #{bio})
</insert>

批量插入:

<insert id="batchInsertAuthor" useGeneratedKeys="true" keyProperty="id">
  INSERT INTO Author (username, password, email, bio) VALUES
  <foreach item="item" collection="list" separator=",">
    (#{item.username}, #{item.password}, #{item.email}, #{item.bio})
  </foreach>
</insert>

Mapper 层代码:

@Mapper
public interface AuthorMapper {
    int insertAuthor(Author author);
    int batchInsertAuthor(List<Author> authList);
}

返回的int为插入的记录条数,成功后会自动为参数的author对象的id属性赋值。(注意:即使不写useGeneratedKeys="true" keyProperty="id",数据库记录及其主键也能正常生成,只不过不会回设主键值到 Java 参数对象上而已。)

1.2.2. 提前生成主键

对于不支持自动生成类型的数据库或可能不支持自动生成主键的JDBC驱动,MyBatis 有另外一种方法来生成主键。这里有一个简单的示例,它可以生成一个随机id(开发中最好不要这么做,这里只是为了展示 MyBatis 处理问题的灵活性及其所关心的广度。Oracle中一般是通过查询该表对应的序列获取主键值):

<insert id="insertAuthor">
  <selectKey keyProperty="id" order="BEFORE">
    SELECT CAST(RANDOM()*1000000 as INTEGER) a FROM SYSIBM.SYSDUMMY1
  </selectKey>
  INSERT INTO Author (id, username, password, email, bio)
  VALUES (#{id}, #{username}, #{password}, #{email}, #{bio})
</insert>

在上面的示例中,selectKey元素将会首先运行,Authorid会被设置,然后插入语句会被调用。这给你了一个和数据库中来处理自动生成的主键类似的行为,避免了使 Java 代码变得复杂。

selectKey 标签的属性:

<selectKey
  keyProperty="id"
  resultType="int"
  order="BEFORE"
  statementType="PREPARED">
属性 描述 keyProperty selectKey语句结果应该被设置的目标属性。如果希望得到多个生成的列,也可以是逗号分隔的属性名称列表。 keyColumn 匹配属性的返回结果集中的列名称。如果希望得到多个生成的列,也可以是逗号分隔的属性名称列表。 resultType 结果的类型。MyBatis 通常可以推断出来,但是为了更加精确,写上也不会有什么问题。MyBatis 允许将任何简单类型用作主键的类型,包括字符串。如果希望作用于多个生成的列,则可以使用一个包含期望属性的Object或一个Map。 order 这可以被设置为BEFOREAFTER。如果设置为BEFORE,那么它会首先生成主键,设置 keyProperty然后执行插入语句。如果设置为AFTER,那么先执行插入语句,然后是selectKey中的语句 - 这和Oracle数据库的行为相似,在插入语句内部可能有嵌入索引调用。 statementType 与前面相同,MyBatis 支持STATEMENTPREPAREDCALLABLE语句的映射类型,分别代表StatementPreparedStatementCallableStatement类型。

1.3. UPDATE

<update
  id="updateAuthor"
  parameterType="domain.blog.Author"
  flushCache="true"
  statementType="PREPARED"
  timeout="20">
<update id="updateAuthor">
  UPDATE Author SET
  username = #{username}, password = #{password}, email = #{email}, bio = #{bio}
  WHERE id = #{id}
</update>

1.4. DELETE

<delete
  id="deleteAuthor"
  parameterType="domain.blog.Author"
  flushCache="true"
  statementType="PREPARED"
  timeout="20">
<delete id="deleteAuthor">
  DELETE FROM Author WHERE id = #{id}
</delete>

2. SQL 片段

sql这个元素可以被用来定义可重用的 SQL 代码段,这些 SQL 代码可以被包含在其他语句中。它可以(在加载的时候)被静态地设置参数。在不同的包含语句中可以设置不同的值到参数占位符上。比如:

<sql id="userColumns"> 
  ${alias}.id, ${alias}.username, ${alias}.password 
</sql>

这个 SQL 片段可以被包含在其他语句中,例如:

<select id="selectUsers" resultType="map">
  SELECT
    <include refid="userColumns">
      <property name="alias" value="t1"/>
    </include>,
    <include refid="userColumns">
      <property name="alias" value="t2"/>
    </include>
  FROM some_table t1 CROSS JOIN some_table t2
</select>

property标签的属性值可以直接用在include元素包含的sql标签的内部语句中,也可以被用在include元素的refid属性里,例如:

<sql id="sometable">
  ${prefix}Table
</sql>

<sql id="someinclude">
  FROM <include refid="${include_target}"/>
</sql>

<select id="select" resultType="map">
  SELECT field1, field2, field3
  <include refid="someinclude">
    <property name="prefix" value="Some"/>
    <property name="include_target" value="sometable"/>
  </include>
</select>

平时工作中不需要像上面示例那样嵌套好几层,这里写一个常用的简单示例,查询字段和查询条件封装到sql片段中,所有的查询语句都能复用:

<sql id="basicColumns">
  id,
  username,
  password,
  email,
  bio
</sql>
<sql id="conditionWhere">
  <where>
    <if test="id != null">
      AND id = #{id}
    </if>
    <if test="username != null and username != ''">
      AND username = #{username}
    </if>
  </where>
</sql>
<select id="findById" resultType="author">
  SELECT
  <include refid="basicColumns" />
  FROM Author
  <include refid="conditionWhere" />
</select>

3. 参数使用

3.1. 参数传递

3.1.1. 单个简单类型参数

可以接受基本类型以及对应的包装类型,集合类型的值。这种情况 MyBatis 可直接使用这个参数,不需要经过任何处理。

User selectUsers(Long id);
<select id="selectUsers" resultType="User">
  SELECT id, username, password FROM users WHERE id = #{id}
</select>

基本类型的话直接使用#{},变量名可以随意,一般直接写接口中的形式参数变量名即可。如果使用${},变量名必须为value,也就是必须写为${value}

集合类型的话一般用在foreach标签中,和接口中的形参变量名也无关,collection里需要对应着写。List类型参数就写listSet类型参数就写collection;数组类型参数就写array

源码在org.apache.ibatis.session.defaults.DefaultSqlSession中:

private Object wrapCollection(final Object object) {
    if (object instanceof Collection) {
      StrictMap<Object> map = new StrictMap<Object>();
      map.put("collection", object);
      if (object instanceof List) {
        map.put("list", object);
      }
      return map;
    } else if (object != null && object.getClass().isArray()) {
      StrictMap<Object> map = new StrictMap<Object>();
      map.put("array", object);
      return map;
    }
    return object;
}

3.1.2. 单个 JavaBean 参数

传入一个对象的情况,User类型的参数对象传递到了语句中,idusernamepassword属性将会被查找,然后将它们的值传入预处理语句的参数中。

int insertUser(User user);

和接口形参中变量名称无关,获取的变量名都是JavaBean中属性名。

<insert id="insertUser">
  INSERT INTO users (id, username, password) 
  VALUES(#{id}, #{username}, #{password})
</insert>

3.1.3. 单个 Map 参数

可以封装多个参数为map,直接传递。

int insertUser(Map<String, Object> map);

和接口形参中变量名称无关,获取的变量名都是Map中的key名。

<insert id="insertUser">
  INSERT INTO users (id, username, password) 
  VALUES(#{id}, #{userName}, #{password})
</insert>

3.1.4. 多个参数

任意多个参数,都会被 MyBatis 重新包装成一个Map传入。

int insertUser(Long id, String username, String password);

和接口形参中变量名称无关,获取的变量名是按照参数顺序取的#{param1}, #{param2}...,或者#{0}, #{1}...

<insert id="insertUser">
  INSERT INTO users (id, username, password) 
  VALUES(#{0}, #{1}, #{2})
</insert>

3.1.5. 注解参数(推荐使用)

使用@Param注解为接口的形参起一个名字(一般名字都和形参变量名相同),MyBatis 就会将这些参数封装进Map中,key就是我们自己指定的名字。该注解全限定类名为org.apache.ibatis.annotations.Param;

int insertUser(@Param("id")Long id, @Param("username")String username, @Param("password")String password);
<insert id="insertUser">
  INSERT INTO users (id, username, password) 
  VALUES(#{id}, #{userName}, #{password})
</insert>

3.2. 参数属性

下面介绍几种参数的属性设置,但大多时候只须简单地指定属性名,其他的事情 MyBatis 会自己去推断,顶多要为可能为NULL的列指定jdbcType而已。

3.2.1. javaType

#{property, javaType=int, jdbcType=NUMERIC}

javaType通常可以根据参数对象的类型确定下来,所以一般可以省略。除非该参数对象是一个HashMap。如果所使用的参数是个HashMap,需要显式指定 javaType来确保 MyBatis 会使用正确的类型处理器(TypeHandler)。

3.2.2. jdbcType

#{property, javaType=int, jdbcType=NUMERIC}

如果一个列允许null值,并且会传递值为null的参数,就必须要指定 jdbcType

3.2.3. numericScale

#{height, javaType=double, jdbcType=NUMERIC, numericScale=2}

对于数值类型,有一个小数保留位数的设置属性numericScale,用来指定小数点后保留的位数。

3.2.4. typeHandler

由于 Java 代码中的String类型对应的默认typeHandlerStringTypeHandler,当用String类型处理时,如果String长度超过一定长度,就会报如下错误:

setString can only process strings of less than 32766 chararacters

解决办法是指定该属性的typeHandler,如下:

 #{message, typeHandler=org.apache.ibatis.type.ClobTypeHandler}

我们也可以自定义typeHandler来处理需要的数据。

3.2.5. mode

这个属性在写存储过程中会用到。所以一般我们不需要配置。

mode属性允许你指定INOUTINOUT参数。如果参数的modeOUTINOUT,就像你在指定输出参数时所期望的行为那样,参数对象的属性实际值将会被改变。

如果modeOUT(或INOUT),而且jdbcTypeCURSOR(也就是 Oracle 的REFCURSOR),你必须指定一个resultMap引用来将结果集ResultMap映射到参数的类型上。要注意这里的javaType属性是可选的,如果留空并且jdbcTypeCURSOR,它会被自动地被设为 ResultMap

#{department, mode=OUT, jdbcType=CURSOR, javaType=ResultSet, resultMap=departmentResultMap}

MyBatis 也支持很多高级的数据类型,比如结构体(structs),但是当使用OUT参数时,你必须显式设置类型的名称jdbcTypeName

#{middleInitial, mode=OUT, jdbcType=STRUCT, jdbcTypeName=MY_TYPE, resultMap=departmentResultMap}

3.3. 参数获取

3.3.1. ${} 和 #{}

默认情况下,使用#{}格式的语法会导致 MyBatis 创建PreparedStatement参数占位符并安全地设置参数(就像使用?一样)。 有时需要直接在SQL语句中插入一个不转义的字符串。 比如,像ORDER BY,你可以这样来使用:

ORDER BY ${columnName}

${}常用于表名字段名的替换。

@Select("SELECT * FROM user WHERE ${column} = #{value}")
User findByColumn(@Param("column") String column, @Param("value") String value);

其中${column}会被直接替换,而#{value}会被使用?预处理。

${}方式接受用户的输入,并将其用于语句中的参数是不安全的,会导致潜在的SQL注入攻击,因此要么不允许用户输入这些字段,要么自行转义并检验。

3.3.2. LIKE 写法

使用${}实现:

<select id="selectPerson" resultType="hashmap">
  SELECT * FROM Person WHERE name LIKE '%${name}%'
</select>

这种写法可能会受到SQL注入的攻击,尽量避免使用。

使用#{}实现:

<select id="selectPerson" resultType="hashmap">
  SELECT * FROM Person WHERE name LIKE "%"#{name}"%"
</select>

因为#{}解析成SQL语句时候,会在变量外侧自动加单引号'',所以这里%需要使用双引号"",不能使用单引号'',不然会查不到任何结果。

使用CONCAT()实现:

<select id="selectPerson" resultType="hashmap">
  SELECT * FROM Person WHERE name LIKE CONCAT('%', #{name}, '%')
</select>

这是 SQL 中自带的一个字符串拼接函数,直接使用即可。