mybatis的实际使用
@Test
public void test() {
try(SqlSession sqlSession = sqlSessionFactory.openSession()){
// mybatis在 getMapper就会给我们创建jdk动态代理
EmpMapper mapper = sqlSession.getMapper(EmpMapper.class);
Emp emp = mapper.QueryEmp(4);
}
}
为什么使用Mybatis
JDBC
优点:运行期:快捷、高效
缺点:编辑期:代码量大、繁琐异常处理、不支持数据库跨平台
DBUtils
封装了对 JDBC 的操作
Hibernate
优点:
使用 xml 映射 java类到 数据库表格,不用编写任何代码
为在数据库中直接存储和检索 Java 对象提供简单 API
数据库出现变化,仅需要修改 XML 文件属性
抽象不熟悉的 SQL,提供熟悉的 Java 对象
缺点:
完全封装导致无法使用数据的一些功能
缓存问题
对于代码的耦合度太高
寻找bug 困难
批量数据操作需要大量的内存空间且执行过程中需要的对象太多
JDBCTemplate
优点:运行期:高效、内嵌框架中、支持基于 AOP 的声明式事务
缺点:必须与 Spring 框架结合在一起使用、不支持数据库跨平台、默认没有缓存
mybatis
半 ORM(对象关系映射) 框架(需要自己写SQL),开发者只需要关注 SQL 本身,不需要关心 加载驱动,创建连接,创建 statement 等过程。
可以使用 XML 或注解配置和映射原生信息,将 POJO 映射为数据库中的记录。将执行的各种 statement 配置起来,通过 java 对象和 statement中的 SQL动态参数映射生成最终执行的 SQL 语句。
优点:
- 基于 SQL 语句编程,不会对应用程序或者数据库的现有设计造成影响,SQL 写在 XML 里,可重用,易维护。
- 减少代码量(手动开关连接)
- 与各种数据库兼容(mybatis 使用 JDBC 连接数据库,JDBC 支持的数据库都支持)
- 与 Spring 很好集成
- 提供映射标签
缺点:
- SQL语句的编写工作量大,尤其字段多、关联表多时
- SQL 语句依赖于数据库,导致数据库移植性差,不能随便更换数据库(更换数据库SQL语句很大差别)
#{} 与 ${}
${} 相当于直接替换为括号中内容,有SQL注入风险,一般用在动态表名、列名。如
select * from ${teble_name};
使用 select * from #{table_name};会造成生成的表名有 "" 而查询失败
#{} 会预处理,相当于 ? 占位符
其中的参数不用
@param
注解时,1个参数随便写什么值,多个参数则使用arg1 arg2 param1 param2
传参为 javaBean(一个java对象)时
- 1个对象,可以直接使用对象的属性名(如
#{id} #{name}
等)- 多个对象,例子:
#{param1} #{param2.id}
(参数一是基本数据类型,参数二是对象类型)集合或数组
- 集合(list):
- 用了
@Param
注解,使用#{usernames[0]}
(注解定义名字)。- 没用注解,使用
#{list[0]} #{arg0[0]}
- 数组:会自动封装为 map
{key: "array": value: username}
- 使用
#{arg0[0]} #{param0[0]} #{array[0]} #{myArray[0]}
映射map:与 javaBean 相同
传递多个参数
- 使用#{0} #{1} #{2}
- 使用 @param 注解,#{参数名}
- 使用 map 传参,#{map的key},注意
parameterType
为 map
Dao接口和XML文件的SQL 如何创建关联
- 创建SqlSource,mybatis会把SQL标签封装成SqlSource 对象。
- 创建 MappedStatement对象,每一个SQL标签对应一个 MappedStatement对象。创建完 MappedStatement对象后将它缓存到 Configuration中。
Configuration是mybatis中的大管家,基本所有的配置信息都维护在这里。
- 通过
全限定类名+方法名
找到MappedStatement
对象,然后解析里面的SQL内容,执行即可。
Dao代理接口, 通过 jdk 动态代理,返回一个Dao 接口代理对象,这个代理对象的处理器是 MapperProxy对象,所以,我们通过 @Autowired
注入Dao 接口时,注入的就是这个代理对象,调用 Dao 接口方法时,则会调用到 MapperProxy 对象的 invoke()
方法
invoke()
方法实际上调用的就是 SqlSession中的东西
不同的 XML 映射文件,id 是否可以重复?
不同的Xml 映射文件,如果配置了namespace,那么 id 可以重复;如果没有配置 namespace,那么 id 不能重复。因为 namespace+id 是作为 Map<String, MapperStatement>
的 key 使用的。(新版本中namespace是必须的)
如何分页,分页原理
使用 RowBounds 对象进行分页,是将结果查出后在内存中进行分页。
分页插件的基本原理是使用mybatis提供的接口,实现自定义插件。在插件的拦截方法内,拦截待执行的SQL,然后重写SQL,根据 dialect 方言,添加对应的威力分页语句和物理分页参数
ParameterHandler:拦截执行Sql的参数的组装
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7TClZPPo-1624950967312)(E:\学习\img\36-339645944.png)]
插件运行原理
Configuration:所有配置的相关信息。
ResultSetHandler:用于拦截执行结果的组装。
ParameterHandler:拦截执行Sql的参数的组装。
Executor:执行Sql的全过程,包括组装参数、组装结果和执行Sql的过程。
BoundSql:执行的Sql的相关信息。
mybatis可以编写针对 ParameterHandler
、ResultSetHandler
、StatementHandler
、Executor
四种接口的插件。使用jdk动态代理,为需要拦截的接口生成代理对象以实现接口方法拦截功能。具体是InvocationHandler
中的 invoke()
方法。
编写插件:实现Mybatis 的 Interceptor 接口并复写 intercept()
方法,然后再给插件编写注解,指定要拦截哪一个方法即可,最后在配置文件中配置你编写的插件
是否支持延迟加载
仅支持 association
关联对象和 collection
关联集合对象,association
指一对一,collection
指的就是一对多查询。需要开启延迟加载
<settings>
<!-- 开启延迟加载,所有分步查询都是懒加载 (默认是立即加载)-->
<setting name="lazyLoadingEnabled" value="true"/>
<!--当开启时, 使用 pojo 中任意属性都会加载延迟查询 ,默认是 false -->
<setting name="aggressiveLazyLoading" value="false"></setting> 默认FALSE,不设置也可
<!--设置对象的哪些方法调用会加载延迟查询 默认:equals,clone,hashCode,toString-->
<setting name="lazyLoadTriggerMethods" value=""/>
</settings>
<!-- 单独关闭一个延迟加载 -->
<association property="dept" fetchType="eager" column="dept_id" select="cn.tulingxueyuan.mapper.DeptMapper.SelectDept">
</association>
延迟加载的基本原理是:使用 GCLIB 创建目标对象的代理对象,当调用目标方法时,进入拦截器方法。比如调用
a.getB().getName()
,拦截器invoke()
方法发现a.getB()
是null
,那么就会单独发送事先保存好的查询关联 B 对象的SQL,把 B 查询上来,于是 a 的 b 属性就有了值,接者完成方法调用
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZoIREMyn-1624950967314)(E:\学习\img\1-10.png)]
在映射文件中,元素和元素中都已默认配置了延迟加载属性,即默认属性
fetchType=”lazy”
(属性fetchType=”eager”
表示立即加载),所以在配置文件中开启延迟加载后,无需在映射文件中再做配置
一对一:返回一个结果,有一个 association 标签
一对多:返回一个结果对象,有每个结果映射有 collection 标签
多对一:返回一个 LIst 对象,映射结果有 一个 association 标签
多对多:返回一个 List 对象,映射结果有一个 collection 标签
一对一方式association
- 使用级联
<resultMap type="PersonResult" id="PersonMap">
<id property="id" column="id" />
<result property="name" column="name" />
<!-- 一对一关联:单向。方式零:使用级联属性 -->
<result property="idCard.id" column="cid"/>
<result property="idCard.number" column="number"/>
<result property="idCard.expiredTime" column="expired_time"/>
</resultMap>
- 使用拓展类,在子类将需要组装的属性拆分为单个属性,重写 setter 方法以达成对关联对象的组装
<resultMap type="PersonVO" id="PersonMap">
<id property="id" column="id" />
<result property="name" column="name" />
<!-- 一对一关联:单向。方式一:使用扩展类,必须重写setter方法,并且父类必须将字段修改成protected,同时修改type。不推荐 -->
<result property="cardId" column="cid" />
<result property="cardNumber" column="number" />
<result property="cardExpiredTime" column="expired_time" />
</resultMap>
- 使用association内联
<resultMap type="PersonResult" id="PersonMap">
<id property="id" column="id" />
<result property="name" column="name" />
<!-- 一对一关联:单向。方式二:使用内联方式直接列出。 -->
<association property="idCard" column="idcard_id" javaType="IdCard">
<id column="cid" property="id" />
<result column="number" property="number" />
<result column="expired_time" property="expiredTime" />
</association>
</resultMap>
- 使用 association 引用 resultMap
<resultMap type="PersonResult" id="PersonMap">
<id property="id" column="id" />
<result property="name" column="name" />
<!-- 一对一关联:单向。方式三:使用resultMap引用。 注意的是column名称必须与关联表select时的一致 -->
<association property="idCard" column="cid" resultMap="IdCardMap" />
</resultMap>
- 使用单表查询,不用 inner 查询(以避免再次查询,可以利用延迟加载)
<resultMap type="PersonResult" id="PersonMap">
<id property="id" column="id" />
<result property="name" column="name" />
<!-- 一对一关联:单向。方式四:使用select引用,可以设置延迟加载方式 -->
<association property="idCard" column="idcard_id"
javaType="IdCard"
//这个select意思是,当要拥戴idCard对象时,再到IdcardMapper接口中去查找填充
select="com.dyq.mybatis.mapper.IdCardMapper.selectById"
fetchType="lazy"/>
</resultMap>
<select id="selectById" parameterType="Integer" resultMap="PersonMap">
select id, name, idcard_id from t_person p where p.id=#{id}
</select>
多对多collection
//select 引用方式,可以延迟加载
<collection property="courses" column="sid"
ofType="Course"
select="com.sunwii.mybatis.mapper.CourseMapper.selectByStudent"
fetchType="lazy" />
<collection property="categories" ofType="com.mybatis.entity.Category">
<id property="cid" column="cid" />
<result property="cname" column="cname" />
</collection>
一级缓存,二级缓存
一级缓存:本地缓存,会话层面(SqlSession),默认开启,基于 PerpetualCache 的 HashMap 本地缓存,当Session flush 或 close 之后,该 session 中的所有 Cache 就将清空
localCacheScope=STATEMENT
关闭一级缓存
二级缓存:默认也采用 PerpetualCache,HashMap 存储,不同在于作用域为 Mapper(Namespace),并且可以自定义存储源,如 Ehcache。mapper.xml文件中使用<cache/>
进行开启,使用二级缓存需要实现 Serializable 序列化接口。
缓存更新机制:当某一个作用域(一级缓存 Session/二级缓存 Namespace)进行了 C/UD 操作后,默认将作用域下的所有 select 中的缓存将被 clear。要执行sqlsession.commit()
之后才清理
全局二级缓存加 <setting name="cacheEnabled" value="true"/>
(默认开启)。需要使用的 mapper.xml 文件中 加上 <cache/>
,需要实现 Serializable 序列化接口。
二级缓存可以通过select
标签 useCache=false
关闭缓存,其他SQL标签flushCache=false
关闭更新后的缓存刷新。sqlSession.clearCache()
清除一级缓存
先从二级缓存中获取,再从一级缓存中获取
二级缓存属性
eviction:表示缓存回收策略,默认是LRU
- LRU:最近最少使用的,移除最长时间不被使用的对象
- FIFO:先进先出,按照对象进入缓存的顺序来移除
- SOFT:软引用,移除基于垃圾回收器状态和软引用规则的对象
- WEAK:弱引用,更积极地移除基于垃圾收集器状态和弱引用规则的对象
flushInternal:刷新间隔,单位毫秒
- 默认情况是不设置,也就是没有刷新间隔,缓存仅仅调用语句时刷新
size:引用数目,正整数
- 代表缓存最多可以存储多少个对象,太大容易导致内存溢出
readonly:只读,true/false
- true:只读缓存,会给所有调用这返回缓存对象的相同实例,因此这些对象不能被修改。
- false:读写缓存,会返回缓存对象的拷贝(序列化实现),这种方式比较安全,默认值
如何封装对象并返回
使用 <resultMap>
标签,逐一定义数据库列名和对象属性名之间的映射关系
第二种使用 sql 列的别名功能,将列名书写为属性名,或者写为下划线命名法,开启驼峰规则,自动映射
有了列名与属性名的映射关系后,Mybatis 通过反射创建对象,同时使用反射给对象的属性逐一赋值并返回。找不到映射关系的属性,无法完成赋值
动态Sql
9中动态SQL标签:trim,where,set,foreach,if,choose,when,otherwise,bind
if
<select id="getEmpByCondition" resultType="cn.tulingxueyuan.bean.Emp">
select * from emp where
<if test="empno!=null">
empno > #{empno} and
</if>
<if test="ename!=null">
ename like #{ename} and
</if>
<if test="sal!=null">
sal > #{sal}
</if>
</select>
where
子元素只有值才添加 where 语句,会自动去除子句开头的 AND 或者 OR
<select id="getEmpByCondition" resultType="cn.tulingxueyuan.bean.Emp">
select * from emp
<where>
<if test="empno!=null">
empno > #{empno}
</if>
<if test="ename!=null">
and ename like #{ename}
</if>
<if test="sal!=null">
and sal > #{sal}
</if>
</where>
</select>
trim
trim截取字符串:
prefix:前缀,为sql整体添加一个前缀
prefixOverrides:去除整体字符串前面多余的字符,如写 “and” ,当SQL 开头为 and 时 会清除该 and
suffixOverrides:去除后面多余的字符串
<select id="getEmpByCondition" resultType="cn.tulingxueyuan.bean.Emp">
select * from emp
<trim prefix="where" prefixOverrides="and" suffixOverrides="and">
<if test="empno!=null">
empno > #{empno} and
</if>
<if test="ename!=null">
ename like #{ename} and
</if>
<if test="sal!=null">
sal > #{sal} and
</if>
</trim>
</select>
foreach
foreach 是对集合进行遍历
collection = “deptnos” 指定要遍历的集合
close="" 表示以什么结束
index="" 给定一个索引值
item="" 遍历的每一个元素的值
open="" 表示以什么开始
separator="" 表示多个元素的分隔符
<select id="getEmpByDeptnos" resultType="Emp">
select * from emp where deptno in
<foreach collection="deptnos" close=")" index="idx" item="deptno" open="(" separator=",">
#{deptno}
</foreach>
</select>
choose、when、otherwise
有点像 Switch 语句
<select id="getEmpByConditionChoose" resultType="cn.tulingxueyuan.bean.Emp">
select * from emp
<where>
<choose>
<when test="empno!=null">
empno > #{empno}
</when>
<when test="ename!=null">
ename like #{ename}
</when>
<when test="sal!=null">
sal > #{sal}
</when>
<otherwise>
1=1
</otherwise>
</choose>
</where>
</select>
set
<update id="updateEmpByEmpno">
update emp
<set>
<if test="empno!=null">
empno=#{empno},
</if>
<if test="ename!=null">
ename = #{ename},
</if>
<if test="sal!=null">
sal = #{sal}
</if>
</set>
<where>
empno = #{empno}
</where>
</update>
bind
在 OGNL 表达式以外创建一个变量,将其绑定到当前的上下文。
_parameter
代表的就是一个参数对象,getTitle()
是getter 方法,也可直接使用 title
<select id="selectBlogsLike" resultType="Blog">
<bind name="pattern" value="'%' + _parameter.getTitle() + '%'" />
SELECT * FROM BLOG
WHERE title LIKE #{pattern}
</select>
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>
常用 OGNL 表达式
e1 or e2
e1 and e2
e1 == e2,e1 eq e2
e1 != e2,e1 neq e2
e1 lt e2:小于
e1 lte e2:小于等于,其他gt(大于),gte(大于等于)
e1 in e2
e1 not in e2
e1 + e2,e1 * e2,e1/e2,e1 - e2,e1%e2
!e,not e:非,求反
e.method(args)调用对象方法
e.property对象属性值
e1[ e2 ]按索引取值,List,数组和Map
@class@method(args)调用类的静态方法
@class@field调用类的 静态字段值
like 语句怎么写
使用 bind,使用 "%"#{questtion}"%"
必须使用 “” ,使用CONCAT('%', #{questtion}, '%')
- 代码种拼写,传入xml
string wildcardname = “%smi%”;
list<name> names = mapper.selectlike(wildcardname);
<select id=”selectlike”>
select * from foo where bar like #{value}
</select>
- 在xml中拼接,有SQL注入风险
//会引起SQL注入
string wildcardname = “smi”;
list<name> names = mapper.selectlike(wildcardname);
<select id=”selectlike”>
select * from foo where bar like "%"${value}"%"
</select>
主键回填
主键自增长(其他主键策略在 java 代码中生成主键)
-
使用
useGeneratedKeys="true"
<insert id="insertBook" useGeneratedKeys="true" keyProperty="id"> insert into t_book (b_name,author) values (#{name},#{author}); </insert>
-
利用MySQL 自带的
last_insert_id()
查询刚插入的 id<insert id="insertBook"> <selectKey keyProperty="id" resultType="java.lang.Integer" order="AFTER"> SELECT LAST_INSERT_ID() </selectKey> insert into t_book (b_name,author) values (#{name},#{author}); </insert>
selectKey:keyProperty,语句结果设置的对象
order,可以为BEFORE 或 AFTER,Before 是先进行设置后执行insert语句,After相反
主键策略
数据库支持自增(mysql)与不支持自增(oracle)
<!--如果数据库支持自增可以使用这样的方式-->
<insert id="insertUser" useGeneratedKeys="true" keyProperty="id">
insert into user(user_name) values(#{userName})
</insert>
<!--如果数据库不支持自增的话,那么可以使用如下的方式进行赋值查询-->
<insert id="insertUser2" >
<selectKey order="BEFORE" keyProperty="id" resultType="integer">
select max(id)+1 from user
</selectKey>
insert into user(id,user_name) values(#{id},#{userName})
</insert>
Mapper 编写有几种方式
- 接口实现类继承 SqlSessinDaoSupport:需要编写 mapper 接口,mapper接口实现类,mapper.xml 文件
- 在 sqlMapConfig.xml 中配置 mapper.xml 的位置
<mappers> <mapper resource="mapper.xml文件的地址" /> <mapper resource="mapper.xml文件的地址" /> </mappers>
- 定义mapper接口
- 实现类继承 SqlSessionDaoSupport
- mapper方法中可以
this.getSqlSession()
进行数据增删改查。
- spring 配置
<bean id=" " class="mapper接口的实现"> <property name="sqlSessionFactory" ref="sqlSessionFactory"></property> </bean>
- 使用
org.mybatis.spring.mapper.MapperFactoryBean
:
- (1)在 sqlMapConfig.xml 中配置 mapper.xml 的位置,如果 mapper.xml 和 mappre 接口的名称相同且在同一个目录,这里可以不用配置
<mappers> <mapper resource="mapper.xml文件的地址" /> <mapper resource="mapper.xml文件的地址" /> </mappers>
定义mapper接口:
2. mapper.xml 中的 namespace 为 mapper 接口的地址
2. mapper 接口中的方法名和 mapper.xml 中的定义的 statement 的 id 保持一致
3. Spring 中定义<bean id="" class="org.mybatis.spring.mapper.MapperFactoryBean"> <property name="mapperInterface" value="mapper接口地址" /> <property name="sqlSessionFactory" ref="sqlSessionFactory" /> </bean>
- 使用 mapper 扫描器
- mapper.xml 文件编写:
- mapper.xml 中的 namespace 为 mapper 接口的地址;
- mapper 接口中的方法名和 mapper.xml 中的定义的 statement 的 id 保持一致;
- 如果将 mapper.xml 和 mapper 接口的名称保持一致则不用在 sqlMapConfig.xml 中进行配置。
- 定义 mapper 接口:
- 注意 mapper.xml 的文件名和 mapper 的接口名称保持一致,且放在同一个目录
- 配置mapper扫描器:
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="mapper接口包地址"></property> <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/> </bean>
- 使用扫描器后从 spring 容器中获取 mapper 的实现对象。
Dao接口(Mapper接口)与 Xml 映射文件的对应关系
调用接口方法时,接口全限名+方法名拼接为 key,可唯一定位一个 MappedStatement,可以唯一找到该 namespace下与 key 的方法名相同的 MappedStatement(每一个<select> <insert> <update>
都会解析为一个 MappedStatement对象)。
接口工作原理是 JDK 动态代理,运行时会为 Dao 接口生成代理 Proxy对象,代理对象 Proxy会拦截接口方法,转而执行 MappedStatement 所代表的 sql,然后将 SQL执行结果返回