Loading... 进阶版来了!前期回顾看这里: <div class="preview"> <div class="post-inser post box-shadow-wrap-normal"> <a href="http://www.tangsong.fun/index.php/Mybatis.html" target="_blank" class="post_inser_a no-external-link no-underline-link"> <div class="inner-image bg" style="background-image: url(https://blog-picture01.oss-cn-shenzhen.aliyuncs.com/img/20210606201052.JPG);background-size: cover;"></div> <div class="inner-content" > <p class="inser-title">[Mybatis]回顾与优化</p> <div class="inster-summary text-muted"> MySQL5与MySQL8的配置差异#mysql5 mysql.driver=com.mysql.jdbc.Dri... </div> </div> </a> <!-- .inner-content #####--> </div> <!-- .post-inser ####--> </div> <!--more--> ## 多环境切换 当用多个数据库存放不同资源的时候,需要配置多数据源。 ### Mybatis版 conf.xml ```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> <!-- 引用db.properties配置文件 --> <properties resource="db.properties"/> <!--default指定環境 --> <environments default="devOracle"> <!--oracle --> <environment id="devOracle"> <transactionManager type="JDBC" /> <!-- 配置数据库连接信息 --> <dataSource type="POOLED"> <property name="driver" value="${oracle.driver}" /> <property name="url" value="${oracle.url}" /> <property name="username" value="${oracle.username}" /> <property name="password" value="${oracle.password}" /> </dataSource> </environment> <!--mysql --> <environment id="devMysql"> <transactionManager type="JDBC" /> <!-- 配置数据库连接信息 --> <dataSource type="POOLED"> <property name="driver" value="${mysql.driver}" /> <property name="url" value="${mysql.url}" /> <property name="username" value="${mysql.username}" /> <property name="password" value="${mysql.password}" /> </dataSource> </environment> </environments> <!-- 配置数据库支持类--> <databaseIdProvider type="DB_VENDOR"> <property name="MySQL" value="mysql" /> <property name="Oracle" value="oracle" /> </databaseIdProvider> </configuration> ``` db.properties ```xml #oracle oracle.driver=oracle.jdbc.OracleDriver oracle.url=jdbc:oracle:thin:@127.0.0.1:1521:ORCL oracle.username=scott oracle.password=tiger #mysql mysql.driver=com.mysql.jdbc.Driver mysql.url=jdbc:mysql://localhost:3306/mydb?allowMultiQueries=true mysql.username=root mysql.password=root ``` xxxMapper ```xml <select id="queryStudentByNo" resultType="com.yanqun.entity.Student" parameterType="int"> select * from student where stuNo = #{stuNo} <if test="_databaseId == 'oracle'"> select * from student where stuNo = #{_parameter} </if> <if test="_databaseId == 'mysql'"> select * from student where stuNo != #{_parameter} </if> </select> <insert id="addStudentOracle" databaseId="oracle"> <foreach collection="list" open="begin" close="end ;" item="student"> insert into student(stuno,stuname) values(#{student.stuNo},#{student.stuName}) ; </foreach> </insert> <insert id="addStudentMySql" databaseId="mysql"> insert into student(stuno,stuname) values <foreach collection="list" item="student" separator="," close=";" > (#{student.stuNo},#{student.stuName}) </foreach> </insert> ``` ### SpringBoot版 和SSM的设计目录差不多,都是一个Controller、一个DataSourceConfig、一个Entity、两个Service以及两个Mapper **步骤**大致为: yml中配置两个数据源、通过注解配置DataSourceConfig、以类方式配置两个数据源,选择主从、Mapper的sql中指定使用哪个数据源。 参考地址:https://blog.csdn.net/weixin_39835887/article/details/84921565 上述的场景实际开发中还没碰到过,就只记录了学习到的知识。后面碰到再补上吧。 --- ## MySQL/Oracle自增问题 **细节:**int默认值是0,Integer默认值是NULL,所以在entity定义的时候一般都用Integer这个包装类 ### MySQL mysql支持自增,所以只需要多配置两个属性即可: ```xml <!--useGeneratedKeys使用主键生成器,keyProperty新增后回写的属性值--> <insert id="insert" keyProperty="id" useGeneratedKeys="true"> insert into test.user(name, age) values (#{name}, #{age}) </insert> ``` ### Oracle Oracle不支持自增,需要通过序列模拟实现。执行自增sql的前提条件是先生成一个自增序列,其中序列指针在start前,`nextval`为下一值,`currval`为当前值: `create sequence myseq increment by 1 start with 1;` ```xml <!--方式一(推荐)--> <insert id="addStudent" parameterType="com.yanqun.entity.Student" databaseId="oracle"> <!--order="BEFORE"是先执行这句话,保证keyProperty将nextval回写入stuNo--> <selectKey keyProperty="stuNo" resultType="Integer" order="BEFORE"> select myseq.nextval from dual </selectKey> insert into student(stuno,stuName,stuAge,graName) values(#{stuNo} , #{stuName},#{stuAge},#{graName}) </insert> <!--方式二--> <insert id="addStudent" parameterType="com.yanqun.entity.Student" databaseId="oracle"> <selectKey keyProperty="stuNo" resultType="Integer" order="AFTER"> select myseq.currval from dual </selectKey> insert into student(stuno,stuName,stuAge,graName) values(myseq.nextval , #{stuName},#{stuAge},#{graName}) </insert> ``` --- ## MySQL/Oracle执行的参数为NULL问题 当添加或修改一个属性值(如:StuName)时,如果对应的字段为 `NULL`且数据库设计字段没有非空约束时,MySQL可以正常执行,而Oracle则会报错。 **原因**是对于不同数据库,Mybatis对各种数据类型的默认值也是有点差异的。Mybatis将空值(`jdbcTypeForNull`)判定为默认值 `OTHER`。MySQL底层能自动将 `OTHER`转换成 `NULL`,而Oracle则无法处理。 **解决办法** (1)修改具体的sql标签:`jdbcType=NULL`(当Oracle遇到无法处理的类型时,默认用 `NULL`。该操作不会影响到正常值) ```Java <insert id="addStudent" databaseId="oracle"> insert into student(stuno,stuName) values(#{stuNo} , #{stuName,jdbcType=NULL}) </insert> ``` (2)配置 mybatis全局配置文件conf.xml(**推荐** 空值问题在项目中还是经常遇到的,直接全局配置一劳永逸) ```xml <properties resource="db.properties"/> <!--注意 settings要在properties底下--> <settings> <setting name="jdbcTypeForNull" value="NULL"/> </settings> ``` --- ## Mybatis处理多参问题 sql中传参一般是把参数通过JavaBean的方式写入到对象中,再从对象中提取出属性值,因此要设置一个parameterType。但是如果遇到非要传n个属性值可以通过以下三种方法(不需要设置parameterType): 1. `#{arg0}、#{arg1}、#{arg2}` 2. `#{param1}、#{param2}、#{param3}` 3. 命名参数: `addStudent(@Param("stuNo") Integer stuNo,@Param("stuName") Integer stuName)`<br>`#{stuNo}、#{stuName}` 遇到两种组合出现的情况: `addStudent(@Param("stuNo") Integer stuNo,@Param("student") Student student)` `#{stuNo}、#{student.stuName}` --- ## Mybatis拦截器 拦截各种sql进行更改,比如伪装的概率up,底层直接把概率sql改成0.0001%,然后再设置日志只显示原先sql,或者是几次必得那种直接底层给你累加妥妥的上限更新。 ![多个拦截器的执行顺序](https://s1.ax1x.com/2020/10/23/BAyfXT.png) **编写拦截器、添加注解:** ```Java package com.example.demo.myinterceptors; import org.apache.ibatis.executor.statement.StatementHandler; import org.apache.ibatis.plugin.*; import org.apache.ibatis.reflection.MetaObject; import org.apache.ibatis.reflection.SystemMetaObject; import org.apache.ibatis.session.ResultHandler; import java.sql.Statement; import java.util.Properties; @Intercepts({ //拦截查询 //@Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class}) //拦截参数 @Signature(type = StatementHandler.class, method = "parameterize", args = {Statement.class}) }) public class MyInterceptor implements Interceptor { /** * 拦截方法 * @param invocation * @return * @throws Throwable */ @Override public Object intercept(Invocation invocation) throws Throwable { System.out.println("进入拦截方法 intercept()"); Object target = invocation.getTarget(); System.out.println("目标对象" + target); MetaObject metaObject = SystemMetaObject.forObject(target); Object value = metaObject.getValue("parameterHandler.parameterObject"); System.out.println("getValue:" + value); //强行更改参数值 metaObject.setValue("parameterHandler.parameterObject", 1); Object value2 = metaObject.getValue("parameterHandler.parameterObject"); System.out.println("getValue2:" + value2); //放行,也是执行的机制 Object proceed = invocation.proceed(); System.out.println("放行 proceed() 返回值:" + proceed); return proceed; } /** * 将拦截器中定义的增强功能和原来的核心对象合并为 最终核心对象 * @param target * @return */ @Override public Object plugin(Object target) { Object wrap = Plugin.wrap(target, this); System.out.println("增强四大核心对象:" + wrap); return wrap; } @Override public void setProperties(Properties properties) { System.out.println("xml中plugin设置的属性:" + properties); } } ``` **还可以在xml中配置参数** ```xml <configuration> <plugins> <plugin interceptor="com.example.demo.myinterceptors.MyInterceptor"> <property name="name" value="zs"/> <property name="age" value="23"/> </plugin> </plugins> </configuration> ``` --- ## PageHelper 分页插件 [食用说明](https://github.com/pagehelper/Mybatis-PageHelper/blob/master/wikis/en/HowToUse.md) Demo: ```xml <!--pagehelper Mybatis的分页插件--> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper</artifactId> <version>5.2.0</version> </dependency> ``` ```xml <plugins> <plugin interceptor="com.github.pagehelper.PageInterceptor"> <!-- 输入的跳转页大于总共的则返回第一页 --> <property name="reasonable" value="true"/> </plugin> </plugins> ``` ```Java @GetMapping("selectByPage") public void selectByPage() { //查询第2页,每页3条数据 Page<Object> page = PageHelper.startPage(2, 3); List<Student> students = this.studentService.queryAll(); for (Student student : students) { System.out.println(student); } System.out.println("当前页:" + page.getPageNum()); System.out.println("总数据量:" + page.getTotal()); System.out.println("总页码大小:" + page.getPages()); System.out.println("页面大小:" + page.getPageSize()); //PageInfo功能比Page更多一些,更好的跟前端框架结合使用 PageInfo<Student> pageInfo = new PageInfo<>(students); System.out.println("分页数组:" + Arrays.toString(pageInfo.getNavigatepageNums())); } ``` --- ## 细节 ### 细节一:增删改的返回值问题 增删改一般都是用void,但是可以直接把接口返回值类型修改成Integer、Long、Boolean类型 ### 细节二:职务的手动/自动提交 常规写法一般都是用手动提交,即每次都是: ```Java SqlSession session = sessionFactory.openSession(); ... session.commit();//手动提交 session.close(); ``` 自动提交只需把openSession加一个参数 `true`即可 ```Java SqlSession session = sessionFactory.openSession(true); ... session.close(); ``` **自动提交在整个controller中如果有多个方法则会一直提交,所以还是推荐手动一次性提交。** ### 细节三:批量操作 只需要改动一个地方(十万条20秒变1秒不到) `sessionFactory.openSession(ExecutorType.BATCH);` **`BATCH`**: 预编译SQL一次,其余DML只需要设置参数值即可.(没有BATCH:预编译N次,每次DML都需要执行完整的SQL) ```xml sert id="addStudent" parameterType="student"> insert into student(stuNo,stuName,stuAge,graName) values(#{stuNo} , #{stuName},#{stuAge},#{graName}) </insert> ``` **不推荐:SQL拼接** 1.没有用到mybatis对批量插入的支持 2.不适合数据库迁移 3.如果大量数据,则会将拼接的SQL语句拉的很长,而部分数据库对SQL语句的长度有限制。 **有争议:存储过程** 存储过程(Stored Procedure)是在大型数据库系统中,一组为了完成特定功能的SQL语句集,它存储在数据库中,一次编译后永久有效,用户通过指定存储过程的名字并给出参数(如果该存储过程带有参数)来执行它。存储过程是数据库中的一个重要对象。 **存储过程的优点:** 1、可以减少程序在调用DB时候的信息传输量(其实减少的只有Request的时候,所以减少的时间微乎其微) 2、存储过程是预先优化和预编译的,节省每次运行编译的时间,所以一般情况下认为存储过程的性能是优于sql语句的。 3、对调用者可以隐藏数据库的复杂性,将数据组装的过程封装。(所以不适合多团队开发) 4、参数化的存储过程可以防止SQL注入式攻击,而且可以将Grant、Deny以及Revoke权限应用于存储过程。 5、如果业务开发中,数据人员和业务代码人员是分离的,业务人员可以不用关心数据,直接调用存储过程,更加面向分层开发设计理念。 **存储过程的劣势:** 1、存储过程这种“一次优化,多次使用”的策略节省了每次执行时候编译的时间,但也是该策略导致了一个致命的缺点:可能会使用错误的执行计划。因为有的时候随着数据量的增加或者数据结构的变化,原来存储过程选择的执行计划也许并不是最优的了,需要手动干预或重新编译。 2、存储过程难以调试,虽然有些DB提供了调试功能,但是一般的账号根本就没有那种权限,更何况线上的数据库不可能会给你调试权限的,再进一步就算能调试效果也比程序的调试效果要差很多。 3、可移植性差,当碰到切换数据种类的时候,存储过程基本就会歇菜。 4、如果业务数据模型有变动,存储过程必须跟着业务代码一起更改,如果是大型项目,这种改动是空前的,是要命的。 目前阿里巴巴规范手册中也不推荐存储过程的使用了,上面是写到**存储过程难以调试和扩展,更没有移植性**。(此条应该是针对MySQL的,因为MySQL对SQL编程和复杂查询性能的优化太烂了) 在众多解读中,我最认同的观点是:**数据库就应该做它最擅长的事情:存储相关。**一个业务系统的设计往往需要你从数据库的层面抽离出来,把主要精力放在业务模型的设计上,在程序层面体现业务逻辑,而不是把业务逻辑都交给数据层面的管理者。**存储过程从来都不是提高性能的关键,反而系统的架构,缓存的设计,数据一致性更是系统关键问题。**它是一种解决方案,但不是唯一的,在选择该方案前请先确保他们是正确的。 > **观点一:**将产品部署到客户处,并且需要客制化。如果不用存储过程,就要动代码,编译。如果客户想自己改逻辑,存储过程可以改,代码就没法自己改了。阿里没有这种场景,所以可以拒绝存储过程。 > **观点二:**包含业务逻辑的存储过程,在遇到DB扩容时很麻烦 > **观点三:**老程序员偏爱存储过程,接手的项目可能拿到的时候就有一堆存储过程,只能一条路走到黑。大公司有专门的DBA优化,但是小公司就没有了。 > **观点四:**还是要看业务需求,适合大数据量低频处理(银行、社保系统)、夜批,但不适合高并发、业务类。 Last modification:August 14, 2022 © Allow specification reprint Like 0 喵ฅฅ