零散点记录
入门
1、接口式编程
原生: Dao -------> DaoImpl
mybatis:Mapper --------> xxxMapper.xml
2、SqlSession代表和数据库的一次会话,用完必须关闭
3、SqlSession和Connection一样都是非线程安全,若声明为成员变量,就有可能存在资源竞争,每次使用都应该获取新的对象。
4、mapper接口没有实现类,但是mybatis会为这个接口生成一个代理对象。
- 代理对象的产生(接口和xml绑定)
session.getMapper(xxxMapper.class)
5、两个重要的配置文件:
1)mybatis的全局配置文件,包含数据库连接池信息,事务管理器信息等。
2)sql映射文件,保存了每一个sql语句的映射信息。
动态SQL
1、对于增删改,<insert>
、update
、delete
没有resultType
属性,在使用原始JDBC记性操作的时候通常是返回int类型的值,代表受结果影响的行数,但是MyBatis对于增删改支持自定义返回值类型, Integer Boolean Long void
(包括它们的基本类型)。
<insert id="insert" keyProperty="id" parameterType="Person">
insert into person(username,password) values(#{username},#{password}),(#{username},#{password});
</insert>
// 返回结果受影响的行数(Long同理)
Integer insert(Person person);
// 影响行数>0返回true,否则返回false
boolean insert(Person person);
2、数据库自增主键,对于Oracle不支持自增主键,可以通过预先生成一个id之后在进行插入。
<insert id="insertAuthor" databaseId="oracle">
<!--
keyProperty:查出的主键值封装给JavaBean的哪个属性
order="BEFORE|AFTER":当前sql在插入sql之前还是之后运行
resultType:返回值类型
-->
<selectKey keyProperty="authorId" resultType="int" order="BEFORE">
<!--Oracle 利用序列获取自增主键-->
select AUTHOR.nextval from dual
</selectKey>
insert into author(authorId,name,email)
values(#{id},#{name},#{email})
</insert>
提供了对于不支持数据库自增主键获取的一种解决方案。
3、Mybatis映射文件参数处理
1.
单个参数(除了集合):mybatis不会做特殊处理
#{参数名}:取出参数值,名字符合常规定义就行,不强制要和函参名一致。
2.
多个参数或者集合类型:mybatis会做特殊处理,将参数封装成一个map,map的
key为param1...paramN,或者参数的索引 arg0..arg1
value等于传入的参数值
#{param1 | paramN}取出第N个键为paramN的值
3.
命名参数:明确指定封装参数时map的key:@Param("id")
多个参数会封装成一个map
key:使用@Param注解指定的值
value:参数值
#{指定的key}取出对应的参数值
4.
POJO:
如果多个参数正好是我们业务逻辑的数据模型,我们就可以直接传入pojo
#{属性名}:取出传入的pojo的属性值
5.
Map:
如果多个参数不是业务模型中的数据,没有对应的pojo,为了方便,也可以传入map
#{key}
6.
TO:
如果多个参数不是业务模型中的数据,但是经常要使用,推荐来编写一个TO(Transfer Object)数据传输对象
比如分页对象
Page{
int index;
int limit;
...
}
public Employee getEmp(@Param("id")Integer id, String lastName);
取值方式:==> #{id|param1}, #{param2}
public Employee getEmp(Integer id, @Param("e")Employee emp);
取值方式:==> #{param1} #{param2.lastName|e.lastName}
/*
注意:如果是Collection(List、Set)或者数组类型,Mybatis在类型转化的时候
也会特殊处理,也是把传入的list或者数组封装在map中,
key: collection | list | array
示例如下:
*/
public Employee getEmpById(List<Integer> ids);
取值方式:#{list[0]}
Mybatis参数封装源码解析
Mapper文件
<select id="query" resultType="mybatis.po.Person">
select * from person where username=#{param1} and password=#{param2}
</select>
Dao接口
class PersonDao{
Person query(String username, String password);
}
当dao接口中的方法是多个参数时,这时Mapper文件中<select>
标签取值必须是param1,arg0
才能取到调用的方法第一个实参的值,如果用#{username},#{password}
等方式取值的话,则会抛出如下异常
Parameter 'username' not found. Available parameters are [arg1, arg0, param1, param2]
要想使用上述方法取值,可以通过在方法形参上增加@Param("username")
注解。
为什么在没有使用注解的前提下,使用方法形参名取值就会出现取不到传递的具体实参值呢?
多个参数的情况下,传递的实参值会被封装为map作为具体的值value属性,方法形参如果没有使用@Param
注解,那么最终封装的map会以param1或者arg0
做为map的键key属性,实参的值做为map具体的值,加了注解则是以@Param
注解里面的值当做键。当最后对sql语句的参数进行注入赋值时,是把#{参数名}
里面的参数名当做key去map中取值,如果方法参数没有@Param注解,那么map中的可以就是{param1,arg0...}
之类的键。
比如#{username}
,在最后对sql语句中的参数进行赋值的时候preparedStatement.setXxx()
,会将username当做键去之前封装实参的map中寻找实参值,显然这是找不到的,因为实参中没有username这个键,实参在封装的时候因为没有具体的@param注解,默认的key则会是param1,paramN之类的,当加了@param(username)
注解后,则封装实参的map会以username
为键,并且还会额外封装上param1,paramN的键,所以即使用了注解,仍然能使用#{param1...}
之类的方式取值。
源码分析:
PersonDao mapper = session.getMapper(PersonDao.class);
// session.getMapper得到PersonDao代理对象mapper
Person p = mapper.query("stronger", "123");
当执行具体的查询方法时,会被MapperProxy
的inovke方法拦截,MapperProxy
实现了InvocationHandler
接口,所以说它是基于JDK的动态代理来对方法拦截进行增强的。
public class MapperProxy<T> implements InvocationHandler, Serializable {
...
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
// method.getDeclaringClass()得到方法所在的类对象,如果该类是Object类,则放行,因为Object存在
//toString,euqals...,为了不拦截基类的方法所以放行
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else if (method.isDefault()) {
if (privateLookupInMethod == null) {
return invokeDefaultMethodJava8(proxy, method, args);
} else {
return invokeDefaultMethodJava9(proxy, method, args);
}
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
// 获得MapperMethod对象,里面封装了执行sql的必要属性,比如操作是CRU的哪一种
final MapperMethod mapperMethod = cachedMapperMethod(method);
// 调用execute方法
return mapperMethod.execute方法(sqlSession, args);
}
...
}
mapperMethod.execute方法
属于类MapperMethod
,这里主要对sql命令的类型CRUD进行判断,然后将实参值转化为sql命令的参数,就是将实参值进行了一步具体的封装。
public class MapperMethod {
...
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {
case INSERT: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
case UPDATE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
}
case DELETE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
}
case SELECT:
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
result = executeForMap(sqlSession, args);
} else if (method.returnsCursor()) {
result = executeForCursor(sqlSession, args);
} else {
//convertArgsToSqlCommandParam对实参值进一步的封装
// 调用本类下的convertArgsToSqlCommandParam
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
if (method.returnsOptional()
&& (result == null || !method.getReturnType().equals(result.getClass()))) {
result = Optional.ofNullable(result);
}
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}
public Object convertArgsToSqlCommandParam(Object[] args) {
// paramNameResolver返回封装的实参值
return paramNameResolver.getNamedParams(args);
}
...
}
ParamNameResolver
类存在一个有参的构造器,主要完成将方法形参值的封装,目的是将其作为封装map的key。这里面存在着两步转化
1)构造器中,首先会将这个方法的形参值以键值对形式存入SortedMap<Integer, String> names
中,key是Integer类型,代表形参的位置;value是方法形参值,这里的形参值并不是真实的形参值;如果形参值上标有@param注解,则形参值为注解的值,否则形参值默认为参数位置。
第一步将方法的形参值有注解则为注解的值,无注解则为默认参数位置作为value存入names中,下面注释也有说明。
{0:“username”}(有注解的情况),无注解则是 {0:“0”}
2)将names
中的值做为一个键,值为实参的值存入getNamedParams方法的param中,param就是一个map。
第二步将names中的值作为key,值为具体的实参值,存入param中。
{username:“stronger”}(有注解),无注解则是{param1:“stronger”}
源码如下:
public class ParamNameResolver {
private static final String GENERIC_NAME_PREFIX = "param";
/**
* <p>
* The key is the index and the value is the name of the parameter.<br />
* The name is obtained from {@link Param} if specified. When {@link Param} is not specified,
* the parameter index is used. Note that this index could be different from the actual index
* when the method has special parameters (i.e. {@link RowBounds} or {@link ResultHandler}).
* </p>
* <ul>
* <li>aMethod(@Param("M") int a, @Param("N") int b) -> {{0, "M"}, {1, "N"}}</li>
* <li>aMethod(int a, int b) -> {{0, "0"}, {1, "1"}}</li>
* <li>aMethod(int a, RowBounds rb, int b) -> {{0, "0"}, {2, "1"}}</li>
* </ul>
*/
private final SortedMap<Integer, String> names;
private boolean hasParamAnnotation;
public ParamNameResolver(Configuration config, Method method) {
final Class<?>[] paramTypes = method.getParameterTypes();
final Annotation[][] paramAnnotations = method.getParameterAnnotations();
final SortedMap<Integer, String> map = new TreeMap<>();
int paramCount = paramAnnotations.length;
// get names from @Param annotations
for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) {
if (isSpecialParameter(paramTypes[paramIndex])) {
// skip special parameters
continue;
}
String name = null;
for (Annotation annotation : paramAnnotations[paramIndex]) {
if (annotation instanceof Param) {
hasParamAnnotation = true;
name = ((Param) annotation).value();
break;
}
}
if (name == null) {
// @Param was not specified.
if (config.isUseActualParamName()) {
name = getActualParamName(method, paramIndex);
}
if (name == null) {
// use the parameter index as the name ("0", "1", ...)
// gcode issue #71
name = String.valueOf(map.size());
}
}
map.put(paramIndex, name);
}
names = Collections.unmodifiableSortedMap(map);
}
/**
* <p>
* A single non-special parameter is returned without a name.
* Multiple parameters are named using the naming rule.
* In addition to the default names, this method also adds the generic names (param1, param2,
* ...).
* </p>
*/
public Object getNamedParams(Object[] args) {
final int paramCount = names.size();
if (args == null || paramCount == 0) {
return null;
// 1.对于单参并且没有注解的情况,直接返回args[0],并不是存入map中取,所以
// 单参取值不依赖于具体的参数名,如#{name},name与方法形参名称相同与否没有关系。
} else if (!hasParamAnnotation && paramCount == 1) {
return args[names.firstKey()];
} else {
// 2. 多参或者存在注解(包括单参注解)
// 都是封装为map,之后注入取值的时候根据key去map中取值
final Map<String, Object> param = new ParamMap<>();
int i = 0;
// 下面就解释了为什么用username(没有加注解)的情况下,取值取不到,
// 因为并没有这个键,此时只能使用通用键值的形式取值(param1, param2, ...)。
for (Map.Entry<Integer, String> entry : names.entrySet()) {
// 将names的value属性作为key,names的键作为数组的下标去args取具体的实参值。
param.put(entry.getValue(), args[entry.getKey()]);
// add generic param names (param1, param2, ...)
// 下面还会增加一个通用的键和值 param1,param2,...
final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1);
// ensure not to overwrite parameter named with @Param
if (!names.containsValue(genericParamName)) {
param.put(genericParamName, args[entry.getKey()]);
}
i++;
}
return param;
}
}
}
字符串替换
#{}
和${}
的区别:
1、#{}
:是以预编译的形式,将参数设置到sql语句中;使用 #{} 参数语法时,MyBatis 会创建 PreparedStatement
参数占位符,并通过占位符安全地设置参数(就像使用 ? 一样)。 这样做更安全,更迅速,通常也是首选做法。
2、${}
:将取出的值直接拼接在sql语句中,存在安全问题。
建议:
-
一般情况下,使用
#{}
能有效防止sql注入 -
对于想要直接在SQL语句中插入一个不转义的字符串,比如动态地查询更改表名,字段排序等。
order by ${columnName}
Mapper文件
<select id="query" resultType="mybatis.po.Person">
select * from person where username=#{param1} and password=#{param2}
</select>
Dao接口
class PersonDao{
Person query(String username, String password);
}
当dao接口中的方法是多个参数时,这时Mapper文件中<select>
标签取值必须是param1,arg0
才能取到调用的方法第一个实参的值,如果用#{username},#{password}
等方式取值的话,则会抛出如下异常
Parameter 'username' not found. Available parameters are [arg1, arg0, param1, param2]
要想使用上述方法取值,可以通过在方法形参上增加@Param("username")
注解。
为什么在没有使用注解的前提下,使用方法形参名取值就会出现取不到传递的具体实参值呢?
多个参数的情况下,传递的实参值会被封装为map作为具体的值value属性,方法形参如果没有使用@Param
注解,那么最终封装的map会以param1或者arg0
做为map的键key属性,实参的值做为map具体的值,加了注解则是以@Param
注解里面的值当做键。当最后对sql语句的参数进行注入赋值时,是把#{参数名}
里面的参数名当做key去map中取值,如果方法参数没有@Param注解,那么map中的可以就是{param1,arg0...}
之类的键。
比如#{username}
,在最后对sql语句中的参数进行赋值的时候preparedStatement.setXxx()
,会将username当做键去之前封装实参的map中寻找实参值,显然这是找不到的,因为实参中没有username这个键,实参在封装的时候因为没有具体的@param注解,默认的key则会是param1,paramN之类的,当加了@param(username)
注解后,则封装实参的map会以username
为键,并且还会额外封装上param1,paramN的键,所以即使用了注解,仍然能使用#{param1...}
之类的方式取值。
源码分析:
PersonDao mapper = session.getMapper(PersonDao.class);
// session.getMapper得到PersonDao代理对象mapper
Person p = mapper.query("stronger", "123");
当执行具体的查询方法时,会被MapperProxy
的inovke方法拦截,MapperProxy
实现了InvocationHandler
接口,所以说它是基于JDK的动态代理来对方法拦截进行增强的。
public class MapperProxy<T> implements InvocationHandler, Serializable {
...
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
// method.getDeclaringClass()得到方法所在的类对象,如果该类是Object类,则放行,因为Object存在
//toString,euqals...,为了不拦截基类的方法所以放行
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else if (method.isDefault()) {
if (privateLookupInMethod == null) {
return invokeDefaultMethodJava8(proxy, method, args);
} else {
return invokeDefaultMethodJava9(proxy, method, args);
}
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
// 获得MapperMethod对象,里面封装了执行sql的必要属性,比如操作是CRU的哪一种
final MapperMethod mapperMethod = cachedMapperMethod(method);
// 调用execute方法
return mapperMethod.execute方法(sqlSession, args);
}
...
}
mapperMethod.execute方法
属于类MapperMethod
,这里主要对sql命令的类型CRUD进行判断,然后将实参值转化为sql命令的参数,就是将实参值进行了一步具体的封装。
public class MapperMethod {
...
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {
case INSERT: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
case UPDATE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
}
case DELETE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
}
case SELECT:
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
result = executeForMap(sqlSession, args);
} else if (method.returnsCursor()) {
result = executeForCursor(sqlSession, args);
} else {
//convertArgsToSqlCommandParam对实参值进一步的封装
// 调用本类下的convertArgsToSqlCommandParam
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
if (method.returnsOptional()
&& (result == null || !method.getReturnType().equals(result.getClass()))) {
result = Optional.ofNullable(result);
}
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}
public Object convertArgsToSqlCommandParam(Object[] args) {
// paramNameResolver返回封装的实参值
return paramNameResolver.getNamedParams(args);
}
...
}
ParamNameResolver
类存在一个有参的构造器,主要完成将方法形参值的封装,目的是将其作为封装map的key。这里面存在着两步转化
1)构造器中,首先会将这个方法的形参值以键值对形式存入SortedMap<Integer, String> names
中,key是Integer类型,代表形参的位置;value是方法形参值,这里的形参值并不是真实的形参值;如果形参值上标有@param注解,则形参值为注解的值,否则形参值默认为参数位置。
第一步将方法的形参值有注解则为注解的值,无注解则为默认参数位置作为value存入names中,下面注释也有说明。
{0:“username”}(有注解的情况),无注解则是 {0:“0”}
2)将names
中的值做为一个键,值为实参的值存入getNamedParams方法的param中,param就是一个map。
第二步将names中的值作为key,值为具体的实参值,存入param中。
{username:“stronger”}(有注解),无注解则是{param1:“stronger”}
源码如下:
public class ParamNameResolver {
private static final String GENERIC_NAME_PREFIX = "param";
/**
* <p>
* The key is the index and the value is the name of the parameter.<br />
* The name is obtained from {@link Param} if specified. When {@link Param} is not specified,
* the parameter index is used. Note that this index could be different from the actual index
* when the method has special parameters (i.e. {@link RowBounds} or {@link ResultHandler}).
* </p>
* <ul>
* <li>aMethod(@Param("M") int a, @Param("N") int b) -> {{0, "M"}, {1, "N"}}</li>
* <li>aMethod(int a, int b) -> {{0, "0"}, {1, "1"}}</li>
* <li>aMethod(int a, RowBounds rb, int b) -> {{0, "0"}, {2, "1"}}</li>
* </ul>
*/
private final SortedMap<Integer, String> names;
private boolean hasParamAnnotation;
public ParamNameResolver(Configuration config, Method method) {
final Class<?>[] paramTypes = method.getParameterTypes();
final Annotation[][] paramAnnotations = method.getParameterAnnotations();
final SortedMap<Integer, String> map = new TreeMap<>();
int paramCount = paramAnnotations.length;
// get names from @Param annotations
for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) {
if (isSpecialParameter(paramTypes[paramIndex])) {
// skip special parameters
continue;
}
String name = null;
for (Annotation annotation : paramAnnotations[paramIndex]) {
if (annotation instanceof Param) {
hasParamAnnotation = true;
name = ((Param) annotation).value();
break;
}
}
if (name == null) {
// @Param was not specified.
if (config.isUseActualParamName()) {
name = getActualParamName(method, paramIndex);
}
if (name == null) {
// use the parameter index as the name ("0", "1", ...)
// gcode issue #71
name = String.valueOf(map.size());
}
}
map.put(paramIndex, name);
}
names = Collections.unmodifiableSortedMap(map);
}
/**
* <p>
* A single non-special parameter is returned without a name.
* Multiple parameters are named using the naming rule.
* In addition to the default names, this method also adds the generic names (param1, param2,
* ...).
* </p>
*/
public Object getNamedParams(Object[] args) {
final int paramCount = names.size();
if (args == null || paramCount == 0) {
return null;
// 1.对于单参并且没有注解的情况,直接返回args[0],并不是存入map中取,所以
// 单参取值不依赖于具体的参数名,如#{name},name与方法形参名称相同与否没有关系。
} else if (!hasParamAnnotation && paramCount == 1) {
return args[names.firstKey()];
} else {
// 2. 多参或者存在注解(包括单参注解)
// 都是封装为map,之后注入取值的时候根据key去map中取值
final Map<String, Object> param = new ParamMap<>();
int i = 0;
// 下面就解释了为什么用username(没有加注解)的情况下,取值取不到,
// 因为并没有这个键,此时只能使用通用键值的形式取值(param1, param2, ...)。
for (Map.Entry<Integer, String> entry : names.entrySet()) {
// 将names的value属性作为key,names的键作为数组的下标去args取具体的实参值。
param.put(entry.getValue(), args[entry.getKey()]);
// add generic param names (param1, param2, ...)
// 下面还会增加一个通用的键和值 param1,param2,...
final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1);
// ensure not to overwrite parameter named with @Param
if (!names.containsValue(genericParamName)) {
param.put(genericParamName, args[entry.getKey()]);
}
i++;
}
return param;
}
}
}
-
一般情况下,使用
#{}
能有效防止sql注入 -
对于想要直接在SQL语句中插入一个不转义的字符串,比如动态地查询更改表名,字段排序等。
order by ${columnName}
#{}
:规定参数的一些规则,参数也可以指定一个特殊的数据类型。
#{age,javaType=int,jdbcType=NUMERIC,typeHandler=MyTypeHandler}
javaType jdbcType mode(存储过程) numericScale
resultMap typeHandler jdbcTypeName expression(未来支持的功能)
大多时候,你只须简单指定属性名,顶多要为可能为空的列指定 jdbcType
,其他的事情交给 MyBatis 自己去推断就行了。
jdbcType:当插入的数据为空时,有些数据库不能很好地支持mybatis对null的默认处理,比如Oracle在插入空值的时候会报错JdbcType OTHER,无效的数据类型
,因为mybatis对所有的nulll映射的都是原生JDBC的OTHER类型,但是Oracle不支持。
由于全局配置中 其值为 OTHER,Oracle不支持 所以会报错,但是MySQL支持。
解决方案:
-
修改全局配置中的设置项
jdbcTypeForNull=NULL
-
指定插入的数据类型
#{email, jdbcType=NULL}
结果映射
resultType
和resultMap
是对select结果进行封装的两个属性。
1、resultType:期望从这条语句中返回结果的类全限定名或别名
1) 如果返回的是集合,那应该设置为集合包含的类型,而不是集合本身的类型。 如果返回的是map,则表示将查询结果封装到以column_name-value的形式封装到map中。
Map<Object, Object> selectReturnMap(Integer id);
<select id="selectReturnMap" resultType="map">
select * from employee where id=${id}
</select>
EmployeeDao mapper = sqlSession.getMapper(EmployeeDao.class);
Map<Object, Object> emp = mapper.selectReturnMap(1);
// 输出:将结果集中的列直接封装到map,并没有做任何转化
// {address=石家庄, gender=1, name=张三, id=1, dept_id=1, age=23}
//@MapKey value值可以作为整个结果的key
@MapKey("id")
Map<Object, Object> selectReturnMap(Integer id);
// {1={{address=石家庄, gender=1, name=张三, id=1, dept_id=1, age=23}}}
结果能成功地映射到map上,但是更推荐使用JavaBean或者pojo映射。
2)这个属性,并不能详细地规定具体每一列应该封装成什么类中的具体某个属性。
如数据库下划线命名转实体类驼峰命名:可以有两种方式进行配置这种数据库字段到实体类字段的封装。
- 开启下划线转驼峰命名规范,缺陷是数据库命名和实体类命名一致性要求比较高。
- 查询结果配置别名,select a_name as aName…,配置成mybatis能够自动封装的字段。
- 使用resultMap属性,对列和实体属性进行详细映射。
3)如果封装的对象里面包括非Java自带类的引用类型,那么设置改属性往往就不能成功地将结果封装到对象。
这时,就可以使用resultMap来解决。
2、resultMap:自定义结果集映射规则
一个员工对应一个部门,实体类中包含有部门的引用,在对emp表和dept表连接查询时,查询的结果集如何封装到员工类中的部门属性。
-
将需要封装到复合引用对象的某个结果列重命名为
"引用对象.属性名的方式"
,<select id="selectUserAndRoleById" resultType="wjx.mybatis.model.SysUser"> select u.id, u.user_name userName, u.user_password userPassword, u.user_email userEmail, u.user_info userInfo, u.head_img headImg, u.create_time createTime, r.id "role.id", r.role_name "role.roleName", r.enabled "role.enabled", r.create_by "role.createBy", r.create_time "role.createTime" from sys_user u inner join sys_user_role ur on u.id=ur.user_id inner join sys_role r on r.id=ur.role_id where u.id=#{id} </select>
-
使用
<resultMap>
标签子标签:
<id property="" column=""/>
:id定义对象的唯一标识也就是主键。<result property="" column=""/>
:定义普通列结果集映射规则。可以将JavaBean的全部属性都在resultMap中声明封装规则(推荐),也可以只写一部分,其余部分采用默认封装规则,但是推荐写的话就全写上。
<resultMap id="userMap" type="wjx.mybatis.model.SysUser"> <id property="id" column="id"/> <result property="userName" column="user_name"/> <result property="userPassword" column="user_password"/> <result property="userEmail" column="user_email"/> <result property="userInfo" column="user_info"/> <result property="headImg" column="head_img" jdbcType="BLOB"/> <result property="createTime" column="create_time" jdbcType="TIMESTAMP"/> <result property="role.id" column="id"/> <result property="role.roleName" column="role_name"/> <result property="role.enabled" column="enabled"/> <result property="role.createBy" column="create_by"/> <result property="role.createTime" column="create_time" jdbcType="TIMESTAMP"/> </resultMap>
-
方式2的进一步优化,
<assocation>
标签嵌套查询<!--方式 1--> <assocation property="role" javaType="xx.xx.Role"> <result property="id" column="id"/> <result property="roleName" column="role_name"/> <result property="enabled" column="enabled"/> <result property="createBy" column="create_by"/> <result property="createTime" column="create_time" jdbcType="TIMESTAMP"/> </assocation> <!--方式 2:结合标签中的select属性--> <assocation property="role" select="xx.xx.RoleDao.queryById" column="role_id"/> <!-- select:dao接口下全限定方法名-->
注意:resultMap
中的嵌套查询(分布查询),默认是及时加载,可以使用fetchType="lazy|eager"
局部或者lazyLoadingEnabled
全局设置,配置嵌套查询的执行时机。
assocation
用来配置一对一的结果映射,一对多的结果映射是通过collection
标签配置的,用法都是大同小异。
关联assocation
关联的不同之处是,你需要告诉 MyBatis 如何加载关联。MyBatis 有两种不同的方式加载关联:
- 嵌套 Select 查询:通过执行另外一个 SQL 映射语句来加载期望的复杂类型。
- 嵌套结果映射:使用嵌套的结果映射来处理连接结果的重复子集。
集合collection
集合元素和关联元素几乎是一样的,他们的不同之处如下。
-
ofType
属性指定集合中的元素类型,javaType
指定具体的集合类型。ofType”
属性。这个属性非常重要,它用来将JavaBean(或字段)
属性的类型和集合存储的类型区分开来。 所以你可以按照下面这样来阅读映射:<collection property="posts" javaType="ArrayList" column="id" ofType="Post" select="selectPostsForBlog"/>
读作: “posts 是一个存储 Post 的 ArrayList 集合”
动态SQL
1、<if test="" />
test:判断表达式(OGNL,Apache)
-
作用:从参数中取值进行判断
-
注意点:遇见特殊符号应该写转移字符
<if test="id != null"/> <if test="name != null and name != ''"/> <if test="name != null && name != """/>
2、mybatis条件查询的时候可能出现的问题
如果条件查询中没有匹配的条件,查询语句就有可能变成这样:
SELECT * FROM BLOG
WHERE
解决方法:
1)where后新增 1 = 1
2)使用<where>
标签,mybatis会将<where>
标签后第一个出现的and去除,只是去除第一个紧挨着的,不会去除条件之后的。
where 元素只会在子元素返回任何内容的情况下才插入 “WHERE” 子句。而且,若子句的开头为 “AND” 或 “OR”,where 元素也会将它们去除。
当name不为空时,则进行查询,如果后面的if判断条件不成立,sql语句就可能为where name like ... and
,后面的and并不会被去掉,如果其后为and order by ..
,就会出现查询错误。
<where>
<if test="name != null and name != ''">
and name like #{name} and
</if>
<if test="...">
..
</if>
显然<where>
标签不能解决上面的这个问题,这时就可以自定义trim元素
来定制where元素
的功能。
<trim prefix="WHERE" prefixOverrides="AND |OR ">
...
</trim>
prefixOverrides 属性会忽略通过管道符分隔的文本序列(注意此例中的空格是必要的)。上述例子会移除所有 prefixOverrides 属性中指定的内容,并且插入 prefix 属性中指定的内容。
所以上面的例子可以改写为
<trim prefix="where" suffixOverrides="AND |OR ">
<if test="name != null and name != ''">
and name like #{name} and
</if>
<if test="...">
..
</if>
3、<set>
标签:set标签会动态的在行首插入set关键字,并会删除额外的逗号包括头和尾的逗号,所以说不会出现where标签只去头不去尾的情况。
同样<set>
标签也可以用<trim>
代替
<trim prefixOverrides="," prefix="set" suffixOverrides="," suffix="where">
<if test="name != null">
,name=#{name},
</if>
<if test="age != null">
age=#{age}
</if>
</trim>
总结:
where
标签主要是阶解决拼接动态SQL时的and关键字遗留问题。
set
标签主要是解决拼接动态SQL时遗留的逗号问题。
trim标签可以实现了对where
标签和set
标签高度自定义。
4、foreach
标签:对集合中的元素进行遍历
使用场景:批量插入(MySQL),构建IN条件语句。
<select id="selectPostIn" resultType="domain.blog.Post">
SELECT *
FROM POST P
WHERE ID in
<foreach item="item" index="index" collection="list"
open="(" separator="," close=")">
#{item}
</foreach>
</select>
元素属性说明:
-
collection:遍历的集合对象,可以是(List、Set、Map)
注意:这里的值不能随便写,前面曾经提到过mybatis在对参数进行封装的时候,集合类型的也会封装成map,在没有
@Param
注解标注的前提下,取值的时候集合类型必须写collection、list,对于Map值为map。 -
item:本次迭代获取到的元素,当使用map对象时,item表示值,index表示键
-
index:当前迭代的序号(索引,相对于list来说)
-
open:指定字符串开始的字符
-
close:指定字符串结尾的字符
-
separator:遍历的元素与元素之间的分隔符
示例:批量插入
<!--第一种写法-->
<insert id="insertBatch">
insert into employee(name, age, gender, address, dept_id)
values
<foreach collection="list" item="e" separator=",">
(#{e.name},#{e.age},#{e.gender},#{e.address},#{e.deptId})
</foreach>
</insert>
<!--第二种写法
这种方式,必须配置数据库的连接属性
jdbc:mysql:///db?characterEncoding=utf8&allowMultiQueries=true
allowMultiQueries详情可见官网https://dev.mysql.com/doc/connector-j/5.1/en/connector-j-reference-configuration-properties.html
-->
<insert id="insertBatch">
<foreach collection="list" item="e" separator=";">
insert into employee(name, age, gender, address, dept_id)
values
(#{e.name},#{e.age},#{e.gender},#{e.address},#{e.deptId})
</foreach>
</insert>
Oracle批量插入的方式:
Oracle不支持MySQL形式的批量插入,可以通过两种方式实现批量插入。
1)多个insert放在begin - end块中。
begin
insert ...;
insert ...;
end;
动态SQL形式
<insert>
<foreach collection="list"
item="item"
seperator=";"
open="begin" end="end;">
insert ...
</foreach>
</insert>
2)利用中间表
insert into employees(id, name, email)
select employees.nextval,name,email from(
select '插入的名字' name, '邮箱' email from dual
union
select '插入的名字' name, '邮箱' email from dual
union
...
);
动态SQL形式
<insert id="...">
insert into employees(id, name, email)
select employees.nextval,name,email from
<foreach collection="list"
item="item"
seperator="union"
open="(" end=")">
select ...
</foreach>
</insert>
5、mybatis的两个内置参数_parameter
和_databaseId
_parameter
:代表整个参数
单个参数:_parameter
就是这个参数
多个参数:参数会被封装为一个map:_parameter
就是代表这个map,_paramter.get(0)
可以得到第一个参数对象。
_databaseId
:如果配置了databaseIdProvider
标签
_databaseId
就是代表当前数据库的别名。
应用场景:动态sql if标签条件判断中,往往都是对参数中的属性进行判断比如判空,没有一个对象能够代表方法参数,_parameter
就可以用来代表方法上的参数对象。
6、bind
:从OGNL表达式中创建一个变量,并将其绑定到上下文中,常用于模糊查询的SQL中。
模糊查询的处理方式:
假设要查询姓名为姓张的员工
错误的模糊查询:
xml
<select id="selectEmpByName" resultMap="baseMap">
select id,name,gender,age,dept_id,address
from employee
where name like '%#{name}%';
</select>
//dao接口定义
List<Employee> selectEmps(String name);
EmployeeDao mapper = sqlSession.getMapper(EmployeeDao.class);
List<Employee> list = mapper.selectEmpByName("张");
上述代码运行后存在一个Cause: java.sql.SQLException: Parameter index out of range (1 > number of parameters, which is 0)
异常,这种方式并不能'%#{name}%'
不能支持模糊查询。
#{}
是以预编译的形式通过PreparedStatement对象进行设置参数。
1)like '%#{name}%'
修改为like #{name}
,调用查询API的时候传入"%"
mapper.selectEmpByName("张%");
2)使用${}
字符串拼接的形式
name like '%${name}%';
这样在调用API的时候直接传入要查的姓即可。
3)通过bind
标签将值取出二次修改后,在上下文中引用这个修改后的值
<select id="selectEmpsByName" resultMap="baseMap">
<!--将name值取出进行%拼接,下文中在通过#{_name}的形式引用定义在name属性中的变量-->
<bind name="_name" value="'%'+name+'%'"/>
select id,name,gender,age,dept_id,address
from employee
where name like #{_name};
</select>
7、<sql>
和<include refid="">
<sql>
:定义可重用的代码片段,以便在其他语句中使用。参数可以静态地(在加载的时候)确定下来,并且可以在不同的 include 元素中定义不同的参数值。
应用场景:比如插入和查询常用的列片段,可以将其定义在sql
中,之后直接可以通过<include refid="">
标签进行引用。
<include refid="">
:引用sql定义的代码片段,包含的子标签
<property name="" value=""/>
,可以用于对引用的代码片段赋值。
示例1:片段的引用
<sql id="cols">
id,name,gender,age,dept_id,address
</sql>
<select id="selectEmpsByName" resultMap="baseMap">
select
<!--通过include标签进行对sql片段进行引用-->
<include refid="cols"/>
from employee
where name like #{_name};
</select>
示例2:片段的赋值
<sql id="cols">
${tb_emp}.name,${tb_dept}.name
</sql>
<select id="selectName" resultType="map">
select
<include refid="cols">
<property name="tb_emp" value="employee"/>
<property name="tb_dept" value="dept"/>
</include>
from
employee,dept
where
employee.dept_id=dept.dept_id;
</select>
sql语句
select employee.name,dept.name from employee,dept where employee.dept_id=dept.dept_id;
缓存
MyBatis默认有两级缓存:
一级缓存
一级缓存(本地缓存):SqlSession
级别的缓存。一级缓存是一直开启的,不能关闭;与数据库同一次会话期间查询到的数据会放在本地缓存中。以后如果需要获取相同的数据,直接从缓存中拿,不需要查询数据库。
一级缓存失效情况(没有使用到当前一级缓存的情况,效果就是,还需要再向数据库发出查询)
-
通过
SqlSessionFactory
拿到不同的SqlSession
对象,不同的SqlSession
都有自己的缓存,它们之间互不干扰。 -
SqlSession
相同,查询条件不同。(当前一级缓存中没有这个数据) -
SqlSession
相同,两次查询之间进行了增删改操作,缓存也会失效。以我们的角度去想,增删改可能会对当前查询的数据造成影响,所以要去数据库中查询。 -
SqlSession
相同,查询期间执行了清空缓存操作。
二级缓存
二级缓存(全局缓存):基于namespace
级别的缓存,一个namespace
对应一个二级缓存。
工作机制:首次查询数据,数据就会放在当前会话的一级缓存中,如果会话关闭,一级缓存中的数据会被保存到二级缓存中,开启一个新的会话查询信息,此时就会去二级缓存中查找。
不同的namespace查出的数据会放在自己对应的缓存中(map)
sqlSession == EmployeeMapper ==> Employee
DepartmentMapper ==> Department
二级缓存的使用步骤:
1)在全局配置文件中开启缓存
设置名 | 描述 | 有效值 | 默认值 |
---|---|---|---|
cacheEnabled | 全局性地开启或关闭所有映射器配置文件中已配置的任何缓存。 | true | false | true |
<settings>
<!--cacheEnabled:默认是true,但是为了防止以后版本变化所带来的影响,
一般还会显示声明一下-->
<setting name="cacheEnabled" value="true"/>
</settings>
2)二级缓存基于namespace的缓存,在需要使用二级缓存的SQL映射文件中添加一行:
<cache/>
3)一定要实现序列化接口
<cache>
标签的属性:
<cache
eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true"
type="自定义Cache接口的实现类全类名"/>
eviction:清除策略,默认的是LRU
可用的清除策略有:
LRU
– 最近最少使用:移除最长时间不被使用的对象。FIFO
– 先进先出:按对象进入缓存的顺序来移除它们。SOFT
– 软引用:基于垃圾回收器状态和软引用规则移除对象。WEAK
– 弱引用:更积极地基于垃圾收集器状态和弱引用规则移除对象。
flushInterval(刷新间隔)属性可以被设置为任意的正整数,设置的值应该是一个以毫秒为单位的合理时间量。 默认情况是不设置,也就是没有刷新间隔,缓存仅仅会在调用语句时刷新。
size(引用数目)属性可以被设置为任意正整数,要注意欲缓存对象的大小和运行环境中可用的内存资源。默认值是 1024。
readOnly(只读)属性可以被设置为 true 或 false。只读的缓存会给所有调用者返回缓存对象的相同实例。 因此这些对象不能被修改。这就提供了可观的性能提升。
而可读写的缓存会(通过序列化)返回缓存对象的拷贝,所以说使用二级缓存涉及到的实体对象要实现序列化接口。 速度上会慢一些,但是更安全,因此默认值是 false。
注意点:只有会话被关闭,一级缓存中的数据会被保存到二级缓存中。
缓存有关的属性设置
1、全局配置cacheEnabled
掌管着二级缓存的开启关闭,若全局二级缓存处于关闭状态,那么即便在标签中<select useCache="true">
也不能使用二级缓存。这个属性对一级缓存没有影响。
2、<select>
标签的useCache="true|false"
属性,决定是否使用二级缓存,对一级缓存没有影响。
3、每个增删改标签的flushCache="true"
,清空一二级缓存;查询标签flushCache="false"
,如果将查询标签的这个属性设置为true
,那么在执行一次查询后也会清空一二级缓存。
4、sqlSession.clearCache()
;只会清除一级缓存。
5、对于增删改操作,默认都会清空缓存,但是多了一步去缓存中查找的过程,尽管有命中率,但是缓存中实际没有数据,所以还会再次发送一条sql语句。
缓存的原理
MyBatis运行原理
1、获取SqlSessionFactory
//===============================SqlSessionFactory
//1.SqlSessionFactory.buid
public SqlSessionFactory build(InputStream inputStream) {
return build(inputStream, null, null);
}
...
//2.构建XMLConfigBuilder,实例化该对象的同时会初始化重要的类成员比如
//configuration,XPath对象等
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
// 调用parser.parse
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
inputStream.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
//=============================XMLConfigBuilder
//3.
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
//parser.evalNode("/configuration")调用XPath解析获得根节点对象root
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
//4.解析配置文件的每个标签中的每个属性放入到configuration中
private void parseConfiguration(XNode root) {
try {
//issue #117 read properties first
propertiesElement(root.evalNode("properties"));
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
loadCustomLogImpl(settings);
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
//5.解析完毕之后返回Configuration对象给buid方法,最后生成一个DefaultSqlSessionFactory实例
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
)
2、获取SqlSession
//==================================DefaultSqlSessionFactory
public SqlSession openSession() {
/*
configuration.getDefaultExecutorType()获取默认执行器类型,有三种
SIMPLE/BATCH/官网有介绍 defaultExecutorType 配置默认的执行器。
SIMPLE 就是普通的执行器;REUSE 执行器会重用预处理语句(PreparedStatement); BATCH 执行器不仅重用语句还会执行批量更新。 SIMPLE REUSE BATCH
*/
return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
// 获取执行器
final Executor executor = configuration.newExecutor(tx, execType);
// 最后返回DefaultSqlSession对象,里面封装了configuration,executor重要的属性
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
//================================Configuration.newExecutor
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);
}
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
// !!!拦截器重要的一个环节
// 调用拦截器链的pluginAll方法保证Executor
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
3、获取MapperProxy
//=============================DefaultSqlSession.getMapper
@Override
public <T> T getMapper(Class<T> type) {
return configuration.getMapper(type, this);
}
//=============================Configuration.getMapper
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}
//=============================MapperRegistry.getMapper
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
//根据dao类获取MapperProxyFactory,前面提到过,MapperRegistry有两个重要属性,config和knownMappers
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
try {
// 调用mapperProxyFactory生成MapperProxy,MapperProxy就是一个实现了InvocationHandler的
//接口
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
public class MapperProxyFactory<T> {
@SuppressWarnings("unchecked")
protected T newInstance(MapperProxy<T> mapperProxy) {
//JDK动态代理
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
}