1 Mybatis
1.1 mybatis
-
原始SQL编程复杂,依赖数据库,移植性差,功能欠缺
-
支持定制化SQL、存储过程和高级映射的数据持久层框架,消除JDBC样板代码;二级缓存机制不佳
-
关键类和执行过程/生命周期:
-
编写并加载全局配置文件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> <!-- 配置全局属性 --> <typeAliases alias="类别名" type="类"></typeAliases> <!-- 当与Spring结合时选择使用Spring的配置 --> <enviroments>数据源等</enviroments> <settings> <!-- 使用JDBC的getGeneratedKeys获取数据库自增主键策略;插入bean时,数据库id自增而bean的id值为null,查询时不为null;需要指明主键和启用 --> <setting name="useGeneratedKeys" value="true"/> <!-- 使用列标签替换列别名 --> <setting name="useColumnLabel" value="true"></setting> <!-- 开启驼峰命名转换 --> <!-- 查询数据时,若某个字段在数据库有值,而查询出来为null,很可能就是因为字段名和属性名对应不上 --> <setting name="mapUnderscoreToCamelCase" value="true" /> </settings> <!-- 注册mappper类 --> <mappers></mappers> </configuration>
// 读取配置文件 InputStream configFile = new FileInputStream(filePath); InputStream configFile = Resource.getResourceAsStream(filePath);
-
SqlSessionFactoryBuilder:读取配置文件,使用工厂创建对象;属于method/request级别,即方法返回后消失;作用是build()中解析全局配置文件;解析完会产生全局单例configuration;该bean是管理配置文件信息的容器
-
SqlSessionFactory,SqlSession:使用工厂创建对话和核心对象;SqlSessionFactory为Application级别;SqlSession为request级别;作用是创建会话;openSession()获取会话;过程中创建了事务工厂、执行器,最后返回DefaultSqlSession核心对象,但非线程安全,整合Spring后要用其代理类;封装操作数据库的方法
// 调用SqlsessionFactoryBuilder对象的build(inputStream)创建XMLConfigBuilder对象(inputStream读取配置文件作输入流);Builder设计模式;新对象调用parse()解析xml文件(转为document对象,解析DOM节点Node即标签<...>,封装),得到Configuration对象 SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configFile); // 新对象创建DefaultSessionFactory/SqlSessionFactory对象;与Spring整合后由SqlSessionTemplate生成 SqlSession sqlSession = sqlSessionFactory.openSession(true); // SqlSession:默认一级缓存:session级别的缓存,即在同一个session中查询,再做相同查询时从缓存中获取数据,sql仅一条;不同session中执行相同查询,则各自查询数据库即二级缓存;查询之间有增删改操作则不视为相同查询,即增删改会自动flush缓存
-
- Mapper,MapperProxy:调用动态代理;属于request级别;作用是调用MapperProxy.invoke()动态代理
-
核心映射器配置:
-
解决问题:
-
传统JDBC底层没有用连接池,数据库操作会频繁创建和关闭,消耗资源
-
修改SQL繁琐且需要整体编译,不利于维护
-
-
ORM用于实现不同类型系统的数据之间的切换;如bean中为id类型Long,切换到数据库为bigint
-
1.2 Mapper与动态SQL
- mybatis读取mapper文件的四种方式:< mappers>< mapper resource/url/class/name="userMappper.xml">< /mapper>< /mappers>
<!-- name为绑定到上下文的变量名,value为OGNL表达式 --> <if test="userName != null and userName != ''"> <bind name="nameLike" value="'%' + userName + '%'"/> and user_name like #{nameLike} </if>
-
编写DaoMapper接口及对应的.xml:
<!-- namespace指定扫描对应包下的Bean --> <mapper namespace="命名空间,类对象路径"> <!-- 封装表名,后面所有需要表名的地方都引用tableName,如select from <include refid="tableName" /> --> <sql id="tableName">表名</sql> <!-- 封装有id和无id的所有字段共后面引用,如select <include refid="all_columns" --> <sql id = "all_columns"> id, name, ... </sql> <!-- 封装参数映射值 --> <sql id="params"> #{id}, #{name}, ... </sql> <!-- 封装返回结果,resultMap自定义映射结果(高级映射);当bean属性名与表字段名不一致时,也可用此建立映射关系,属性上使用@Column --> <!-- 注意查询返回List<Map>类型数据时,返回结果要写为resultType="map"才能映射到 --> <resultMap id="beanResult" type="bean路径"> <result property="bean属性名" column="表字段名" jdbcType="字段类型"></result> ... </resultMap> <!-- 执行<insert><update><select><delete>等statement --> <!-- 查;注意查询的动态SQL语句中没有逗号 --> <select id="对应dao层中方法名" parameterType="传入参数的类型;如果值为int或Integer,表示Integer,即int是Integer的别名;而_int表示int" resultType="返回结果类型,如果是resultMap,则可能='beanResult'"> select <include refid="all_columns"/> from <include refid="tableName"/> left join 表名 <if test="userName!=null and userName!=''"> on 表.user_name = 表.user_name </if> where 1=1 <if test="id!=null"> and id = #{id} </if> <!-- 参数条件查询;判断参数是否为空;前位为表字段名,后为参数名/属性名 --> <if test="userName!=null and userName!=''"> and user_name = #{userName} </if> <!-- 当参数类型为时间类型 --> <if test="startTime!=null and startTime!='' and endTime!=null and endTime!=''"> and time >= #{startTime} and time <=(小于符号在xml中被解析为左键括号,可写为<) #{endTime} <!-- 有时候需要使用$直接获取值,如 --> and time > ${startTime} </if> <!-- 模糊查询 --> <if test="userName!=null and userName!=''"> and user_name like '${userNmame}%' and user_name like CONCAT('%', #{userName}, '%') </if> <!-- 传入一个参数,做所有字段的条件匹配 --> <if test="keyWord!=null and keyWord!=''"> and user_name like concat(#{keyWord}, %) </if> <if test="keyWord!=null and keyWord!=''"> and user_age like concat(#{keyWord}, %) </if> ... order by time desc </select> <!-- 增 --> <insert id="add" parameterType="beanName"> insert into <include refid="tableName"/> (<include refid="all_columns"/>) values (<include refid="all_columns") </insert> <!-- 批量增 --> <insert id="add" parameterType="list"> insert into <include refid="tableName"/> (<include refid="all_columns"/>) values <foreach collection="beanList" item="item" open="(" separator="," close=")"> (<include refid="all_columns"/>) </foreach> </insert> <!-- 删 --> <delete id="delete" parameterType="String"> delete from <include refid="tableName"/> where id = #{id} <!-- 批量删除操作;parameterType="String[]";foreach用来循环collection --> <!-- collection:指定传入的需要循环的参数名,可取array,list,map item:为循环中每个元素指定别名;index:下标 open:循环开始;close:循环结束;separator:元素之间的分隔符 --> where id in <foreach collection="ids" item="id" open="(" separator="," close=")"> #{id} </foreach> </delete> <!-- 改;注意修改的动态SQL语句中有逗号,且最后一条set没有逗号 --> <update id="update" parameterType="beanName"> update <include refid="tableName"/> <set> <if test="userName!=null and userName!=''"> user_name = #{userName}, </if> ... </set> where id = #{id} </update> </mapper>
-
Bean中不再使用关联对象的属性即外键,而直接将关联对象做为属性
-
< association property="关联对象变量名" column="查询关联对象的列名一般为id" javaType="关联对象" select="关联对象对应mapper.xml中对应查询sql语句的id全名">:一对一/多对一,对应java的set或list
-
-
<collection>:一对多;javaType=”集合/数组 ”,ofType=”关联对象”;连表查询
-
鉴别器:根据表中某个字段区别数据,将查询结果自动封装成不同类型的对象;父类如People,根据属性sex鉴别子类Male和Famale;父类分类查询时,< resultMap>下使用鉴别器< discrimitor column="鉴别列">,其下< case value="鉴别列的值1…" resultType="子类">< reslut column="子类自有的列名,即另一个子类中没有的属性和数据库中值为null的列" property="属性名">
-
-
动态SQL语句:基于OGNL表达式,<where><if><set><choose><when><otherwise><trim><foreach>;< foreach>一般放在in条件语句中,批量删除>;动态sql在条件查询时即可修改特定列(另一种方式先查询再修改,则执行了两次数据库操作);不能省略“and”或"or
-
根据时间查询:报错Error querying database. ...DateTime and java.lang.String;原因是MyBatis的xml文件中写的if为空判断类型出错,把日期类型与空串作比较,代码片段如下:<if test="createTime != null and createTime != ''">;解决:createTime为日期类型,去掉 and createTime != ''
-
IN语句的使用:在SQL中可以直接支持如:where userName IN ('A', 'B')或IN (A,B);但Mybatis中传入userName="A,B";再用where userName IN (#{userName})获取,则不支持;可简单替换为$,使其直接替换为IN (A,B)形式,但这无法反防止SQL注入问题,最好还是用# + 集合遍历的方式:方法传参分别是List,Map,String[]等集合类型;遍历 WHERE id IN <foreach collection="list\array" item="id" index="index" open="(" close=")" separator=","> #{id} </foreach>
-
动态查询:
-
<if>:and关系,符合条件的选项都匹配
-
<choose-when-otherwise>:or关系;从多个选项中选择一个;when列表依次执行,test一旦有一个成立,则执行其下的SQL且choose结束;都不成立则执行otherwise;类似if..else if...else;
-
<trim>:格式化标签,可以完成<set>或<where>标记的功能。参数:prefix:给SQL添加前缀;prefixOverrides:去掉SQL中的第一个指定值;suffix:添加后缀;suffixOverrides:去掉最后一个指定值
-
<bind>标签用法:不同数据库的Concat函数用法不一样,如Mysql的有3个参数,Oracle的有2个;为了避免这条SQL在不同环境中运行出错,使用ONGL(Object-Graph Navigation Language, 对象图形化导航语言)表达式创建一个变量并将其绑定到上下文中
-
过往实践:动态SQL与Group by和select的集合运用,以及去掉首尾的某些符号:
SELECT count(1) AS number <choose> <when test="param.startTime != null and param.endTime != null"> , DATE_FORMAT(create_time, '%Y-%m-%d') AS createTime </when> <when test=""> <if test="param.eventcaseName != null and param.eventcaseName != ''"> , eventcase_name </if> <if test="param.eventcaseLabel != null and param.eventcaseLabel != ''"> , eventcase_label </if> ...... </when> <otherwise> , event_type </otherwise> </choose> FROM ( SELECT * FROM <include refid="table" /> where 1 = 1 <if test="param.eventcaseName != null and param.eventcaseName != ''"> and eventcase_name like concat('%', #{param.eventcaseName}, '%') </if> <if test="param.eventcaseLabel != null and param.eventcaseLabel != ''"> and eventcase_label like concat('%', #{param.eventcaseLabel}, '%') </if> ...... ) AS A GROUP BY <choose> <when test="param.startTime != null and param.endTime != null"> DATE_FORMAT(A.create_time, '%Y-%m-%d'); </when> <when test=""> <trim prefixOverrides=","> <if test="param.eventcaseName != null and param.eventcaseName != ''"> , eventcase_name </if> <if test="param.eventcaseLabel != null and param.eventcaseLabel != ''"> , eventcase_label </if> ...... </trim> </when> <otherwise> event_type </otherwise> </choose>
-
-
-
idea不会编译src下的java目录下的xml文件,所有mapper.xml需要放在resources下
-
业务中调用接口的增删改方法时需要session.commit()事务提交,或者使用注解开启事务支持,才会保存数据到DB
-
编写SQL:
-
注解方式:
-
编写带有SQL相关注解和SQL语句的mapper类
-
编写mybatis相关配置类,如配置当数据库列名和实体类属性名不一样(如驼峰命名和_连接)
-
使用@Configuration添加一个重写了ConfigurationCustomizer类中方法的@Bean
-
当mapper类太多时,可在启动类上使用@MapperScan指定扫描包,批量操作
@Mapper @Component public interface EmployeeMapper { @Select("select * from employee where id = #{id}") public Employee queryEmployeeById(Integer id); @Select("select * from Employee") public List<Employee> queryEmployees(); @Delete("delete from employee where id = #{id}") public int deleteEmployeeById(Integer id); // 使用自增长策略,指定主键 @Options(useGeneratedKeys = true, keyProperty = "id") @Insert("insert into employee(name) values(#{name})") public int addEmployee(Employee employee); @Update("update employee set name = #{Employee} where id = #{id}") public int updateEmployee(Employee employee); }
-
SQL-XML映射方式(推荐,支持更复杂的动态SQL)
-
-
-
$:
-
配合模糊查询,纯粹的字符串替换后显示
-
一般用于传入数据库对象,如参数为表名、列名或order by时;参数是字符串时用引号
-
select * from user where name = ${name},解析为:select * from user where name = 实际值,此时如果恶意构造值为name = 'a' or 1 = 1,直接替换后为:select * from user where name = 'a' or 1 = 1;就形成了SQL注入的安全问题
-
-
#:推荐
-
在动态解析/预编译时以安全值如?填充,然后调用PreparedStatement的set()赋值,传入的字符串被添加单引号;防止SQL注入;预编译效率高
-
select * from user where name = #{name},解析为:select * from user where name = ?,在代码中由参数值来初始化?的实际值;此时如果恶意构造值为name = 'a' or 1 = 1,预编译后为select * from user where name = " 'a' or 1 = 1";此时检查不通过
-
1.3 分页
-
逻辑分页:查询所有数据,然后根据页码选出数据显示;参数currentPage和pageSize;offset和limit
-
物理分页:判断出应该选的数据并查询
-
PageHelper:分页插件
-
引入依赖
-
在mybaitsConfig.xml中配置拦截器插件;或者使用Spring配置方式(略);SpringBoot似乎只需引入依赖即可?
<plugins> <plugin interceptor="com.github.pagehelper.PageHelper"> <property name="dialect" value="mysql"/> <!-- 该参数默认为false;设置为true时,会将RowBounds第一个参数offset当成pageNum页码使用 --> <!-- 和startPage中的pageNum效果一样--> <property name="offsetAsPageNum" value="true"/> <!-- 该参数默认为false;设置为true时,使用RowBounds分页会进行count查询 --> <property name="rowBoundsWithCount" value="true"/> <!-- 设置为true时,如果pageSize=0或者RowBounds.limit = 0就会查询出全部的结果 --> <!-- (相当于没有执行分页查询,但是返回结果仍然是Page类型)--> <property name="pageSizeZero" value="true"/> <!-- 3.3.0版本可用 - 分页参数合理化,默认false禁用 --> <!-- 启用合理化时,如果pageNum<1会查询第一页,如果pageNum>pages会查询最后一页 --> <!-- 禁用合理化时,如果pageNum<1或pageNum>pages会返回空数据 --> <property name="reasonable" value="true"/> </plugin> </plugins>
-
在Service层中,调用Dao层执行查询之前使用,紧接着的第一个select的SQL将执行分页
// Mapper接口方式调用 Page<Object> objects = PageHelper.startPage(1, 10); // RowBounds方式调用 List<Object> objects = sqlSession.selectList("x.y.selectIf", null, new RowBounds(1, 10)); // 把objects封装到PageInfo,便可以使用其中的属性,如pageNum,pageSize,size,startRow,endRow,pages,prePage,nextPage,isFirstPage,isLastPage,hasNextPage... PageInfo<Object> objectPageInfo = new PageInfo<>(objects);
-
-
@PageableDefault:SpringJPA提供的分页注解
public Page<Object> queryByPage(@PageableDefault(value = 10, sort = {"id"}, direction = Sort.Direvtion.DESC) Pageable pageable) { // pageable传入SQL调用的方法中,该对象包含了分页信息,如pageSize }
1.4 整合Spring/SpringBoot
-
Mybatis已经能操作数据库了,但与Spring整合,将类管理交给容器,省略掉SqlSession等相关代码,如Spring配置好数据源,< bean>定义SqlSessionFactoryBean并注入数据源属性即可;事务跟从Spring
-
整合思路:将mybatis的配置改写到spring的ApplicationContext.xml
-
SqlSessionFactoryBean:封装SqlSessionFactory创建的代码;< bean>注册到容器,< property>注入各种属性
-
MapperScannerConfigure:封装SqlSession创建和bean获取的代码;bean的id值为类首字母小写
<bean id="ssf" class="SqlSessionFactoryBean"> <property name="dataSource" ref="指定dataSource" /> <property name="typeAliasesPackage" value="映射的实体" /> <property name="mapperLocations" "映射的mapper文件,如classpath:com../*Mapper.xml" /> </bean> <bean id="scaner" class="MapperScannerConfigure"> <property name="sqlSessionFactoryBeanName" value="ssf" /> <property name="basePackage" value="扫描dao接口所在的包" /> </bean>
-
-
引入连接Spring的jar:mybatis-spring
-
原始mybatis创建数据连接时设置Connection.setAutoCommit(false),即需要手动操作事务的开关;整合后Conncetion交由连接池管理,连接池保持了事务自动提交机制
-
详解:SSM框架——详细整合教程(Spring+SpringMVC+MyBatis)_AndyLizh的专栏-CSDN博客_ssm框架
-
TooManyResultsException:mybatis异常,期望获得一个结果,但查询出了多个;方法返回类型与SQL执行结果不匹配
-
ReflectionExcepton:There is no getter for property named:dao层方法参数与mapper中不一致;可用@Param限制参数名
- 整合SpringBoot:
- 在application.yml中配置Mybatis相关设置
- 日志打印Mybatis的SQL语句:在application.yml中配置logging-level:
logging: level: com.xxx.xxx.*: debug # 此路径为Mybatis对应的方法(一般为dao或mapper接口)所在的包,而不是mapper.xml所在的包
1.5 mybaits-generatorConfiguration
-
generator插件:dao层与mybatis.xml文件的映射
1.6 Free mybatis plugin
-
dao层方法与xml文件中方法快速跳转;mybatis自动补全和语法错误提示
-
Plugins中搜索Free mybatis plugin,install即可
1.7 缓存
-
默认开启一级缓存(不用配置),不开启二级缓存(开启需要配置;spring项目默认也开启了二级缓存,但需要配置才能使用)
-
优先查询二级缓存,没有的话再查询一级缓存,最后查询数据库
-
一级缓存:
-
具有和SqlSession相同的的生命周期,SqlSession相当于一个JDBC的Connection对象,在一次请求事务会话后其关闭
-
-
二级缓存:
-
相同的Mapper类,相同的sql语句,不同的SqlSession却会查询数据库两次,需要二级缓存解决
-
作用域为Mapper级别,即可以在SqlSessionFactory层面上(全局单例,对应一个数据库连接池,应用启动后一直存在)能提供给各个SqlSession共享
-
执行相同namespace下的mapper映射文件并执行commit操作时,会清空该namespace下的二级缓存
-
-
配置:< cache>标签,属性包括:
-
eviction="LRU"
-
缓存回收策略,包括:
-
LRC:最近最少使用策略,移除最长时间不使用的对象
-
FIFO:先进先出策略
-
SOFT:软引用,移除基于垃圾回收器的状态和软引用规则的对象
-
WEAK:弱引用,更积极地移除基于垃圾回收期的状态和弱引用规则的对象
-
-
-
flushInterval="100000":刷新间隔时间
-
size="512":引用数目,代表缓存最多可以储存多少个对象
-
readOnly="true":只读,意味着缓存数据只能读取,不能修改,好处是可以快速读取缓存,缺点是没有办法修改缓存,默认值false(可读/可写)
-
-
自定义缓存:Redis,RedisTemplate
-
内置缓存机制不常用,因为缓存结果要存储在当前服务器的JVM中,数据量过大会造成jvm服务器臃肿缓慢,大量占用内存空间
-
将缓存的数据从tomcat服务器上转移到redis服务器上,redis存取速度快,并且是一个独立进程和jvm没有关系
-