1. SQL 语句
SQL标签的属性列表:
| 属性 | 描述 |
|---|---|
| id | 在命名空间中唯一的标识符,可以被用来引用这条语句。 |
| parameterType | 将会传入这条语句的参数类的完全限定名或别名。这个属性是可选的,因为 MyBatis 可以通过类型处理器(TypeHandler) 推断出具体传入语句的参数,默认值为未设置(unset)。该参数一般不需要写。 |
| parameterMap | 这是引用外部parameterMap的已经被废弃的方法。请使用内联参数映射和 parameterType 属性。 |
| resultType | 从这条语句中返回的期望类型的类的完全限定名或别名。 注意如果返回的是集合,那应该设置为集合包含的类型,而不是集合本身。可以使用resultType或resultMap,但不能同时使用。 |
| resultMap | 外部resultMap的命名引用。结果集的映射是 MyBatis 最强大的特性,如果你对其理解透彻,许多复杂映射的情形都能迎刃而解。可以使用resultMap或resultType,但不能同时使用。 |
| flushCache | 将其设置为true后,只要语句被调用,都会导致本地缓存和二级缓存被清空,默认值:false。 |
| useCache | 将其设置为true后,将会导致本条语句的结果被二级缓存缓存起来,默认值:对select元素为true。 |
| timeout | 这个设置是在抛出异常之前,驱动程序等待数据库返回请求结果的秒数。默认值为未设置(unset)(依赖驱动)。 |
| fetchSize | 这是一个给驱动的提示,尝试让驱动程序每次批量返回的结果行数和这个设置值相等。 默认值为未设置(unset)(依赖驱动)。 |
| statementType | 值为STATEMENT,PREPARED或CALLABLE中的一个。这会让 MyBatis 分别使用 Statement,PreparedStatement或CallableStatement,默认值:PREPARED。 |
| resultSetType | 值为FORWARD_ONLY,SCROLL_SENSITIVE,SCROLL_INSENSITIVE或DEFAULT(等价于unset) 中的一个,默认值为未设置(unset)(依赖驱动)。 |
| databaseId | 如果配置了数据库厂商标识(databaseIdProvider),MyBatis 会加载所有的不带 databaseId或匹配当前databaseId的语句;如果带或者不带的语句都有,则不带的会被忽略。 |
| resultOrdered | 这个设置仅针对嵌套结果select语句适用:如果为true,就是假设包含了嵌套结果集或是分组,这样的话当返回一个主结果行的时候,就不会发生有对前面结果集的引用的情况。 这就使得在获取嵌套的结果集的时候不至于导致内存不够用。默认值:false。 |
| resultSets | 这个设置仅对多结果集的情况适用。它将列出语句执行后返回的结果集并给每个结果集一个名称,名称是逗号分隔的。 |
| useGeneratedKeys | (仅对insert和update有用)这会令 MyBatis 使用JDBC的getGeneratedKeys 方法来取出由数据库内部生成的主键(比如:像MySQL和SQL Server这样的关系数据库管理系统的自动递增字段),默认值:false。 |
| keyProperty | (仅对insert和update有用)唯一标记一个属性,MyBatis 会通过getGeneratedKeys的返回值或者通过insert语句的selectKey子元素设置它的键值,默认值为未设置(unset)。如果希望得到多个生成的列,也可以是逗号分隔的属性名称列表。 |
| keyColumn | (仅对insert和update有用)通过生成的键值设置表中的列名,这个设置仅在某些数据库(像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. 返回自增主键
如果数据库支持自动生成主键(比如MySQL和SQL 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元素将会首先运行,Author的id会被设置,然后插入语句会被调用。这给你了一个和数据库中来处理自动生成的主键类似的行为,避免了使 Java 代码变得复杂。
selectKey 标签的属性:
<selectKey
keyProperty="id"
resultType="int"
order="BEFORE"
statementType="PREPARED">
| 属性 | 描述 |
|---|---|
| keyProperty | selectKey语句结果应该被设置的目标属性。如果希望得到多个生成的列,也可以是逗号分隔的属性名称列表。 |
| keyColumn | 匹配属性的返回结果集中的列名称。如果希望得到多个生成的列,也可以是逗号分隔的属性名称列表。 |
| resultType | 结果的类型。MyBatis 通常可以推断出来,但是为了更加精确,写上也不会有什么问题。MyBatis 允许将任何简单类型用作主键的类型,包括字符串。如果希望作用于多个生成的列,则可以使用一个包含期望属性的Object或一个Map。 |
| order | 这可以被设置为BEFORE或AFTER。如果设置为BEFORE,那么它会首先生成主键,设置 keyProperty然后执行插入语句。如果设置为AFTER,那么先执行插入语句,然后是selectKey中的语句 - 这和Oracle数据库的行为相似,在插入语句内部可能有嵌入索引调用。 |
| statementType | 与前面相同,MyBatis 支持STATEMENT,PREPARED和CALLABLE语句的映射类型,分别代表Statement、PreparedStatement和CallableStatement类型。 |
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类型参数就写list;Set类型参数就写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类型的参数对象传递到了语句中,id、username和password属性将会被查找,然后将它们的值传入预处理语句的参数中。
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类型对应的默认typeHandler为StringTypeHandler,当用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属性允许你指定IN,OUT或INOUT参数。如果参数的mode为OUT或INOUT,就像你在指定输出参数时所期望的行为那样,参数对象的属性实际值将会被改变。
如果mode为OUT(或INOUT),而且jdbcType为CURSOR(也就是 Oracle 的REFCURSOR),你必须指定一个resultMap引用来将结果集ResultMap映射到参数的类型上。要注意这里的javaType属性是可选的,如果留空并且jdbcType是CURSOR,它会被自动地被设为 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 中自带的一个字符串拼接函数,直接使用即可。
3.3.3. 时间参数
| Java类型 | MySQL类型 | 说明 |
|---|---|---|
| LocalDate | DATE | 仅日期,比如生日(yyyy-MM-dd) |
| LocalDateTime | DATETIME | 日期+时间(无时区),最常用(yyyy-MM-dd HH:mm:ss) |
| LocalDateTime | TIMESTAMP | 时间戳,注意时区,一般不使用 |
| Instant | TIMESTAMP | 时间戳,强时区一致性,一般不使用 |
| Date | DATETIME | 新项目不推荐使用,JDK8之前的旧项目使用 |
Java 代码
private LocalDateTime createTime;
private String createTime;
SQL 字段
`create_time` datetime NOT NULL COMMENT '创建日期'
MyBatis 字段值获取:不论是 LocalDateTime 类型还是 String 类型,都可以直接获取传递给 SQL。
create_time > #{createTime}
但是不建议传递字符串时间。当代码传递的是字符串时间时,数据库能隐式把字符串转成时间类型,SQL 可以跑,不会报错。
createTimeStr = "2026-01-24 10:00:00"
create_time > #{createTimeStr}
数据库在执行时会变成:
create_time > CAST('2026-01-24 10:00:00' AS DATETIME)
会有如下问题:
- 不同数据库行为不完全一致。
- 不同 SQL_MODE 行为可能不同。
- 容易导致索引失效
- 如果时间字符串格式错误,MySQL 不报错,可能会转为转成
0000-00-00 00:00:00,导致查询结果完全不对。 - Mapper 中的 SQL 只看到 String 类型,看不出这是“时间”。
如果非常想用,那也建议至少显式指定 jdbcType:
create_time > #{createTimeStr, jdbcType=DATETIME}
但注意:这只是告诉 MyBatis JDBC 类型,并不会帮你校验字符串是否合法,前面说的问题依然存在。