Mybatis模块
简介
什么是MyBatis?
MyBatis是一款优秀的持久层框架,他支持自定义 SQL、存储过程以及高级映射。MyBatis免除了几乎所有的JDBC代码以及设置参数和获取结果集的工作。
MyBatis可以通过简单的 XML 或者注解来配置和映射原始类型、接口和 Java POJO(普通老式 Java 对象)为数据库中的记录。
入门
导入相关依赖
要使用 MyBatis, 只需将 mybatis-x.x.x.jar 文件置于类路径(classpath)中即可。
如果是 Maven 工程,导入下面的依赖
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>x.x.x</version>
</dependency>
从 XML 中构建 SqlSessionFactory
每个基于 MyBatis 的应用都是以一个 SqlSessionFactory 的实例为核心的。SqlSessionFactory 的实例可以通过 SqlSessionFactoryBuilder 获得。而 SqlSessionFactoryBuilder 则可以从 XML 配置文件或一个预先配置的 Configuration 实例来构建出 SqlSessionFactory 实例。
? 从 XML 文件中构建 SqlSessionFactory 的实例非常简单,建议使用类路径下的资源文件进行配置。 但也可以使用任意的输入流(InputStream)实例,比如用文件路径字符串或 file:// URL 构造的输入流。MyBatis 包含一个名叫 Resources 的工具类,它包含一些实用方法,使得从类路径或其它位置加载资源文件更加容易。
String resource = "org/mybatis/example/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
MyBatis核心配置文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="org/mybatis/example/BlogMapper.xml"/>
</mappers>
</configuration>
这里只给出数据源配置,详细配置在后边总结。
从 SqlSessionFactory 中获取 SqlSession
try (SqlSession session = sqlSessionFactory.openSession()) {
BlogMapper mapper = session.getMapper(BlogMapper.class);
Blog blog = mapper.selectBlog(101);
}
映射文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.mybatis.example.BlogMapper">
<select id="selectBlog" resultType="Blog">
select * from Blog where id = #{id}
</select>
</mapper>
命名空间的作用:
利用更长的全限定名来将不同的语句隔离开来。
实现接口绑定。
对于像 BlogMapper 这样的映射器类来说,还有另一种方法来完成语句映射。他们映射的语句可以不用 XML 来配置,而可以使用 java 注解来配置。比如上面的 XML 就可以写成如下的配置:
package org.mybatis.example;
public interface BlogMapper {
@Select("SELECT * FROM blog WHERE id = #{id}")
Blog selectBlog(int id);
}
使用注解来映射简单语句会使代码显得更加简洁,但对于稍微复杂一点的语句,Java 注解不仅力不从心,还会让本就复杂的 SQL 语句更加混乱不堪。 因此,如果你需要做一些很复杂的操作,最好用 XML 来映射语句。
XML 配置文件
1、properties :
引用外部配置文件,并可以进行动态替换。
如在 db.properties 文件中定义了如下的数据源信息
driver=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/2019Home
username=root
password=root
设置好的属性可以在整个配置文件中用来替换需要动态配置的属性值。
<properties resource="db.properties"></properties>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
2、settings :
这是 MyBatis 中极为重要的调整设置,它们会改变 MyBatis 的运行时行为。下表描述了设置中各项设置的含义、默认值等。
一个配置完整的 settings 元素的示例如下:
<settings>
<setting name="cacheEnabled" value="true"/>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="true"/>
<setting name="multipleResultSetsEnabled" value="true"/>
<setting name="useColumnLabel" value="true"/>
<setting name="useGeneratedKeys" value="false"/>
<setting name="autoMappingBehavior" value="PARTIAL"/>
<setting name="autoMappingUnknownColumnBehavior" value="WARNING"/>
<setting name="defaultExecutorType" value="SIMPLE"/>
<setting name="defaultStatementTimeout" value="25"/>
<setting name="defaultFetchSize" value="100"/>
<setting name="safeRowBoundsEnabled" value="false"/>
<setting name="safeResultHandlerEnabled" value="true"/>
<setting name="mapUnderscoreToCamelCase" value="false"/>
<setting name="localCacheScope" value="SESSION"/>
<setting name="jdbcTypeForNull" value="OTHER"/>
<setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/>
<setting name="defaultScriptingLanguage" value="org.apache.ibatis.scripting.xmltags.XMLLanguageDriver"/>
<setting name="defaultEnumTypeHandler" value="org.apache.ibatis.type.EnumTypeHandler"/>
<setting name="callSettersOnNulls" value="false"/>
<setting name="returnInstanceForEmptyRow" value="false"/>
<setting name="logPrefix" value="exampleLogPreFix_"/>
<setting name="logImpl" value="SLF4J | LOG4J | LOG4J2 | JDK_LOGGING | COMMONS_LOGGING | STDOUT_LOGGING | NO_LOGGING"/>
<setting name="proxyFactory" value="CGLIB | JAVASSIST"/>
<setting name="vfsImpl" value="org.mybatis.example.YourselfVfsImpl"/>
<setting name="useActualParamName" value="true"/>
<setting name="configurationFactory" value="org.mybatis.example.ConfigurationFactory"/>
</settings>
3、类型别名(typeAliases):
类型别名可为 Java 类型设置一个缩写名字。它仅用于 XML 配置,意在降低冗余的全限定类名书写。例如:
<typeAliases>
<typeAlias alias="Author" type="domain.blog.Author"/>
<typeAlias alias="Blog" type="domain.blog.Blog"/>
<typeAlias alias="Comment" type="domain.blog.Comment"/>
</typeAliases>
也可以指定一个包名,MyBatis 会在包名下面搜索需要的 Java Bean,比如:
<typeAliases>
<package name="domain.blog"/>
</typeAliases>
推荐使用全类名。
4、类型处理器(typeHandlers):
MyBatis 在设置预处理语句(PreparedStatement)中的参数或从结果集中取出一个值时,都会用类型处理器将获取到的值以合适的方式转换成Java类型。下面是一些默认处理器:
你可以重写已有的类型处理器或创建自己的类型处理器来处理不支持的或非标准的类型。具体做法为:实现org.apache.ibatis.type.TypeHandler 接口,或继承一个很便利的类 org.apache.ibatis.type.BaseTypeHandler ,并且可以(可选地)将他映射到一个 JDBC 类型。比如:
@MappedJdbcTypes(JdbcType.VARCHAR)
public class ExampleTypeHandler extends BaseTypeHandler<String> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
ps.setString(i, parameter);
}
@Override
public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
return rs.getString(columnName);
}
@Override
public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return rs.getString(columnIndex);
}
@Override
public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
return cs.getString(columnIndex);
}
}
<typeHandlers>
<typeHandler handler="org.mybatis.example.ExampleTypeHandler"/>
</typeHandlers>
使用上述的类型处理器将会覆盖已有的处理 Java String 类型的属性以及 VARCHAR 类型的参数和结果的类型处理器。要注意 MyBatis 不会通过检测数据库元信息来决定使用哪种类型,所以你必须在参数和结果映射中指明字段是 VARCHAR 类型,以使其能够绑定到正确的类型处理器上。这是因为 MyBatis 直到语句被执行时才清楚数据类型。
通过类型处理器的泛型,MyBatis 可以得知该类型处理器处理的 Java 类型,不过这种行为可以通过两种方法改变:
- 在类型处理器的配置元素(typeHandler 元素)上增加一个
javaType 属性(比如:javaType="String" ); - 在类型处理器的类上增加一个
@MappedTypes 注解指定与其关联的 Java 类型列表。如果在 javaType 属性中也同时指定,则注解上的配置将被忽略。
可以通过两种方式来指定关联的 JDBC 类型:
- 在类型处理器的配置元素上增加一个
jdbcType 属性(比如:jdbcType="VARCHAR" ); - 在类型处理器的类上增加一个
@MappedJdbcTypes 注解指定与其关联的 JDBC 类型列表。如果在 jdbcType 属性中也同时指定,则注解上的配置将被忽略。
当在 ResultMap 中决定使用哪种类型处理器时,此时 Java 类型是已知的(从结果类型中获得),但是 JDBC 类型是未知的。因此 Mybatis 使用 javaType=[Java 类型], jdbcType=null 的组合来选择一个类型处理器。 这意味着使用 @MappedJdbcTypes 注解可以限制类型处理器的作用范围,并且可以确保,除非显式地设置,否则类型处理器在 ResultMap 中将不会生效。 如果希望能在 ResultMap 中隐式地使用类型处理器,那么设置 @MappedJdbcTypes 注解的 includeNullJdbcType=true 即可。 然而从 Mybatis 3.4.0 开始,如果某个 Java 类型只有一个注册的类型处理器,即使没有设置 includeNullJdbcType=true ,那么这个类型处理器也会是 ResultMap 使用 Java 类型时的默认处理器。
最后,可以让 MyBatis 帮你查找类型处理器:
<typeHandlers>
<package name="org.mybatis.example"/>
</typeHandlers>
注意在使用自动发现功能的时候,只能通过注解方式来指定 JDBC 的类型。
你可以创建能够处理多个类的泛型类型处理器。为了使用泛型类型处理器,需要增加一个接受该类的 class 作为参数的构造器,这样 MyBatis 会在构造一个类型处理器实例的时候传入一个具体的类。
public class GenericTypeHandler<E extends MyObject> extends BaseTypeHandler<E> {
private Class<E> type;
public GenericTypeHandler(Class<E> type) {
if (type == null) throw new IllegalArgumentException("Type argument cannot be null");
this.type = type;
}
...
EnumTypeHandler 和 EnumOrdinalTypeHandler 都是泛型类型处理器,我们将会在接下来的部分详细探讨。
5、处理枚举类型
若想映射枚举类型 Enum ,则需要从 EnumTypeHandler 或者 EnumOrdinalTypeHandler 中选择一个来使用。
比如说我们想存储取近似值时用到的舍入模式。默认情况下,MyBatis 会利用 EnumTypeHandler 来把 Enum 值转换成对应的名字。
注意 EnumTypeHandler 在某种意义上来说是比较特别的,其它的处理器只针对某个特定的类,而它不同,它会处理任意继承了 Enum 的类。
不过,我们可能不想存储名字,相反我们的 DBA 会坚持使用整形值代码。那也一样简单:在配置文件中把 EnumOrdinalTypeHandler 加到 typeHandlers 中即可,这样每个 RoundingMode 将通过他们的序数值来映射成对应的整形数值。
<typeHandlers>
<typeHandler handler="org.apache.ibatis.type.EnumOrdinalTypeHandler" javaType="java.math.RoundingMode"/>
</typeHandlers>
但要是你想在一个地方将 Enum 映射成字符串,在另外一个地方映射成整形值呢?
自动映射器(auto-mapper)会自动地选用 EnumOrdinalTypeHandler 来处理枚举类型,所以如果我们想用普通的 EnumTypeHandler ,就必须要显式地为那些 SQL 语句设置要使用的类型处理器。
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.apache.ibatis.submitted.rounding.Mapper">
<resultMap type="org.apache.ibatis.submitted.rounding.User" id="usermap">
<id column="id" property="id"/>
<result column="name" property="name"/>
<result column="funkyNumber" property="funkyNumber"/>
<result column="roundingMode" property="roundingMode"/>
</resultMap>
<select id="getUser" resultMap="usermap">
select * from users
</select>
<insert id="insert">
insert into users (id, name, funkyNumber, roundingMode) values (
#{id}, #{name}, #{funkyNumber}, #{roundingMode}
)
</insert>
<resultMap type="org.apache.ibatis.submitted.rounding.User" id="usermap2">
<id column="id" property="id"/>
<result column="name" property="name"/>
<result column="funkyNumber" property="funkyNumber"/>
<result column="roundingMode" property="roundingMode" typeHandler="org.apache.ibatis.type.EnumTypeHandler"/>
</resultMap>
<select id="getUser2" resultMap="usermap2">
select * from users2
</select>
<insert id="insert2">
insert into users2 (id, name, funkyNumber, roundingMode) values (
#{id}, #{name}, #{funkyNumber}, #{roundingMode, typeHandler=org.apache.ibatis.type.EnumTypeHandler}
)
</insert>
</mapper>
注意,这里的 select 语句必须指定 resultMap 而不是 resultType 。
6、插件(plugins)
MyBatis 允许你在映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:
- Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
- ParameterHandler (getParameterObject, setParameters)
- ResultSetHandler (handleResultSets, handleOutputParameters)
- StatementHandler (prepare, parameterize, batch, update, query)
这些类中方法的细节可以通过查看每个方法的签名来发现,或者直接查看 MyBatis 发行包中的源代码。如果你想做的不仅仅是监控方法的调用,那么你最好相当了解要重写的方法的行为。因为在试图修改或重写已有方法的行为时,很可能会破坏 MyBatis 的核心模块。 这些都是更底层的类和方法,所以使用插件的时候要特别当心。
通过 MyBatis 提供的强大机制,使用插件是非常简单的,只需实现 Interceptor 接口,并指定想要拦截的方法签名即可。
@Intercepts({@Signature(
type= Executor.class,
method = "update",
args = {MappedStatement.class,Object.class})})
public class ExamplePlugin implements Interceptor {
private Properties properties = new Properties();
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object returnObject = invocation.proceed();
return returnObject;
}
@Override
public void setProperties(Properties properties) {
this.properties = properties;
}
}
<plugins>
<plugin interceptor="org.mybatis.example.ExamplePlugin">
<property name="someProperty" value="100"/>
</plugin>
</plugins>
上面的插件将会拦截在 Executor 实例中所有的 “update” 方法调用,这里的 Executor 是负责执行底层映射语句的内部对象。
**覆盖配置类**
除了用插件来修改 MyBatis 核心行为以外,还可以通过完全覆盖配置类来达到目的。只需继承配置类后覆盖其中的某个方法,再把它传递到 SqlSessionFactoryBuilder.build(myConfig) 方法即可。再次重申,这可能会极大影响 MyBatis 的行为,务请慎之又慎。
7、环境配置(environments):
MyBatis 可以配置成适应多种环境,这种机制有助于将 SQL 映射应用于多种数据库之中,现实情况下有多种理由需要这么做。例如,开发、测试和生产环境需要有不同的配置;或者想在具有相同 Schema 的多个生产数据库中使用相同的 SQL 映射。还有许多类似的使用场景。
不过要记住:尽管可以配置多个环境,但每个 SqlSessionFactory 实例只能选择一种环境。
所以,如果你想连接两个数据库,就需要创建两个 SqlSessionFactory 实例,每个数据库对应一个。而如果是三个数据库,就需要三个实例,依此类推,记起来很简单:每个数据库对应一个 SqlSessionFactory 实例
environments 元素定义了如何配置环境。
<environments default="development">
<environment id="development">
<transactionManager type="JDBC">
<property name="..." value="..."/>
</transactionManager>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
注意一些关键点:
- 默认使用的环境 ID(比如:default=“development”)。
- 每个 environment 元素定义的环境 ID(比如:id=“development”)。
- 事务管理器的配置(比如:type=“JDBC”)。
- 数据源的配置(比如:type=“POOLED”)。
这里了解一下即可,因为事务管理及数据源都是 Spring 做的事情。这里具体可以参考官方文档
8、数据库厂商标识(databaseIdProvider)
MyBatis 可以根据不同的数据库厂商执行不同的语句,这种多厂商的支持是基于映射语句中的 databaseId 属性。MyBatis 会加载带有匹配当前数据库 databaseId 属性和所有不带 databaseId 属性的语句。如果同时找到带有 databaseId 和不带 databaseId 的相同语句,则后者会被舍弃。为支持多厂商特性,只要像下面这样在 mybatis-config.xml 文件中加入 databaseIdProvider 即可:
<databaseIdProvider type="DB_VENDOR" />
databaseIdProvider 对应的 DB_VENDOR 实现会将 databaseId 设置为 DatabaseMetaData#getDatabaseProductName() 返回的字符串。由于通常情况下这些字符串都非常长,而且相同产品的不同版本会返回不同的值,你可能想通过设置属性别名来使其变短:
<databaseIdProvider type="DB_VENDOR">
<property name="SQL Server" value="sqlserver"/>
<property name="DB2" value="db2"/>
<property name="Oracle" value="oracle" />
</databaseIdProvider>
在提供了属性别名时,databaseIdProvider 的 DB_VENDOR 实现会将 databaseId 设置为数据库产品名与属性中的名称第一个相匹配的值,如果没有匹配的属性,将会设置为 “null”。在这个例子中,如果 getDatabaseProductName() 返回“Oracle (DataDirect)”,databaseId 将被设置为“oracle”。
你可以通过实现接口 org.apache.ibatis.mapping.DatabaseIdProvider 并在 mybatis-config.xml 中注册来构建自己的 DatabaseIdProvider:
public interface DatabaseIdProvider {
default void setProperties(Properties p) {
}
String getDatabaseId(DataSource dataSource) throws SQLException;
}
9、映射器(mappers)
既然 MyBatis 的行为已经由上述元素配置完了,我们现在就要来定义 SQL 映射语句了。但首先,我们需要告诉 MyBatis 到哪里去找到这些语句。在自动查找资源方面,Java 并没有提供一个很好的解决方案,所以最好的办法是直接告诉 MyBatis 到哪里去找映射文件 。你可以使用相对于类路径的资源引用,或完全限定资源定位符(包括 file:/// 形式的 URL),或类名和包名等。例如:
<mappers>
<mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
<mapper resource="org/mybatis/builder/BlogMapper.xml"/>
<mapper resource="org/mybatis/builder/PostMapper.xml"/>
</mappers>
<mappers>
<mapper url="file:///var/mappers/AuthorMapper.xml"/>
<mapper url="file:///var/mappers/BlogMapper.xml"/>
<mapper url="file:///var/mappers/PostMapper.xml"/>
</mappers>
<mappers>
<mapper class="org.mybatis.builder.AuthorMapper"/>
<mapper class="org.mybatis.builder.BlogMapper"/>
<mapper class="org.mybatis.builder.PostMapper"/>
</mappers>
<mappers>
<package name="org.mybatis.builder"/>
</mappers>
这九个标签,1 、2 、9掌握,其它了解即可,用的时候看官方文档。
XML 映射文件
SQL 映射文件只有很少的几个顶级元素(按照应被定义的顺序列出):
cache - 该命名空间的缓存配置cache-ref - 引用其他命名空间的缓存配置resultMap - 描述如何从数据库结果集中加载对象,是最复杂也是最强大的元素sql - 可被其他语句引用的可重用语句块insert - 映射插入语句update - 映射更新语句delete - 映射删除语句select - 映射查询语句
每个元素的细节:
select
<select id="selectPerson" parameterType="int" resultType="hashmap">
SELECT * FROM PERSON WHERE ID = #{id}
</select>
这个语句名为 selectPerson,接受一个 int(或 Integer)类型的参数,并返回一个 HashMap 类型的对象,其中的键是列名,值便是结果行中的对应值。
注意参数符号:
这就告诉 MyBatis 创建一个预处理语句(PreparedStatement)参数,在 JDBC 中,这样的一个参数在 SQL 中会由一个“?”来标识,并被传递到一个新的预处理语句中,就像这样:
String selectPerson = "SELECT * FROM PERSON WHERE ID=?";
PreparedStatement ps = conn.prepareStatement(selectPerson);
ps.setInt(1,id);
? Select 元素的属性
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VhI5dICd-1665831936606)(https://images-1313675740.cos.ap-shanghai.myqcloud.com/images/%E6%97%A0%E6%A0%87%E9%A2%98.png)]
insert, update 和 delete
<insert
id="insertAuthor"
parameterType="domain.blog.Author"
flushCache="true"
statementType="PREPARED"
keyProperty=""
keyColumn=""
useGeneratedKeys=""
timeout="20">
<update
id="updateAuthor"
parameterType="domain.blog.Author"
flushCache="true"
statementType="PREPARED"
timeout="20">
<delete
id="deleteAuthor"
parameterType="domain.blog.Author"
flushCache="true"
statementType="PREPARED"
timeout="20">
? Insert, Update, Delete 元素的属性
下面是 insert,update 和 delete 语句的示例:
<insert id="insertAuthor">
insert into Author (id,username,password,email,bio)
values (#{id},#{username},#{password},#{email},#{bio})
</insert>
<update id="updateAuthor">
update Author set
username = #{username},
password = #{password},
email = #{email},
bio = #{bio}
where id = #{id}
</update>
<delete id="deleteAuthor">
delete from Author where id = #{id}
</delete>
如前所述,插入语句的配置规则更加丰富,在插入语句里面有一些额外的属性和子元素用来处理主键的生成,并且提供了多种生成方式。
首先,如果你的数据库支持自动生成的主键的字段(比如 MySQL 和 SQL Server ),那么你可以设置useGeneratedKeys=“true”,然后在把 keyProperty 设置为目标属性就 OK 了。例如,如果上面的 Author 表已经在 id 列上使用了自动生成,那么语句可以修改为:
<insert id="insertAuthor" useGeneratedKeys="true"
keyProperty="id">
insert into Author (username,password,email,bio)
values (#{username},#{password},#{email},#{bio})
</insert>
如果你的数据库还支持多行插入, 你也可以传入一个 Author 数组或集合,并返回自动生成的主键。
<insert id="insertAuthor" 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>
对于不支持自动生成主键列的数据库和可能不支持自动生成主键的 JDBC 驱动,MyBatis 有另外一种方法来生成主键。
这里有一个简单(也很傻)的示例,它可以生成一个随机 ID(不建议实际使用,这里只是为了展示 MyBatis 处理问题的灵活性和宽容度):
<insert id="insertAuthor">
<selectKey keyProperty="id" resultType="int" order="BEFORE">
select CAST(RANDOM()*1000000 as INTEGER) a from SYSIBM.SYSDUMMY1
</selectKey>
insert into Author
(id, username, password, email,bio, favourite_section)
values
(#{id}, #{username}, #{password}, #{email}, #{bio}, #{favouriteSection,jdbcType=VARCHAR})
</insert>
在上面的示例中,首先会运行 selectKey 元素中的语句,并设置 Author 的 id,然后才会调用插入语句。这样就实现了数据库自动生成主键类似的行为,同时保持了 Java 代码的简洁。
selectKey 元素描述如下:
<selectKey
keyProperty="id"
resultType="int"
order="BEFORE"
statementType="PREPARED">
? selectKey 元素的属性
参数
之前见到的所有语句都是用了简单的参数形式。但实际上,参数是 MyBatis 非常强大的元素。对于大多数简单的使用场景,你都不需要使用复杂的参数,比如:
<select id="selectUsers" resultType="User">
select id, username, password
from users
where id = #{id}
</select>
上面的这个示例说明了一个非常简单的命名参数映射。鉴于参数类型(parameterType)会被自动设置为int ,这个参数可以随意命名。原始类型或简单数据类型(比如Integer 和 String )因为没有其他属性,会用它们的值来作为参数。然而,如果传入一个复杂的对象,行为就会有点不一样了。比如:
<insert id="insertUser" parameterType="User">
insert into users (id, username, password)
values (#{id}, #{username}, #{password})
</insert>
如果 User 类型的参数对象传递到了语句中,会查找id、username 和 password 属性,然后将它们的值传入预处理语句的参数中。
:这里可能报错
org.apache.ibatis.binding.BindingException: Parameter 'id' not found. Available parameters are [arg2, arg1, arg0, param3, param1, param2]
原因是:传入多个参数,Mybatis会自动将这些参数封装在一个 map 中,key 就是参数的索引或参数的第几个标识。#{key} 就是从这个 map 中取值。
解决:只需在接口中传入参数是给每个参数加上注解 @Param 告诉 MyBatis 使用我们自定义的 key,如:
对传递语句参数来说,这种方式真是干脆利落。不过参数映射的功能远不止于此。
首先,和 MyBatis 的其它部分一样,参数也可以指定一个特殊的数据类型。
#{property,javaType=int,jdbcType=NUMERIC}
和 MyBatis 的其他部分一样,几乎总是可以根据参数对象的类型确定javaType,除非该对象是一个 HashMap 。这个时候,你需要显示指定 javaType 来确保正确的类型处理器(TypeHandler)被使用。
要更进一步地自定义类型处理方式,可以指定一个特殊的类型处理器类(或别名),比如:
#{age,javaType=int,jdbcType=NUMERIC,typeHandler=MyTypeHandler}
对于数值类型,还可以设置 numericScale 指定小数点后保留的位数。
#{height,javaType=double,jdbcType=NUMERIC,numericScale=2}
尽管上边这些选项很强大,但大多时候,你只须简单指定属性名,顶多要为可能为空的列指定 jdbcType ,其他的事情交给 MyBatis 自己去推断就行了。
#{firstName}
#{middleInitial,jdbcType=VARCHAR}
#{lastName}
字符串替换
默认情况下,使用 ${} 参数语法时,MyBatis 会创建 PreparedStatement 参数占位符,并通过占位符安全地设置参数(就像使用 ?一样)。这样做更安全,更迅速,通常也是首选做法,不过有时你就是想直接在 SQL 语句中直接插入一个不转义的字符串。比如 ORDED BY 子句,这时候你可以:
ORDER BY ${columnName}
这样,MyBatis 就不会修改或转义该字符串了。
当 SQL 语句中的元数据(如表名或列名)是动态生成的时候,字符串替换将会非常有用。举个例子,如果你想 select 一个表任意一列的数据时,不需要这样写:
@Select("select * from user where id = #{id}")
User findById(@Param("id") long id);
@Select("select * from user where name = #{name}")
User findByName(@Param("name") String name);
@Select("select * from user where email = #{email}")
User findByEmail(@Param("email") String email);
而是可以只写这样一个方法:
@Select("select * from user where ${column} = #{value}")
User findByColumn(@Param("column") String column, @Param("value") String value);
其中 ${column} 会被直接替换,而 #{value} 会使用 ? 预处理。这样,就能完成同样的任务:
User userOfId1 = userMapper.findByColumn("id", 1L);
User userOfNameKid = userMapper.findByColumn("name", "kid");
User userOfEmail = userMapper.findByColumn("email", "noone@nowhere.com");
这种方式也同样适用于替换表名的情况。
用这种方式接受用户的输入,并用作语句参数是不安全的,会导致潜在的 SQL 注入攻击。因此,要么不允许用户输入这些字段,要么自行转义并检验这些参数。
查询结果的返回类型
查询结果返回 List :
返回的是集合,resultType 的值写的是集合中元素的类型
返回类型为 Map :
返回结果是 Map ,resultType的值写 Map。查询出的结果 列名作为 key ,值作为value 。
如果是查询多条记录且封装在 Map 中,映射文件中的 resultMap 依然写集合中的类型,并且需要使用 @MapKey 注解指定哪个列名作为key。
结果映射
ResultMap 的设计思想是,对简单的语句做到零配置,对于复杂一点的语句,只需要描述语句之间的关系就行了。
之前你已经见过简单映射语句的示例,它们没有显式指定 resultMap 。比如:
<select id="selectUsers" resultType="map">
select id, username, hashedPassword
from some_table
where id = #{id}
</select>
上述语句只是简单地将所有的列映射到 HashMap 的键上,这由 resultType 属性指定。虽然在大部分情况下都够用,但是 HashMap 并不是一个很好的领域模型。你的程序更可能会使用 JavaBean 或 POJO(Plain Old Java Objects,普通老式 Java 对象)作为领域模型。MyBatis 对两者都提供了支持。看看下面这个 JavaBean:
package com.someapp.model;
public class User {
private int id;
private String username;
private String hashedPassword;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getHashedPassword() {
return hashedPassword;
}
public void setHashedPassword(String hashedPassword) {
this.hashedPassword = hashedPassword;
}
}
基于 JavaBean 的规范,上面这个类有 3 个属性:id,username 和 hashedPassword。这些属性会对应到 select 语句中的列名。
这样的一个 JavaBean 可以被映射到 ResultSet ,就像映射到 HashMap 一样简单。
<select id="selectUsers" resultType="com.someapp.model.User">
select id, username, hashedPassword
from some_table
where id = #{id}
</select>
类型别名是你的好帮手。使用它们,你就可以不用输入类的全限定名了。比如:
<typeAlias type="com.someapp.model.User" alias="User"/>
<select id="selectUsers" resultType="User">
select id, username, hashedPassword
from some_table
where id = #{id}
</select>
在这些情况下,MyBatis 会在幕后自动创建一个 ResultMap ,再根据属性名来映射列到 JavaBean 的属性上。==如果列名和属性名不能匹配上,可以在 SELECT 语句中设置列别名(这是一个基本的 SQL 特性)来完成匹配。==比如:
<select id="selectUsers" resultType="User">
select
user_id as "id",
user_name as "userName",
hashed_password as "hashedPassword"
from some_table
where id = #{id}
</select>
在学习了上面的知识后,你会发现上面的例子没有一个需要显式配置 ResultMap ,这就是 ResultMap 的优秀之处——你完全可以不用显式地配置它们。虽然上面的例子不用显式配置 ResultMap 。但为了讲解,我们来看看如果在刚刚的示例中,显式使用外部的 resultMap 会怎样,这也是解决列名不匹配的另外一种方式。
<resultMap id="userResultMap" type="User">
<id property="id" column="user_id" />
<result property="username" column="user_name"/>
<result property="password" column="hashed_password"/>
</resultMap>
然后在引用它的语句中设置 resultMap 属性就行了(注意我们去掉了 resultType 属性)。比如:
<select id="selectUsers" resultMap="userResultMap">
select user_id, user_name, hashed_password
from some_table
where id = #{id}
</select>
高级结果映射
MyBatis 创建时的一个思想是:数据库不可能永远是你所想或所需的那个样子。我们希望每个数据库都具备第三范式或 BCNF 范式,可惜它们并不都是那样。如果能有一种数据库映射模式,完美适配所有的应用程序,那就太好了,但可惜也没有。而 ResultMap 就是 MyBatis 对这个问题的答案。
比如,我们如何映射下面这个语句?
<select id="selectBlogDetails" resultMap="detailedBlogResultMap">
select
B.id as blog_id,
B.title as blog_title,
B.author_id as blog_author_id,
A.id as author_id,
A.username as author_username,
A.password as author_password,
A.email as author_email,
A.bio as author_bio,
A.favourite_section as author_favourite_section,
P.id as post_id,
P.blog_id as post_blog_id,
P.author_id as post_author_id,
P.created_on as post_created_on,
P.section as post_section,
P.subject as post_subject,
P.draft as draft,
P.body as post_body,
C.id as comment_id,
C.post_id as comment_post_id,
C.name as comment_name,
C.comment as comment_text,
T.id as tag_id,
T.name as tag_name
from Blog B
left outer join Author A on B.author_id = A.id
left outer join Post P on B.id = P.blog_id
left outer join Comment C on P.id = C.post_id
left outer join Post_Tag PT on PT.post_id = P.id
left outer join Tag T on PT.tag_id = T.id
where B.id = #{id}
</select>
你可能想把它映射到一个智能的对象模型,这个对象表示了一篇博客,它由某位作者所写,有很多的博文,每篇博文有零或多条的评论和标签。我们先来看看下面这个完整的例子,它是一个非常复杂的结果映射(假设作者,博客,博文,评论和标签都是类型别名)。不用紧张,我们会一步一步地来说明。虽然它看起来令人望而生畏,但其实非常简单。
<resultMap id="detailedBlogResultMap" type="Blog">
<constructor>
<idArg column="blog_id" javaType="int"/>
</constructor>
<result property="title" column="blog_title"/>
<association property="author" javaType="Author">
<id property="id" column="author_id"/>
<result property="username" column="author_username"/>
<result property="password" column="author_password"/>
<result property="email" column="author_email"/>
<result property="bio" column="author_bio"/>
<result property="favouriteSection" column="author_favourite_section"/>
</association>
<collection property="posts" ofType="Post">
<id property="id" column="post_id"/>
<result property="subject" column="post_subject"/>
<association property="author" javaType="Author"/>
<collection property="comments" ofType="Comment">
<id property="id" column="comment_id"/>
</collection>
<collection property="tags" ofType="Tag" >
<id property="id" column="tag_id"/>
</collection>
<discriminator javaType="int" column="draft">
<case value="1" resultType="DraftPost"/>
</discriminator>
</collection>
</resultMap>
resultMap 元素有很多子元素和一个值得深入探讨的结构。下面是resultMap 元素的概念视图。
结果映射(resultMap)
-
constructor - 用于在实例化类时,注入结果到构造方法中
idArg - ID 参数;标记出作为 ID 的结果可以帮助提高整体性能arg - 将被注入到构造方法的一个普通结果 -
id - 一个 ID 结果;标记出作为 ID 的结果可以帮助提高整体性能 -
result - 注入到字段或 JavaBean 属性的普通结果 -
association - 一个复杂类型的关联;许多结果将包装成这种类型
- 嵌套结果映射 - 关联可以是
resultMap 元素,或是对其他结果映射的引用 -
collection - 一个复杂类型的集合
- 嵌套结果映射 - 集合可以是
resultMap 元素,或是对其他结果映射的应用 -
discriminator - 使用结果值来决定使用哪个 resultMap
下一部分将详细说明每个元素。
id & result
<id property="id" column="post_id"/>
<result property="subject" column="post_subject"/>
这些元素是结果映射的基础。id 和 result 元素都将一个列的值映射到一个简单数据类型(String,int,double,Date等)的属性或字段。
这两者之间的唯一不同是,id 元素对应的属性会被标记为对象的标识符,在比较对象实例时使用。这样可以提高整体的性能,尤其是进行缓存和嵌套结果映射(也就是连接嵌套)的时候。
两个元素都有一些属性:
? Id 和 Result 的属性
关联
<association property="author" column="blog_author_id" javaType="Author">
<id property="id" column="author_id"/>
<result property="username" column="author_username"/>
</association>
关联(association)元素处理“有一个”类型的关系。比如,在我们的示例中,一个博客有一个用户。关联结果映射和其它类型的映射工作方式差不多。你需要指定目标属性名以及属性的javaType (很多时候 MyBatis 可以自己推断出来),在必要的情况下你还可以设置 JDBC 类型,如果你想覆盖获取结果值的过程,还可以设置类型处理器。
关联的不同之处是,你需要告诉 MyBatis 如何加载关联。MyBatis 有两种不同的方式加载关联:
- 嵌套 Select 查询:通过执行另外一个 SQL 映射语句来加载期望的复杂类型。
- 嵌套结果映射:使用嵌套的结果映射来处理连接结果的重复子集。
首先,先让我们来看看这个元素的属性。你将会发现,和普通的结果映射相比,它只在 select 和 resultMap 属性上有所不同。
关联的嵌套 Select 查询
示例:
<select id="selectBlog" resultMap="blogResult">
SELECT * FROM BLOG WHERE ID = #{id}
</select>
<resultMap id="blogResult" type="Blog">
<association property="author" column="author_id" javaType="Author" select="selectAuthor"/>
</resultMap>
<select id="selectAuthor" resultType="Author">
SELECT * FROM AUTHOR WHERE ID = #{id}
</select>
关联查询有时候 SQL 可能会比较复杂,所有 MyBatis 考虑到了这一点。所以,我们可以把关联查询分成多步。如上示例,有两个 select 查询:一个用来加载博客(Blog),另外一个用来加载作者(Author),而且博客的结果映射描述了应该使用 selectAuthor 语句加载他的 author 属性。
其他的所有属性将会被自动加载,只要它们的列名和属性名相匹配。
这种方式随简单,但在大星数据集或大型数据表上表现不佳。这个问题被称为“N+1”查询问题。概括地讲,N+1 查询问题是这样子的:
- 你执行了一个单独的 SQL 语句来获取结果的一个列表(就是“+1”)。
- 对列表返回的每条记录,你执行一个 select 查询语句来为每条记录加载详细信息(就是“N”)。
这个问题会导致成百上千的 SQL 语句被执行。有时候,我们不希望产生这样的后果。
好消息是,MyBatis 能够对这样的查询进行延迟加载,因此可以将大量语句同时运行的开销分散开来。然而,如果你加载记录列表之后立刻就遍历列表以获取嵌套的数据,就会触发所有的延迟加载查询,性能可能会变得很糟糕。
关联的嵌套结果映射
下面的例子则是一个非常简单的例子,用于演示嵌套结果映射如何工作。 现在我们将博客表和作者表连接在一起,而不是执行一个独立的查询语句,就像这样:
<select id="selectBlog" resultMap="blogResult">
select
B.id as blog_id,
B.title as blog_title,
B.author_id as blog_author_id,
A.id as author_id,
A.username as author_username,
A.password as author_password,
A.email as author_email,
A.bio as author_bio
from Blog B left outer join Author A on B.author_id = A.id
where B.id = #{id}
</select>
注意查询中的连接,以及为确保结果能够拥有唯一且清晰的名字,我们设置的别名。这使得进行映射非常简单。现在我们可以映射这个结果:
<resultMap id="blogResult" type="Blog">
<id property="id" column="blog_id" />
<result property="title" column="blog_title"/>
<association property="author" column="blog_author_id" javaType="Author" resultMap="authorResult"/>
</resultMap>
<resultMap id="authorResult" type="Author">
<id property="id" column="author_id"/>
<result property="username" column="author_username"/>
<result property="password" column="author_password"/>
<result property="email" column="author_email"/>
<result property="bio" column="author_bio"/>
</resultMap>
在上面的例子中,你可以看到,博客(Blog)作者(author)的关联元素委托名为 “authorResult” 的结果映射来加载作者对象的实例。
现在,上面的示例使用了外部的结果映射元素来映射关联。这使得 Author 的结果映射可以被重用。然而,如果你不打算重用它,或者你更喜欢将你所有的结果映射放在一个具有描述性的结果映射元素中。你可以直接将结果映射作为子元素嵌套在内。这里给出使用这种方式的等效例子:
<resultMap id="blogResult" type="Blog">
<id property="id" column="blog_id" />
<result property="title" column="blog_title"/>
<association property="author" javaType="Author">
<id property="id" column="author_id"/>
<result property="username" column="author_username"/>
<result property="password" column="author_password"/>
<result property="email" column="author_email"/>
<result property="bio" column="author_bio"/>
</association>
</resultMap>
那如果博客(blog)有一个共同作者(co-author)该怎么办?select 语句看起来会是这样的:
<select id="selectBlog" resultMap="blogResult">
select
B.id as blog_id,
B.title as blog_title,
A.id as author_id,
A.username as author_username,
A.password as author_password,
A.email as author_email,
A.bio as author_bio,
CA.id as co_author_id,
CA.username as co_author_username,
CA.password as co_author_password,
CA.email as co_author_email,
CA.bio as co_author_bio
from Blog B
left outer join Author A on B.author_id = A.id
left outer join Author CA on B.co_author_id = CA.id
where B.id = #{id}
</select>
回忆一下,Author 的结果映射定义如下:
resultMap id="authorResult" type="Author">
<id property="id" column="author_id"/>
<result property="username" column="author_username"/>
<result property="password" column="author_password"/>
<result property="email" column="author_email"/>
<result property="bio" column="author_bio"/>
</resultMap>
由于结果中的列名与结果映射中的列名不同。你需要指定 columnPrefix 以便重复使用该结果映射来映射 co-author 的结果。
<resultMap id="blogResult" type="Blog">
<id property="id" column="blog_id" />
<result property="title" column="blog_title"/>
<association property="author"
resultMap="authorResult" />
<association property="coAuthor"
resultMap="authorResult"
columnPrefix="co_" />
</resultMap>
关联的多结果集(ResultSet)
从版本 3.2.3 开始,MyBatis 提供了另一种解决 N+1 查询问题的方法。
某些数据库允许存储过程返回多个结果集,或一次性执行多个语句,每个语句返回一个结果集。 我们可以利用这个特性,在不使用连接的情况下,只访问数据库一次就能获得相关数据。
在例子中,存储过程执行下面的查询并返回两个结果集。第一个结果集会返回博客(Blog)的结果,第二个则返回作者(Author)的结果。
SELECT * FROM BLOG WHERE ID = #{id}
SELECT * FROM AUTHOR WHERE ID = #{id}
在映射语句中,必须通过 resultSets 属性为每个结果集指定一个名字,多个名字使用逗号隔开。
<select id="selectBlog" resultSets="blogs,authors" resultMap="blogResult" statementType="CALLABLE">
{call getBlogsAndAuthors(#{id,jdbcType=INTEGER,mode=IN})}
</select>
现在我们可以指定使用 “authors” 结果集的数据来填充 “author” 关联:
<resultMap id="blogResult" type="Blog">
<id property="id" column="id" />
<result property="title" column="title"/>
<association property="author" javaType="Author" resultSet="authors" column="author_id" foreignColumn="id">
<id property="id" column="id"/>
<result property="username" column="username"/>
<result property="password" column="password"/>
<result property="email" column="email"/>
<result property="bio" column="bio"/>
</association>
</resultMap>
你已经在上面看到了如何处理“有一个”类型的关联。但是该怎么处理“有很多个”类型的关联呢?这就是我们接下来要介绍的。
集合
<collection property="posts" ofType="domain.blog.Post">
<id property="id" column="post_id"/>
<result property="subject" column="post_subject"/>
<result property="body" column="post_body"/>
</collection>
集合元素和关联元素几乎是一样的,它们相似的程度之高,以致于没有必要再介绍集合元素的相似部分。 所以让我们来关注它们的不同之处吧。
我们来继续上面的示例,一个博客(Blog)只有一个作者(Author)。但一个博客有很多文章(Post)。在博客类中,这可以用下面的写法来表示:
private List<Post> posts;
要像上面这样,映射嵌套结果集合到一个 List 中,可以使用集合元素。和关联元素一样,我们可以使用嵌套 Select 查询,或基于连接的嵌套结果映射集合。
集合的嵌套 Select 查询
首先,让我们看看如何使用嵌套 Select 查询来为博客加载文章。
<select id="selectBlog" resultMap="blogResult">
SELECT * FROM BLOG WHERE ID = #{id}
</select>
<resultMap id="blogResult" type="Blog">
<collection property="posts" javaType="ArrayList" column="id" ofType="Post" select="selectPostsForBlog"/>
</resultMap>
<select id="selectPostsForBlog" resultType="Post">
SELECT * FROM POST WHERE BLOG_ID = #{id}
</select>
首先,你会注意到我们使用的是集合元素。接下来你会注意到有一个新的 “ofType” 属性。这个属性非常重要,它用来将 JavaBean(或字段)属性的类型和集合存储的类型区分开来。所以你可以按照下面这样来阅读映射:
<collection property="posts" javaType="ArrayList" column="id" ofType="Post" select="selectPostsForBlog"/>
读作: “posts 是一个存储 Post 的 ArrayList 集合”
在一般情况下,MyBatis 可以推断 javaType 属性,因此并不需要填写。所以很多时候你可以简略成:
<collection property="posts" column="id" ofType="Post" select="selectPostsForBlog"/>
集合的嵌套结果映射
现在你可能已经猜到了集合的嵌套结果映射是怎样工作的——除了新增的 “ofType” 属性,它和关联的完全相同。
首先, 让我们看看对应的 SQL 语句:
<select id="selectBlog" resultMap="blogResult">
select
B.id as blog_id,
B.title as blog_title,
B.author_id as blog_author_id,
P.id as post_id,
P.subject as post_subject,
P.body as post_body,
from Blog B
left outer join Post P on B.id = P.blog_id
where B.id = #{id}
</select>
我们再次连接了博客表和文章表,并且为每一列都赋予了一个有意义的别名,以便映射保持简单。要映射博客里面的文章集合,就这么简单:
<resultMap id="blogResult" type="Blog">
<id property="id" column="blog_id" />
<result property="title" column="blog_title"/>
<collection property="posts" ofType="Post">
<id property="id" column="post_id"/>
<result property="subject" column="post_subject"/>
<result property="body" column="post_body"/>
</collection>
</resultMap>
补充:
一对一关系,1-1 外键放在哪端都行
一对多关系,1-n 外键放在多的那一端,也可以独立出一张表
多对多关系, n-n 外键也就是两张表的关系的表示,必须通过建立新表来表示,因为只有两个表的主键组合起来,才能唯一的标识一条记录
缓存
MyBatis 内置了一个强大的事务性查询缓存机制,它可以非常方便地配置和定制。为了使它更加强大而且易于配置,我们对 MyBatis 3 中的缓存实现进行了许多改进。
默认情况下,只启用了本地的会话缓存(也叫一级缓存、SqlSession级别的缓存),它仅仅对一个会话中的数据进行缓存。
一级缓存
Reader reader = Resources.getResourceAsReader("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
SqlSession session = sqlSessionFactory.openSession();
PersonMapper mapper1 = session.getMapper(PersonMapper.class);
Person person1 = mapper1.queryPersonById(1);
System.out.println(person1);
PersonMapper mapper2 = session.getMapper(PersonMapper.class);
Person person2 = mapper2.queryPersonById(1);
System.out.println(person2);
可以看到,两次查询同一条数据只发送了一次 SQL 语句命令。
一级缓存失效的情况:
1、不同的 SqlSession ,使用不同的一级缓存。
Reader reader = Resources.getResourceAsReader("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
SqlSession session1 = sqlSessionFactory.openSession();
PersonMapper mapper1 = session1.getMapper(PersonMapper.class);
Person person1 = mapper1.queryPersonById(1);
System.out.println(person1);
System.out.println("==========================================");
SqlSession session2 = sqlSessionFactory.openSession();
PersonMapper mapper2 = session2.getMapper(PersonMapper.class);
Person person2 = mapper2.queryPersonById(1);
System.out.println(person2);
可以明显看到,查询同一条数据确发送了两个 SQL 语句,原因是什么呢?
一级缓存是 SqlSession 级别的缓存,只有在同一个sqlSession期间查询到的数据会保存在这个sqlSession的缓存中。下次使用这个sqlSession查询会从缓存中拿。
2、查询条件的值不同
Reader reader = Resources.getResourceAsReader("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
SqlSession session1 = sqlSessionFactory.openSession();
PersonMapper mapper1 = session1.getMapper(PersonMapper.class);
Person person1 = mapper1.queryPersonById(1);
System.out.println(person1);
System.out.println("==========================================");
PersonMapper mapper2 = session1.getMapper(PersonMapper.class);
Person person2 = mapper2.queryPersonById(2);
System.out.println(person2);
结果如图,依然发送两个 SQL 语句。
3、在同一个 sqlSession 期间执行任何一次增删改操作,增删改操作会把缓存清空。
Reader reader = Resources.getResourceAsReader("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
SqlSession session1 = sqlSessionFactory.openSession();
PersonMapper mapper1 = session1.getMapper(PersonMapper.class);
Person person1 = mapper1.queryPersonById(1);
System.out.println(person1);
System.out.println("==========================================");
mapper1.updatePerson(1);
PersonMapper mapper2 = session1.getMapper(PersonMapper.class);
Person person2 = mapper2.queryPersonById(1);
System.out.println(person2);
二级缓存
要启用全局的二级缓存,只需要在你的 SQL 映射文件中添加一行:
<cache/>
基本上就是这样。这个简单语句的效果如下:
- 映射语句文件中的所有 select 语句的结果将会被缓存。
- 映射语句文件中的所有 insert、update 和 delete 语句会刷新缓存。
- 缓存会使用最近最少使用算法(LRU, Least Recently Used)算法来清除不需要的缓存。
- 缓存不会定时进行刷新(也就是说,没有刷新间隔)。
- 缓存会保存列表或对象(无论查询方法返回哪种)的 1024 个引用。
- 缓存会被视为读/写缓存,这意味着获取到的对象并不是共享的,可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。
缓存只作用于 cache 标签所在的映射文件中的语句。
这些属性可以通过 cache 元素的属性来修改。比如:
<cache
eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true"/>
这个更高级的配置创建了一个 FIFO 缓存,每隔 60 秒刷新,最多可以存储结果对象或列表的 512 个引用,而且返回的对象被认为是只读的,因此对它们进行修改可能会在不同线程中的调用者产生冲突。
可用的清除策略有:
LRU – 最近最少使用:移除最长时间不被使用的对象。FIFO – 先进先出:按对象进入缓存的顺序来移除它们。SOFT – 软引用:基于垃圾回收器状态和软引用规则移除对象。WEAK – 弱引用:更积极地基于垃圾收集器状态和弱引用规则移除对象。
flushInterval(刷新间隔)属性可以被设置为任意的正整数,设置的值应该是一个以毫秒为单位的合理时间量。 默认情况是不设置,也就是没有刷新间隔,缓存仅仅会在调用语句时刷新。
size(引用数目)属性可以被设置为任意正整数,要注意欲缓存对象的大小和运行环境中可用的内存资源。默认值是 1024。
readOnly(只读)属性可以被设置为 true 或 false。只读的缓存会给所有调用者返回缓存对象的相同实例。 因此这些对象不能被修改。这就提供了可观的性能提升。而可读写的缓存会(通过序列化)返回缓存对象的拷贝。速度上会慢一些,但是更安全,因此默认值是 false。
这里对缓存的配置(如清除策略、可读或可读写等),不能应用于自定义缓存。
二级缓存是事务性的。这意味着,当 SqlSession 完成并提交时,或是完成并回滚,但没有执行 flushCache=true 的 insert/delete/update 语句时,缓存会获得更新。
请注意,缓存的配置和缓存实例会被绑定到 SQL 映射文件的命名空间中。因此,同一命名空间中的所有语句和缓存将通过命名空间绑定在一起。每条语句可以自定义与缓存交互的方式,或将它们完全排除于缓存之外,这可以通过在每条语句上使用两个简单属性来达成。默认情况下,语句会这样来配置:
<select ... flushCache="false" useCache="true"/>
<insert ... flushCache="true"/>
<update ... flushCache="true"/>
<delete ... flushCache="true"/>
鉴于这是默认行为,显然你永远不应该以这样的方式显式配置一条语句。但如果你想改变默认的行为,只需要设置 flushCache 和 useCache 属性。比如,某些情况下你可能希望特定 select 语句的结果排除于缓存之外,或希望一条 select 语句清空缓存。类似地,你可能希望某些 update 语句执行时不要刷新缓存。
使用自定义缓存
思想:
自己写一个缓存类去实现 org.apache.ibatis.cache.Cache 接口。然后在要使用缓存的映射文件中的标签的type属性使用自定义的缓存类
<cache type="com.domain.something.MyCustomCache"/>
就可以使用了。
这里我们整合 Ehcache 三方缓存:
首先,导入相关 Jar 包:
ehcache-core-x.x.x.jar
mybatis-ehcache-x.x.x.jar
slf4j-api-x.x.x.jar
log4j-x.x.x.jar
由于 mybatis-ehcache 这个 jar包 ,Ehcache 这个类已经实现了 Cache 接口
所以直接使用就可以了。
Ehcache.xml 配置文件
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
updateCheck="false">
<diskStore path="E:\Ehcache"/>
<defaultCache
maxElementsInMemory="1000"
maxElementsOnDisk="1000000"
eternal="false"
overflowToDisk="false"
timeToIdleSeconds="100"
timeToLiveSeconds="100"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU"
></defaultCache>
</ehcache>
然后在要使用缓存的映射文件中使用
<cache type="org.mybatis.caches.ehcache.EhcacheCache">
</cache>
对某一命名空间的语句,只会使用该命名空间的缓存进行缓存或刷新。但你可能会想要在多个命名空间中共享相同的缓存配置和实例。要实现这种需求,你可以使用 cache-ref 元素来引用另一个缓存。
<cache-ref namespace="共享缓存的SQL映射文件的名称空间"/>
总结:
1、不会出现一级缓存和二级缓存中有同一个数据
? 二级缓存中,一级缓存关闭了就有了
? 一级缓存中,二级缓存中没有此数据,就会看一级缓存,一级缓存没有就去查数据库,数据库的查询结果放在一级缓存中。关闭sqlSession会把一级缓存的数据放在二级缓存中。
2、任何时候都是先看二级缓存、再看一级缓存,如果大家都没有就去查询数据库。
动态 SQL
动态 SQL 是 MyBatis 的强大特性之一。如果你使用过 JDBC 或其它类似的框架,你应该能理解根据不同条件拼接 SQL 语句有多痛苦,例如拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号。利用动态 SQL,可以彻底摆脱这种痛苦。
使用动态 SQL 并非一件易事,但借助可用于任何 SQL 映射语句中的强大的动态 SQL 语言,MyBatis 显著地提升了这一特性的易用性。
如果你之前用过 JSTL 或任何基于类 XML 语言的文本处理器,你对动态 SQL 元素可能会感觉似曾相识。在MyBatis 之前的版本中,需要花时间了解大量的元素。借助功能强大的基于 OGNL 的表达式,MyBatis 3 替换了之前的大部分元素,大大精简了元素种类,现在要学习的元素种类比原来的一半还要少。
- if
- choose (when, otherwise)
- trim (where, set)
- foreach
if
使用动态 SQL 最常见情景是根据条件包含 where 子句的一部分。比如:
<select id="findActiveBlogWithTitleLike"
resultType="Blog">
SELECT * FROM BLOG
WHERE state = ‘ACTIVE’
<if test="title != null">
AND title like #{title}
</if>
</select>
这条语句提供了可选的查找文本功能。如果不传入 “title”,那么所有处于 “ACTIVE” 状态的 BLOG 都会返回;如果传入了 “title” 参数,那么就会对 “title” 一列进行模糊查找并返回对应的 BLOG 结果(细心的读者可能会发现,“title” 的参数值需要包含查找掩码或通配符字符)。
如果希望通过 “title” 和 “author” 两个参数进行可选搜索该怎么办呢?首先,我想先将语句名称修改成更名副其实的名称;接下来,只需要加入另一个条件即可。
<select id="findActiveBlogLike"
resultType="Blog">
SELECT * FROM BLOG WHERE state = ‘ACTIVE’
<if test="title != null">
AND title like #{title}
</if>
<if test="author != null and author.name != null">
AND author_name like #{author.name}
</if>
</select>
test :判断条件
trim、where、set
这次我们将 “state = ‘ACTIVE’” 设置成动态条件,看看会发生什么。
<select id="findActiveBlogLike"
resultType="Blog">
SELECT * FROM BLOG
WHERE
<if test="state != null">
state = #{state}
</if>
<if test="title != null">
AND title like #{title}
</if>
<if test="author != null and author.name != null">
AND author_name like #{author.name}
</if>
</select>
如果没有匹配的条件会怎么样?最终这条 SQL 会变成这样:
SELECT * FROM BLOG WHERE
或者说不满足第一个条件,那么 SQL 又会编程这样:
SELECT * FROM BLOG WHERE AND title like #{title} AND author_name like #{author.name}
这样都会查询失败,这类问题不能简单地用条件元素来解决。这个问题是如此的难以解决,以至于解决过的人不会再想碰到这种问题。
MyBatis 有一个简单且适合大多数场景的解决办法。而在其他场景中,可以对其进行自定义以符合需求。而这,只需要一处简单的改动:
<select id="findActiveBlogLike"
resultType="Blog">
SELECT * FROM BLOG
<where>
<if test="state != null">
state = #{state}
</if>
<if test="title != null">
AND title like #{title}
</if>
<if test="author != null and author.name != null">
AND author_name like #{author.name}
</if>
</where>
</select>
where 元素只会在子元素返回任何内容的情况下才插入 “WHERE” 子句。而且,若子句的开头为 “AND” 或 “OR”,where 元素也会将它们去除。
如果 where 元素与你期望的不太一样,你也可以通过自定义 trim 元素来定制 where 元素的功能。比如,和 where 元素等价的自定义 trim 元素为:
<trim prefix="" prefixOverrides="" suffix="" suffixOverrides=""></trim>
prefix :前缀,为 SQL 整体添加一个前缀
prefixOverrides :去除整体字符串前面多余的字符
suffix :为整体添加一个后缀
suffixOverrides :去除后面多余的字符
<select id="findActiveBlogLike"
resultType="Blog">
SELECT * FROM BLOG
<trim prefix="where" prefixOverrides="and" suffixOverrides="and">
<if test="state != null">
state = #{state} AND
</if>
<if test="title != null">
title like #{title} AND
</if>
<if test="author != null and author.name != null">
author_name like #{author.name} AND
</if>
</trim>
</select>
用于动态更新语句的类似解决方案叫做 set。set 元素可以用于动态包含需要更新的列,忽略其他不更新的列。比如:
<update id="updateAuthorIfNecessary">
update Author
<set>
<if test="username != null">
username=#{username},
</if>
<if test="password != null">
password=#{password},
</if>
<if test="email != null">
email=#{email},
</if>
<if test="bio != null">
bio=#{bio}
</if>
</set>
where id=#{id}
</update>
这个例子中,set 元素会动态地在行首插入 SET 关键字,并会删掉额外的都好(这些逗号是在使用条件语句给列赋值时引入的)。或者,你可以通过使用 trim 元素来达到同样的效果:
<trim prefix="SET" suffixOverrides=",">
...
</trim>
注意,我们覆盖了后缀值设置,并且自定义了前缀值。
foreach
动态 SQL 的另一个常见使用场景是对集合进行遍历(尤其是在构建 IN 条件语句的时候)。比如:
<select id="selectPostIn" resultType="domain.blog.Post">
SELECT *
FROM POST P
<where>
<foreach item="item" index="index" collection="list"
open="ID in (" separator="," close=")" nullable="true">
#{item}
</foreach>
</where>
</select>
你可以将任何可迭代对象(如 List、Set 等)、Map 对象或者数组对象作为集合参数传递给 foreach。
当使用可迭代对象或者数组时,index 是当前迭代的序号,item 的值是本次迭代获取到的元素。当使用 Map 对象(或者 Map.Entry 对象的集合)时,index 是键,item 是值。
collection : 指定集合遍历的 key。如果集合是 List 类型就写 list,当然也可指定,不过这是要在接口方法参数处使用 @Param 注解指定。
choose、when、otherwise
**有时候,我们不想使用所有的条件,而只是想从多个条件中选择一个使用。**针对这种情况,MyBatis 提供了 choose 元素,他有点想 Java 中的 switch 语句。
<select id="findActiveBlogLike"
resultType="Blog">
SELECT * FROM BLOG WHERE state = ‘ACTIVE’
<choose>
<when test="title != null">
AND title like #{title}
</when>
<when test="author != null and author.name != null">
AND author_name like #{author.name}
</when>
<otherwise>
AND featured = 1
</otherwise>
</choose>
</select>
script
要在带注解的映射器接口类中使用动态 SQL,可以使用 script 元素。比如:
@Update({"<script>",
"update Author",
" <set>",
" <if test='username != null'>username=#{username},</if>",
" <if test='password != null'>password=#{password},</if>",
" <if test='email != null'>email=#{email},</if>",
" <if test='bio != null'>bio=#{bio}</if>",
" </set>",
"where id=#{id}",
"</script>"})
void updateAuthorValues(Author author);
bind
bind 元素允许你在 OGNL 表达式以外创建一个变量,并将其绑定到当前的上下文。比如:
<select id="selectBlogsLike" resultType="Blog">
<bind name="pattern" value="'%' + _parameter.getTitle() + '%'" />
SELECT * FROM BLOG
WHERE title LIKE #{pattern}
</select>
_parameter :代表传入来的参数
- 传入了单个参数:_parameter 就代表这个参数
- 传入了多个参数:_parameter 就代表多个参数集合起来的 map
多数据库支持
如果配置了 databaseIdProvider,你就可以在动态代码中使用名为 “_databaseId” 的变量来为不同的数据库构建特定的语句。比如下面的例子:
<insert id="insert">
<selectKey keyProperty="id" resultType="int" order="BEFORE">
<if test="_databaseId == 'oracle'">
select seq_users.nextval from dual
</if>
<if test="_databaseId == 'db2'">
select nextval for seq_users from sysibm.sysdummy1"
</if>
</selectKey>
insert into users values (#{id}, #{name})
</insert>
include 、sql
这两个标签要一起使用,作用是抽取重复的 SQL 来复用。
<sql id="ObjectArrayStudents">
<where>
<if test="array !=null and array.length>0 ">
<foreach collection="array" open=" and stuno in (" close=")" item="student" separator=",">
#{student.stuNo}
</foreach>
</if>
</where>
</sql>
<select id="queryStudentsWithObjectArray" parameterType="Object[]" resultType="student">
select * from student
<include refid="ObjectArrayStudents"></include>
</select>
补充:
xml 中的特殊符号,如“,<,>,等这些都需要使用转义字符。
详细看:w3shool api文档中 HTML ISO-8859-1 参考手册
OGNL:
对象图导航语言,是一种非常强大的表达式语言。通过它可以非常方便的来操作对象属性。类似于EL、SpEL。
- 访问对象属性 person.name
- 调用方法 person.getName()
- 调用静态属性/方法 @java.lang.Math @PI @Java.util.UUID @RandomUUID()
- 访问集合伪属性
类型 伪属性 伪属性对应的 Java 方法
List、Set、Map Size、isEmpty List/Set/Map.size(),List/Set/Map.isEmpty()
List、Set iterator List.iterator()、Set.iterator()
Map keys、values Map.keySet()、Map.values()
Iterator next、hasNext Iterator.next()、Iterator.hasNext()
配置总结
db.properties
driver=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/2019Home
username=root
password=2105251354
mybatis-config.xml全局配置:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<properties resource="db.properties"></properties>
<settings>
<setting name="logImpl" value="LOG4J"/>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"/>
<setting name="cacheEnabled" value="true"/>
</settings>
<typeAliases>
<package name="ex"/>
</typeAliases>
<typeHandlers>
<typeHandler handler="ex.ex.converter.BooleanAndIntConverter" javaType="Boolean" jdbcType="INTEGER"/>
</typeHandlers>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="ex/ex/one/studentMapper.xml"/>
<mapper resource="ex/ex/one/studentCardMapper.xml"/>
<mapper resource="ex/ex/one/studentClassMapper.xml"/>
</mappers>
</configuration>
log4j.properties
log4j.rootLogger=DEBUG, stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%5p [%t] - %m%n
ding" value=“false”/>
<!--设定单/多个别名-->
<typeAliases>
<!--设置单个别名-->
<!-- <typeAlias type="ex.Student" alias="a"></typeAlias> -->
<!--批量定义别名 (别名忽略大小写) 一下会自动将包中的所有类 批量定义别名 别名就是类名 (不带包名)-->
<package name="ex"/>
</typeAliases>
<!--类型转换器-->
<typeHandlers>
<!--转换器 将(java)javaType转换为(数据库)jdbcType-->
<typeHandler handler="ex.ex.converter.BooleanAndIntConverter" javaType="Boolean" jdbcType="INTEGER"/>
</typeHandlers>
<environments default="development">
<environment id="development">
<!--配置事物的提交方式
JDBC:利用JDBC方式处理事务(commit rollback close)
Managed:将事件交给其他组件托管(spring jobss),默认会关闭连接
-->
<transactionManager type="JDBC"/>
<!--数据类型
UNPOOLED:传统的JDBC模式(不推荐)
POOLED:使用数据库连接池
JNDI:从Tomcat中获取一个内置的数据库连接池(数据库连接池-数据源)
-->
<dataSource type="POOLED">
<!--配置数据库信息-->
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<!--加载映射文件-->
<mapper resource="ex/ex/one/studentMapper.xml"/>
<mapper resource="ex/ex/one/studentCardMapper.xml"/>
<mapper resource="ex/ex/one/studentClassMapper.xml"/>
</mappers>
```
log4j.properties
log4j.rootLogger=DEBUG, stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%5p [%t] - %m%n
|