Mybatis-mappers
select
<select id="selectPerson" parameterType="int" resultType="hashmap">
SELECT * FROM PERSON WHERE ID = #{id}
</select>
类似于PreparedStatement
// Similar JDBC code, NOT MyBatis…
String selectPerson = "SELECT * FROM PERSON WHERE ID=?";
PreparedStatement ps = conn.prepareStatement(selectPerson);
ps.setInt(1,id);
这个#{id}
当然就是?
占位符了
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>
insert复杂一些,要看已存在的数据
所以insert
标签里有属性useGeneratedKeys
和keyProperty
,用来确定是否用自动生成的属性,和自动生成的属性是什么
<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>
这样会自动生成id
,只能在一些支持自动生成的数据库中使用,比如mysql
和sql server
。
一些不支持的,也可以自定义一些自动生成方法:
<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
标签由于order=before
所以会在insert
语句之前执行,keyProperty=id
所以select
得到的数据会赋值给id
并且传入到下面的insert
语句中
sql
就是一句sql语句的一部分,可以复用
<sql id="userColumns"> ${alias}.id,${alias}.username,${alias}.password </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>
在refid
这个属性中,也可以赋值一些property
,如${include_target}
parameters
就像之前的#{id}
一样,这些就是参数,这些参数在Java程序中使用时传入进去
<select id="selectUsers" resultType="User">
select id, username, password
from users
where id = #{id}
</select>
这里的parameterType
设置为int
这种原始的和简单的数据类型int/String
没有相关的属性,传入的时候会直接完全替代相应的形参。
而使用一些复杂的数据类型的时候就不一样了:
<insert id="insertUser" parameterType="User">
insert into users (id, username, password)
values (#{id}, #{username}, #{password})
</insert>
这样子会把User
类的id, username, password
属性找到,并且替代这些形参
形参也可以额外添加属性
#{property,javaType=int,jdbcType=NUMERIC}
javaType
大部分都可以直接从参数类型确定,除非这个参数类型是HashMap
,这样子需要手动添加javaType
来确保TypeHandler
是正确的。
可以自定义TypeHandler
:
#{age,javaType=int,jdbcType=NUMERIC,typeHandler=MyTypeHandler}
遇到数字类型的还可以添加小数点位置:
#{height,javaType=double,jdbcType=NUMERIC,numericScale=2}
还有个属性叫mode
,mode=[IN | OUT | INOUT]
,如果是OUT | INOUT
的话,那么参数输出的值会发生变化,这个我也不懂,估计是说javaType
会转化成jdbcType
吧。大致就是这么写的。
当mode=OUT | INOUT
而且jdbcType=CURSOR
,这时候就必须定义resultMap
来匹配resultSet
#{department, mode=OUT, jdbcType=CURSOR, javaType=ResultSet, resultMap=departmentResultMap}
mybatis还有structs
等高级等数据类型,但是必须设置jdbcTypeName
#{middleInitial, mode=OUT, jdbcType=STRUCT, jdbcTypeName=MY_TYPE, resultMap=departmentResultMap}
不过绝大多数时间不需要这么多属性,最多给一些nullable
字段设置个jdbcType
#{firstName}
#{middleInitial,jdbcType=VARCHAR}
#{lastName}
String Substitution
用${columnName}
作为形参,可以减少代码
@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");
Result Maps
一次查询,会把查询结果
result set
返回,通过resultMap
将result set
的字段一一映射到Javabean
中。
这是一个完整的resultMap
<!-- Very Complex Result Map -->
<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的各个标签
constructor
- 用来把resultMap
的返回结果插入到对应Java class
的constructor
中。idArg
- id参数arg
- 普通参数
result
- 普通的字段或者JavaBean
属性id
- id型的result
,将result
定义为id
会加快运行速度association
- 一个复杂的type
,比如这里author
显然是一个复杂的类型,他表示另外一个表/Javabean。这个标签里可以有很多个result
。- 嵌套
result mappings
- 一个association
本身就是一个resultMap
,或者可以指向一个resultMap
- 嵌套
collection
- 复杂type
的集合- 他也是嵌套
result mappings
- 他也是嵌套
discriminator
- 辨别器,可以理解为,也是一个resultMap
,但他本身在Javabean
和数据库里面的数据类型都不是引用类型,我们通过这个字段值的不同来确定不同的resultMap
case
- 是一个根据某些值的变化而变化的resultMap
- 这个
case
也是一个嵌套result mappings
- 这个
column
就是查询出来的结果的字段名,property
就是Type: Java class
中的属性,resultMap
就是将查询出来的column
一个个对应传参给property
,创建一个Javabean
实例。
resultMap的属性
id
- 这个resultMap
的id,供其他引用type
-Java class
路径或type alias
,表示对应的Java class
autoMapping
- 自动匹配字段名,该属性默认是unset
。
不要一次性建造一个像上面一样那么大的
resultMap
,这样子容易出错,循序渐进的写,并且边写边进行单元测试
id&result
<id property="id" column="post_id"/>
<result property="subject" column="post_subject"/>
这两个都是表示表格的一个字段,简单的数据类型的字段。区别就是id
可以把这个result
标记为id在对象实例中进行比较。
属性
- property - 字段名的别名,如果该别名有相同的
Javabean
属性,那么就会用这个属性;否则会在字段中去寻找对应的名字。两种情况都可以用带.
来连接的字段名,简单的如username
,复杂的如client.account.username
- column - 数据库里的字段名,或者他的昵称
- javaType - Java的类名,或昵称。如果对应的是一个
Javabean
,mybatis一般会自动解决这个参数;如果对应的是一个HashMap
,那就要明确的指定这个Javatype
- jdbcType - 只有在对
nullable
的字段进行增删改
的时候在需要指定这个参数 - typeHandler - 顾名思义呗。可以是全路径也可以是昵称。
constructor
跟Java里的constructor差不多
<constructor>
<idArg column="id" javaType="int" name="id" />
<arg column="age" javaType="_int" name="age" />
<arg column="username" javaType="String" name="username" />
</constructor>
name
使这些参数可以按照任意的顺序排序
constructor
里面的标签的属性和result
和id
差不多,有这些多出来的:
- select - 这是其他的语句的
id
,会得到复杂的type
。这个标签的column
属性得到的value
会传递给这个相应的select
语句,得到的complex type
就是这个字段了。
比如select * from author where id=#{id}
,这个#{id}
就是通过这个标签的column
属性在调用的时候被传递实参,进行select
,返回的是一个author
对象,这个author
对象就是这个标签的字段了。 - resultMap - 这是一个
resultMap
的id,用来管理嵌套的返回值。比如上面的select
就可以用他来管理返回的author
对象的格式。有了这个可以将多表查询的结果放在一个resultMap
里。 - name - 使这些参数可以按照任意的顺序排序
association
<association property="author" javaType="Author">
<id property="id" column="author_id"/>
<result property="username" column="author_username"/>
</association>
association
标签用来解决has-one
关系
可以通过以下两个方法来得到association
:
nested select
- 嵌套的select
,执行一个sql,返回需要的复杂类型type
nested results
- 嵌套的result
,通过用嵌套的result mappings
来处理重复的多表查询结果
nested select for association
参数
column
- 在这里的column
会被作为参数传入到嵌套的语句中,比如select
, 可以设置多个column
:column="{prop1=col1,prop2=col2}"
select
- 一个select
标签的ID,接收column
的实参进行操作,fetchType
- optional。lazy | eager
<resultMap id="blogResult" type="Blog">
<association property="author" column="author_id" javaType="Author" select="selectAuthor"/>
</resultMap>
<select id="selectBlog" resultMap="blogResult">
SELECT * FROM BLOG WHERE ID = #{id}
</select>
<select id="selectAuthor" resultType="Author">
SELECT * FROM AUTHOR WHERE ID = #{id}
</select>
author
这个字段是selectAuthor
的结果,其他的字段会自动导入。
问题就是会害怕
N+1 problem
由于mybatis可以lazy load
这些查询语句,所以效率还可以,但是如果我们要查询一堆数据,然后马上对这一堆数据进行遍历获取嵌套的数据的话,就要引用所有的这些lazy loads
了,就会很慢
所以有下面的方法:
nested results for association
属性:
resultMap
- 一个resultMap
的IDcolumnPrefix
- 为了防止字段名的重复,加个前缀notNullColumn
- 默认情况下,嵌套result
至少有一个字段有值的时候,才会创建这个嵌套result
。这个notNullColumn
,可以指定确定的某个字段要有值,才会创建嵌套result
。可以设置多个字段。autoMapping
- 是否将result
与这个字段自动匹配。
<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" 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>
这里的column
就是我们查询出来的结果,我们用Javabean
的每一个属性来匹配,这样子就可以得到Java object instance
了。
当然我们可以给
association
加上属性columnPrefix="author_"
,这样子就不需要在另一个resultMap
里每个标签都加上了。
当然association
可以不用在外面写resultMap
,直接在association
标签里写:
<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>
这样子就是不能复用。
multiple resultsets for association
在有些数据库(比如mysql)中,可以设置stored procedure
,一次返回多个结果集。这样子可以通过访问一次数据库,返回多个相关数据,而不需要使用join
column
- 本表所返回的结果集中,外键的字段名
foreignColumn
- 本表的外键所对应的另外那个表的主键
resultSet
- 根据resultSets
定义的名字,得到相应的结果集。resultSets
- 用来给返回的结果集定义名字。用,
分隔,不要加空格!
我们这样子定义一个stored procedure
:
CREATE DEFINER=`root`@`localhost` PROCEDURE `getUsersAndAccounts`(idx int)
BEGIN
select * from account where user_id=idx;
select * from user where id=idx;
END
以上的procedure
返回两个结果集,一个是account表的数据,一个是user表的数据。
我们通过这个select
标签调用这个procedure
,并且命名两个resultSets
<select id="selectua" resultMap="ua" statementType="CALLABLE" resultSets="accounts,users">
{call getUsersAndAccounts(#{id, jdbcType=INTEGER, mode=IN})}
</select>
上面的resultSets
表示将这两个结果集分别命名为accounts和users
,以后使用到这个select
的时候,就可以用resultSet=accounts/users
来分别得到这两个结果集了。
注意没有加空格,要是加空格了的话,引用的时候也要加空格。。。
名字无所谓,到时候resultMap
里使用的时候就是根据顺序引用的
以上的select
结果集由ua
这个resultMap
来处理
<resultMap id="ua" type="Account">
<id column="id" property="id"/>
<result column="balance" property="balance"/>
<association property="user" resultSet="users" javaType="User" column="user_id" foreignColumn="id">
<id column="id" property="id"/>
<result column="username" property="username"/>
<result column="password" property="password"/>
<result column="email" property="email"/>
</association>
</resultMap>
这里的association
用了users
这个resultSet
,在select
中定义的是第二个,所以得到的也就是第二个resultSet
column
和foreignColumn
要同时存在。这里其实不需要,因为只剩下一个字段了。
collection
用来处理has many
情况。
创建一个Group
,在Javabean
里有private List<User> users
一个group
有多个user
。
在user
里加上private int group_id
<resultMap id="gu" type="Group">
<id property="id" column="id"/>
<collection property="users" javaType="ArrayList" column="id" ofType="User" select="selectUsersFromGroup"/>
</resultMap>
这里的collection
可以这么理解:
A collection of users in an ArrayList of type User
javaType
可以省略,因为懂得都懂
这里就是select
属性所指向的select
标签:
<select id="selectUsersFromGroup" resultType="User">
select * from user where group_id=#{id}
</select>
通过这句select
,在Java中调用:
<select id="selectGroup" resultMap="gu">
select * from `group` where id=#{id}
</select>
nested results for collection
跟association
一样,多了个ofType
罢了。不写了
multiple resultSets for collection
也一样
discriminator
我就叫他 区分器
作用类似于Java里的switch
<resultMap id="vehiclers" type="Vehicle">
<id column="id" property="id"/>
<result column="speed" property="speed"/>
<result column="year" property="year"/>
<result column="make" property="make"/>
<discriminator javaType="int" column="type">
<case value="1" resultMap="car"/>
<case value="2" resultMap="truck"/>
<case value="3" resultMap="suv"/>
</discriminator>
</resultMap>
一个discriminator
标签里有column
和JavaType
属性。用来告诉mybatis去resultset
里的哪一个字段去找。
以上就是如果vehicle_type
字段如果是1,那么就以carResult
来映射这一行数据的所有
的字段,2 就truckResult
。。。
这样子的话其他的column
就不能被映射了。就像这样:
Car{color:yellow,doorCount:4}
可以用
<resultMap id="car" type="Car" extends="vehiclers">
<result column="color" property="color"/>
<result column="door_count" property="doorCount"/>
</resultMap>
这个extends
表示这个resultMap
继承了vehiclers
,那么在vehiclers
的字段也可以在car
里面映射了。
这样子的话就要在Car
这个Javabean
中也extends Vehicle
,这样子就得到了一个完整的Car
类型,为了可以在Car
里面输出所有的属性,所以Vehicle
的属性最好不是private
,这样子Car
才能够通过for (Field field : this.getClass().getFields())
来得到所有的属性(包括vehicle
和car
的),然后来重写toString
方法。然后就是这样了:
Car{color:yellow,doorCount:4,id:1,speed:120,year:2020,make:china,type:1}
auto-mapping
就是会自动把一些简单的column
映射给property
,默认是无视大小写,通过设置mapUnderscoreToCamelCase=True
也可以映射一些下划线。
auto-mapping有三个等级
NONE
- 不进行auto-mappingPARTIAL
- 对本身进行auto-mapping但是不对嵌套的resultMap
进行FULL
- 对全部都进行
默认是PARTIAL
。
最好不要设置为FULL
,因为:
<select id="selectBlog" resultMap="blogResult">
select
B.id,
B.title,
A.username,
from Blog B left outer join Author A on B.author_id = A.id
where B.id = #{id}
</select>
<resultMap id="blogResult" type="Blog">
<association property="author" resultMap="authorResult"/>
</resultMap>
<resultMap id="authorResult" type="Author">
<result property="username" column="author_username"/>
</resultMap>
在这种情况下,result set
里有id
这个column
,在authorResult
里也有个id
,所以author
的id
也会被映射
cache
默认只会缓存本地会话local session
,只要在map file
添加这一句:
<cache/>
作用是(默认的):
- 所有的
select
缓存 - 所有的
增删改
会删除缓存 - 会使用
least recently used(LRU)算法
来删掉老的 - 不会在任何类型的时间相关的日程表上删除,就是说不会每隔一段时间删除
- 会缓存1024个
list 或 object
的指针 - 缓存是读/写类型的缓存,就是说我们得到的对象不是共享的,可以被安全的修改,不会因为其他的进程或方法的调用而产生影响。
cache
只对声明了这个标签的map file
有效,如果有Java API
的话,要对相应的interface
添加注解@CacheNamespaceRef
这些属性都可以修改:
<cache
eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true"/>
这里flushInterval=60000
表示每一分钟删除一次,size=512
表示缓存最多512个,readOnly=true
表示返回的对象是只读的,这样子要修改的话会导致和其他的进程冲突。
eviction
:
LRU(默认)
- least recently used: 把好久没用的删掉FIFO
- first in first out: 先进先出SOFT
- soft reference: 根据垃圾收集器和soft reference的规则来删除WEAK
- weak reference: 更加激进的根据垃圾收集器和weak reference的规则来删除
flushInterval
:
- 默认是
unset
,就是不会根据时间来删除缓存,只根据增删改
等操作
size
:
- 默认1024
readOnly
:
- 如果是
true
,那么会返回同一个实例给调用者,所以修改的话会产生冲突,但是会快一点 - 如果是
false
,那么会返回copy给调用者,可以修改,不产生冲突,但是会慢一点
二级缓存是transactional,会话级别的。就是一个
sqlSession
被commit
的时候就更新。或者一个sqlSession
在没有执行增删改 with flushCache=true
的时候更新
using a custom cache
将自己写的或者是第三方的cache类放到这里:
<cache type="com.domain.something.MyCustomCache"/>
实现这样子的接口org.apache.ibatis.cache.Cache
:
public interface Cache {
String getId();
int getSize();
void putObject(Object key, Object value);
Object getObject(Object key);
boolean hasKey(Object key);
Object removeObject(Object key);
void clear();
}
要设置cache
,只需要把Javabean 的 public属性
添加到实现类,然后把属性传到cache
标签中:
<cache type="com.domain.something.MyCustomCache">
<property name="cacheFile" value="/tmp/my-custom-cache.tmp"/>
</cache>
也可以设置placeholder(默认值)
比如${cache.file}
来替代在configuration
里定义的属性
现在mybatis支持initialization
,会在他设置好所有的属性后执行,要实现这个接口org.apache.ibatis.builder.InitializingObject
:
public interface InitializingObject {
void initialize() throws Exception;
}
使用自定义的cache的时候,cache标签的属性设置就不能用了:eviction, flushInterval, size, readOnly
cache
的作用域是一个mapper
文件,如果要让里面的某一部分不使用对应的cache设置的话:
<select ... flushCache="false" useCache="true"/>
<insert ... flushCache="true"/>
<update ... flushCache="true"/>
<delete ... flushCache="true"/>
上面是默认设置,可以自己修改
cache-ref
我们设置cache
的时候可以不用cache
标签而用cache-ref
标签:
<cache-ref namespace="com.someone.application.data.SomeMapper"/>
这样子把另一个mapper
的cache也同时设置到了自己的mapper
中