进阶版来了!前期回顾看这里:


多环境切换

当用多个数据库存放不同资源的时候,需要配置多数据源。

Mybatis版

conf.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

#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

<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支持自增,所以只需要多配置两个属性即可:

<!--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;

<!--方式一(推荐)-->
<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。该操作不会影响到正常值)

<insert id="addStudent"  databaseId="oracle">
    insert into student(stuno,stuName) values(#{stuNo} , #{stuName,jdbcType=NULL}) 
</insert>

(2)配置 mybatis全局配置文件conf.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)
    #{stuNo}、#{stuName}

遇到两种组合出现的情况:
addStudent(@Param("stuNo") Integer stuNo,@Param("student") Student student)
#{stuNo}、#{student.stuName}


Mybatis拦截器

拦截各种sql进行更改,比如伪装的概率up,底层直接把概率sql改成0.0001%,然后再设置日志只显示原先sql,或者是几次必得那种直接底层给你累加妥妥的上限更新。
多个拦截器的执行顺序
编写拦截器、添加注解:

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中配置参数

<configuration>
    <plugins>
       <plugin interceptor="com.example.demo.myinterceptors.MyInterceptor">
           <property name="name" value="zs"/>
           <property name="age" value="23"/>
       </plugin>
    </plugins>
</configuration>

PageHelper 分页插件

食用说明

Demo:

<!--pagehelper Mybatis的分页插件-->
<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper</artifactId>
    <version>5.2.0</version>
</dependency>
<plugins>
    <plugin interceptor="com.github.pagehelper.PageInterceptor">
        <!-- 输入的跳转页大于总共的则返回第一页 -->
        <property name="reasonable" value="true"/>
    </plugin>
</plugins>
@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类型

细节二:职务的手动/自动提交

常规写法一般都是用手动提交,即每次都是:

SqlSession session = sessionFactory.openSession();
...
session.commit();//手动提交
session.close();

自动提交只需把openSession加一个参数true即可

SqlSession session = sessionFactory.openSession(true);
...
session.close();

自动提交在整个controller中如果有多个方法则会一直提交,所以还是推荐手动一次性提交。

细节三:批量操作

只需要改动一个地方(十万条20秒变1秒不到)
sessionFactory.openSession(ExecutorType.BATCH);
BATCH: 预编译SQL一次,其余DML只需要设置参数值即可.(没有BATCH:预编译N次,每次DML都需要执行完整的SQL)

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:November 25th, 2020 at 12:00 pm
喵ฅฅ