1. Mybatis三大对象作用域
为什么要将SqlSession对象放到ThreadLocal当中呢?
这是为了保证一个线程对应一个SqlSession
,在一个session
会话中,一个线程对应一个sqlSession
1.1 SqlSessionFactoryBuilder
这个类的使命是用来创建SqlSessionFactory
的,因此这个类的最佳作用域就是方法作用域,它的build()方法实际上就是在读取xml文件,最好不会一直保留它,要保证所有的XML解析资源被释放给其他进程使用
1.2 SqlSessionFactory
这个对象的使命是用来创建SqlSession
,它一旦被创建就应该在应用的运行期间一直存在,没有任何理论丢弃它或者重新创建新的实例,它在运行期间不要重复创建多次,因为每一次的创建都涉及到IO的操作,因此它的最佳作用域就是应用作用域,可以使用单例模式
或者静态单例模式
来维护这个对象
1.3 SqlSession
SqlSession
的最佳实践就是:每个线程都拥有自己的SqlSession
实例,它的实例不是线程安全的,因此不能被共享,它的最佳作用域是请求域或者方法域,绝对不能将SqlSession
实例的引用放在一个类的静态域中,甚至一个类的实例变量也是不允许的。
它类似于
HttpSession
,你可以考虑将SqlSession
放在一个和Http
请求相似的作用域中,每次收到Http
请求,就可以打开一个SqlSession
,返回一个响应后就关闭它
本质上,SqlSession
是对Conncetion
对象的封装,因此其本身就是不安全的,由于一次缓存是session
级别的,所以如果有多个线程同时使用session
,当A进行set()
,而B进行get()
的时候,这时候就会产生缓存与数据库数据不一致的问题,产生线程不安全的问题。
为什么Connection是线程不安全的
connection.commit()
是提交事务,假设线程A开启了事务,而线程B此时使用commit()
提交了事务,在这种情况下,由于使用的是描述同一次连接,因此线程A就相当于没有开启事务,最终就会导致程序错误。
2. 使用javassist生成类
javassist是一个开源的分析、编辑和创建Java字节码的类库,通过使用Javassist对字节码操作为JBoss实现动态的AOP框架
2.1 示例代码
//1.获取类池,这个类池就是用来生成class的
ClassPool pool = ClassPool.getDefault();
//2.制造类,需要指定类名
CtClass ctClass = pool.makeClass("dao.StudentDAOImpl");
//3.制造方法,src里面写的是方法代码
String methodCode = "public void insert(){System.out.println(123);}";
CtMethod ctMethod = CtMethod.make(methodCode, ctClass);
ctClass.addMethod(ctMethod);
//生成类,在内存中生成class
ctClass.toClass();
//类加载,返回字节码
Class<?> clazz = Class.forName("dao.StudentDAOImpl");
Object newInstance = clazz.newInstance();
//获取方法
clazz.getDeclaredMethod("insert").invoke(newInstance);
2.2 用类去实现接口
public void testGenerate() throws Exception {
//1.获取类池,这个类池就是用来生成class的
ClassPool pool = ClassPool.getDefault();
//2.制造类,需要指定类名
CtClass ctClass = pool.makeClass("dao.TestDAOImpl");
//制造接口
CtClass ctInterface = pool.makeInterface("dao.TestDAO");
//3.制造方法,src里面写的是方法代码
String methodCode = "public void insert(){System.out.println(123);}";
CtMethod ctMethod = CtMethod.make(methodCode, ctClass);
ctClass.addInterface(ctInterface);//让这个类去实现这个接口
ctClass.addMethod(ctMethod);
//生成类,在内存中生成class,同时将生成的类加载到JVM中
Class<?> clazz = ctClass.toClass();
TestDAO testDAO = (TestDAO) clazz.newInstance();
//获取方法
testDAO.insert();
}
2.3 GenerateDaoProxy
作用就是:你只要给我一个接口,那我就能够根据你这个类生成一个Impl
对象
public static Object generate(Class<?> interfaceDAO) throws Exception {
ClassPool pool = ClassPool.getDefault();
//制造类
CtClass ctClass = pool.makeClass("dao.impl"+interfaceDAO.getName()+"Proxy");
//制造接口
pool.makeClass(interfaceDAO.getName());
//实现接口
ctClass.addInterface(ctClass);
//实现接口中的所有方法
Method[] methods = interfaceDAO.getDeclaredMethods();
Arrays.stream(methods).forEach(method->{
//将method进行实现
try {
StringBuilder code = new StringBuilder();
code.append("public ");
code.append(method.getReturnType());
code.append(" ");
code.append(method.getName());
code.append("(");
Class<?>[] parameterTypes = method.getParameterTypes();
for (int i = 0;i<parameterTypes.length;i++) {
Class<?> parameterType = parameterTypes[i];
code.append(parameterType);
code.append(" ");
code.append("arg");
code.append(i);
if(i!=parameterTypes.length-1){
code.append(",");
}
}
code.append(")");
code.append("{");
code.append("System.out.println(\"123\")");
code.append("}");
CtMethod ctMethod = CtMethod.make("", ctClass);
ctClass.addMethod(ctMethod);
} catch (Exception e) {
e.printStackTrace();
}
});
//创建对象
Object obj = ctClass.toClass().newInstance();
return obj;
}
String sqlId = interfaceDAO.getName()+"."+method.getName();
SqlCommandType sqlCommandType = sqlSession.getConfiguration().getMappedStatement(sqlId).getSqlCommandType();
if(sqlCommandType == SqlCommandType.INSERT){
}else if(sqlCommandType == SqlCommandType.DELETE){
}//...
在Mybatis中,规定了namespace
必须是接口的全限定类名,sql的Id
必须是接口方法名称
,这样的话底层才能够定位到你所需要实现的方法
2.4 getMapper方法
Mybatis提供了相关的机制,为我们提供了生成dao接口的实现类
实际上采用了代理模式,在内存中生成了dao接口的代理类,然后创建代理类的实例,使用mybatis的这种代理机制的前提,SqlMapper.xml文件中的namespace必须是dao接口的全限定类名,id是接口中的方法名
SqlSessionFactory build = new SqlSessionFactoryBuilder().build(ClassLoader.getSystemClassLoader().getResourceAsStream("mybatis-config.xml"));
build.openSession().getMapper(StudentDAO.class);
有什么用?
使用场景是这样的,当实现类中的代码相对固定的时候,这时候就不需要创建新的Impl类了,只需要使用代理对象,在内存中生成这段类代码,然后执行即可
3. Mybatis小技巧
3.1 #{}和${}有什么区别?
#{}
:它是先编译语句然后再给占位符进行传值的,其底层是PreparedStatement
,可以防止sql
注入
${}
:先进行sql语句的拼接,然后再编译sql语句,底层是Stament
实现的,存在sql注入的现象,只有在需要进行sql语句关键字的拼接的情况下才会使用
为什么PreparedStatement能够防止sql的注入?
这是因为PreparedStatement程序在第一次查询数据库之前sql语句就被数据库进行了分析、编译、优化、以及具体的查询计划都形成了,只有参数位置才会使用占位符
当程序真正发起请求的时候,这时候传递过来的参数会被认为是某个字段的值,因而不会重新编译和优化,所以不会被认为是一个sql指令
如果传递进来的参数无法被看作是sql指令,那么就无法形成sql的注入了,比如说
'1=1'
这样的参数值会被认为是一个字符串类型的值,而不会看作是sql查询条件正是因为sql的执行计划是在sql编译和优化执行的,因此sql的注入只是在编译阶段起作用,解决办法就是不让参数参与编译的阶段,而是让占位符参与编译,从而解决了sql的注入问题。
3.2 什么时候使用${}
- 当sql的指令不确定的时候,需要前端传入动态的指令的时候,必须使用
${}
- 向sql语句中拼接表名,在实际的业务上,可能会存在分表存储数据的情况,当一张表进行查询的时候,查询效率比较低
3.3 批量删除
#根据id批量删除
delete
from t_car
where id in(${ids})
3.4 模糊查询like
select *
from t_car
where brand_name like ‘%${brandName}%’
或者是使用concat
函数,专门进行字符串的拼接
select *
from t_car
where brand_name like concat('%','#{brandName}','%');
3.5 别名
<!--起别名,别名不区分大小写-->
<typeAliases>
<!--type:原type alias:别名-->
<!--当你省略alias的时候,直接就是类的简名-->
<typeAlias type="domain.Student" alias="Student"/>
<!--对包下的所有alias自动起别名-->
<package name = "domain"></package>
</typeAliases>
3.6 mapper的配置
mapper的配置方式有
- resource:从类的根路径下开始查找资源,配置文件需要放到类路径当中才行
- url:这种方式是绝对路径方式,只需要提供绝对路径就行
- class:全限定接口名,带有包名
作用机制如下:如果class指定的是mapper.CarMapper
,那么它就会去mapper
下查找CarMapper.xml
文件
使用这种方法必须使得接口和xml文件在同一个目录下
<mappers>
<!--前提是你的包下的文件和xml文件在同一级目录,并且名字是一致的-->
<package name = "mapper"></package>
</mappers>
3.7 使用自动生成的主键
当在插入一条新的记录的时候,自动生成的主键,而这个主键需要在其他表中使用
插入一个用户数据的同时需要给该用户分配角色,需要将生成的用户的id插入到角色表的user_id字段上
<insert id = "insert" userGeneratedKeys="true" keyProperty="id">
<!--用car的id属性接收这个生成的主键-->
insert into t_car values(null,#{carNum},#{brand},#{guidePrice},#{produceTime},#{carType})
</insert>
4. Myabtis参数处理
4.1 单个简单类型参数
简单类型包括有
- byte short int long float double char
- Byte Shrot Integer Long Float Double Character
- String
- java.util.Date
- java.sql.Date
parameterType
属性的作用:就是告诉mybatis,这个方法的参数类型是什么类型,但是其具有自动类型推断的机制,所以大部分情况下都是可以省略不写的,其本质是为ps.setXxx()
服务的
mybatis框架实际上内置了很多别名
一个sql语句的完整写法是
<select id = "select" resultType = "student" >
select * from t_student where name = #{name,java_type=String,jdbcType=VARCHAR}
</select>
4.2 Map参数
<select id = "select" resultType = "student" parameterType="map">
select * from t_student where name = #{map集合的key}
</select>
4.3 实体类参数
如果传入的参数是实体类的话,那么就会自动调用getXxx()的方法获取实体类内部的属性值
<select id = "select" resultType = "student" parameterType="student">
select * from t_student where name = #{name}
</select>
4.4 多参数
如果是多个参数,mybatis底层是这样做的:
框架会自动创建一个map集合,并且map集合是以这样的方式存储参数的
map.put("arg0",name);
map.put("arg1",sex);
//或者是
map.put("param0",name);
map.put("param1",sex);
4.5 @Param注解(命名参数)
List<Student> selectByNameAndSex(@Param("name") String name,@Param("sex") Character sex);
当你加上这个@Param
的参数之后,这时候arg0
和arg1
…的对应参数都被替换掉了
4.6 @Param源代码
public class MapperProxy<T> implements InvocationHandler, Serializable {//jdk动态代理
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
return Object.class.equals(method.getDeclaringClass()) ? method.invoke(this, args) : this.cachedInvoker(method).invoke(proxy, method, args, this.sqlSession);
} catch (Throwable var5) {
throw ExceptionUtil.unwrapThrowable(var5);
}
}
}
- proxy:代理对象
- method:目标方法
- args[]:目标方法的参数
当执行mapper的方法的时候,这时候就会将值传递给args
@Override
public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
return mapperMethod.execute(sqlSession, args);
}
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {//获取sql语句的类型
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 {
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());
}
if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
throw new BindingException("Mapper method '" + command.getName()
+ " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
}
return result;
}
private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
List<E> result;
//将args转换为sql的命令
Object param = method.convertArgsToSqlCommandParam(args);
if (method.hasRowBounds()) {
RowBounds rowBounds = method.extractRowBounds(args);
result = sqlSession.selectList(command.getName(), param, rowBounds);
} else {
result = sqlSession.selectList(command.getName(), param);
}
// issue #510 Collections & arrays support
if (!method.getReturnType().isAssignableFrom(result.getClass())) {
if (method.getReturnType().isArray()) {
return convertToArray(result);
} else {
return convertToDeclaredCollection(sqlSession.getConfiguration(), result);
}
}
return result;
}
public Object getNamedParams(Object[] args) {
final int paramCount = names.size();
if (args == null || paramCount == 0) {
return null;
} else if (!hasParamAnnotation && paramCount == 1) {//有没有注解?如果没有注解而且是单个参数
Object value = args[names.firstKey()];
return wrapToMapIfCollection(value, useActualParamName ? names.get(0) : null);
} else {//否则的话就是有注解的,那么就走这个分支
final Map<String, Object> param = new ParamMap<>();
int i = 0;
for (Map.Entry<Integer, String> entry : names.entrySet()) {//遍历0->paramName的集合拿出来
param.put(entry.getValue(), args[entry.getKey()]);//名字->args[i]:参数的值
// add generic param names (param1, param2, ...)
final String genericParamName = GENERIC_NAME_PREFIX + (i + 1);//加param->args[i]
// ensure not to overwrite parameter named with @Param
//标准通用参数名
if (!names.containsValue(genericParamName)) {
param.put(genericParamName, args[entry.getKey()]);
}
i++;
}
return param;
}
}
5. Mybatis返回值处理
5.1 可以返回map
如果编写的Java类中没有属性能够与返回值一一对应,而且确定只返回一条数据,那么就可以用map
进行存储
5.2 返回Map列表
当有多条数据的时候,可以使用List<Map>
进行返回值的接收
5.3 返回大Map
可以设计一种Map<String,Map>
,可以让StringKey存储id即可
5.4 结果映射
为了使得返回的结果中的字段名和类的属性名能够对得上,可以:
- 用as起别名
- 用
resultMap
进行结果集的映射 - 开启驼峰命名自动映射
<!--使用resultMap-->
<!--指定数据库表的字段名和Java类的属性名的对应关系-->
<!--type:指定pojo类的类名-->
<!--id:指定resultMap的唯一标识,在select标签中使用-->
<resultMap id="StudentResult" type="domain.Student">
<id property="id" column="id"/>
<result property="name" column="name"/>
</resultMap>
<select id="selectOne" resultMap="StudentResult">
select * from student where id = #{id};
</select>
<settings>
<setting name="logImpl" value="SLF4J"/>
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
6. 动态SQL
6.1 if标签
<!--如果为true,那么就把里面的内容拼接到sql里面去-->
<!--注意:里面的参数也是从函数签名的定义中取的-->
<!--因此如果你用paramsX也是可以取到相关的值的-->
<!--如果是传过来一个pojo,那么就只能够使用属性名了-->
<!--如果使用了@Parmas("name"),那么可以使用里面指定的参数名-->
<!--&&符号可以被写成为and-->
<if test="brand!=null and name !=null">
brand like "%"#{brand}"%" and name like "%"#{param1}"%"
</if>
6.2 where标签
where标签是编写动态sql的核心:
- 当所有的条件都为空的时候,where标签保证不会生成where子句
- 自动去除某些条件前面多余的and 或者or
- 不会去除条件后面多余的and或者or
6.3 trim标签
它是用来在标签的前面或者后面添加内容的
- prefix:在trim标签中的语句前添加内容
- suffix:在trim标签中的语句后添加内容
- prefixOverrides:前缀覆盖掉(去掉)
- suffixOverrides:后缀覆盖掉(去掉)
- 将trim标签中内容的后缀或者前缀给去掉
<trim prefix="where" suffixOverrides="and|or"></trim>
6.4 set标签
主要用在update
语句当中,用来生成关键字,同时去掉最后面多余的,
比如只更新提交不为空的字段,如果提交的字段为空或者是””,那么就不更新了
6.5 choose when otherwise
仅有一个分支会被走
<chose>
<when></when>
<when></when>
<when></when>
<when></when>
<otherwise></otherwise>
</chose>
6.6 批量删除
<delete id = "delete">
delete from t_car where id in(
<!--collection:指定数组或者集合,写array或者argX,或者使用@Params中你所指定的名字-->
<!--item:代表数组或者集合中的元素-->
<!--separator:循环中的分割符-->
<!--开始符和结束符可以用open和close来指定-->
<foreach collection="ids" item="id" separator="," open = "(" close=")">
#{id}
</foreach>
)
</delete>
6.7 批量插入
<insert id="insertBatch">
insert into t_car values(id,name,type)
<foreach collections="cars" item="car" separator="," >
(#{car.id},#{car.name},#{car.type})
</foreach>
</insert>
6.8 sql标签与include
sql标签是用来声明sql的片段的,include标签用来将声明的sql片段包含到某个sql语句当中
作用:代码复用,容易维护
<sql id = "carCol">
id,
car_num as carNum
</sql>
<select id ="select" resultType="car">
select
<include refid = "carCol"/>
from t_car where id = #{id}
</select>