MyBatis进阶

jefxff 153,636 2020-05-05

1. MyBatis 数据库环境切换

  • 在实际的开发过程中,数据库环境的切换很有实用性, 比如, 在开发的时候, 使用的是测试数据库, 但是最后快上线的时候, 就需要在模拟实际环境的数据库上进行压力测试, 最后部署在服务器上之后, 使用生产数据库, 或者在数据库之间迁移也很使用, 只需要配置environments下的 environment 即可, 使用时通过 environments 下面的 default 来指定即可

  • 在conf.xml总配置中配置mysql, oracle数据库之间的切换环境

	<!-- 该标签中可配置数据库信息, 如连接oracle, mysql 等; 可通过default来指定所选择的环境 -->
    <environments default="devMySQL">
        <!-- MySQL 数据库 -->
        <environment id="devMySQL">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <!-- 配置数据库信息; 可以直接在value中写死, 也可以将值以kv对的形式写在properties文件中,
                以类似于EL表法是的方式动态引入(需要先加入 properties标签, 引入properties文件)-->
                <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>

        <!-- Oracle 数据库 -->
        <environment id="devOracle">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <!-- 配置数据库信息; 可以直接在value中写死, 也可以将值以kv对的形式写在properties文件中,
                以类似于EL表法是的方式动态引入(需要先加入 properties标签, 引入properties文件)-->
                <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>
    </environments>
  • 在src下配置properties文件
	# MySQL 数据库
	mysql.driver=com.mysql.cj.jdbc.Driver
	mysql.url=jdbc:mysql://127.0.0.1:3306/MyBatisTest?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true&noAccessToProcedureBodies=true
	mysql.username=root
	mysql.password=123456


	# Oracle 数据库
	oracle.driver=oracle.jdbc.OracleDriver
	oracle.url=jdbc:oracle:thin:@127.0.0.1:1521:MyBatisTest
	oracle.username=root
	oracle.password=123456
  • 在conf.xml中配置数据库支持类(即配置 Provider 别名)
	<!-- 配置数据库支持类: 大小写必能乱写 -->
    <databaseIdProvider type="DB_VENDOR">
        <property name="MySQL" value="mysql"/>
        <property name="Oracle" value="oracle"/>
        <!--<property name="SQL Server" value=""/>-->
    </databaseIdProvider>
  • 根据数据库的特性在mapper.xml中编写对应的SQL语句; 并加上 databaseId="Provider别名"
	<!-- oracle -->
	<select id="queryStudentByNo" resultType="Student" parameterType="int" dataBaseid="oracle">
		select * from student where stuNo=#{stuNo}
	</select>
	<!-- mysql -->
	<select id="queryStudentByNo" resultType="Student" parameterType="int" dataBaseid="mysql">
		select * from student where stuNo=#{stuNo}
	</select>
  • 注意: 如果 mapper.xml 的SQL标签仅有一个不带 databaseId的标签, 则该标签自动适应当前数据库; 如果既有不带databaseId的标签, 又有带databaseId的标签, 则程序会优先使用带databaseId的标签

2. 增删改的返回值问题

  • 由于xml中增删改没有resultType标签所以不能定义返回值类型, 解决办法就是:只需要在接口的增删改抽象方法中加上返回值类型即可

  • 返回值可以是: void, Integer, Long, Boolean

  • 例子:

	<!-- mapper.xml中增加的SQL没有返回值 -->
	<!-- 增加 -->
    <insert id="addStudent" parameterType="Student" >
        insert into student(stuNo, stuName, stuAge, graName, stuSex) values(#{stuNo}, #{stuName}, #{stuAge}, #{graName}, #{stuSex})
    </insert>
    // 增加学生
    Boolean addStudent(Student student);
    // 测试代码
    public static void addStudent() throws IOException {
        SqlSession sqlSession = getSqlSession();
        // 下面两句代码是使用动态代理方式
        StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
        Student student1 = new Student(3, "ju", 20, "class3", false);
        Boolean aBoolean = studentMapper.addStudent(student1);
        if(aBoolean){
            System.out.println("增加成功!");
        } else{
            System.out.println("增加失败!");
        }
        // 下面两句是使用MyBatis基础CRUD的方式, 不提倡了以及, 知道怎么用就可以了
        // String stmt = "xyz.xmcs.Entity.studentMapper.addStudent";
        // Student student = new Student(2, "ls", 20, "class1");
        // int insert = sqlSession.insert(stmt, student);
        sqlSession.commit();
        // System.out.println("Insert Result:" + insert);
        sqlSession.close();
    }

3. MyBatis注解

3.1 使用 @Select 注解

  • 首先修改 conf.xml 文件中的一些配置, 可通过 class或package选之一来指定

    1. mapper中使用class属性指定有注解的接口 <\mapper class="xyz.xmcs.mapper.StudntMapper" />

    2. 或者使用package指定studentMapper接口所在的包(批量注册:可以一次性引入注解接口和mapper文件) <\package name="xyz.xmcs.mapper"/>

  • 修改 StudentMapper.java 接口中的相关方法

	@Select("select * from student where stuNo=#{stuNo}")
	Student queryStudentByNo(int stuNo);
  • 总结:

    • 将 sql 语句写在接口的方法上@select("SQL语句")

    • 将接口的全类名写入总配置conf.xml的<\mapper>, 让mybatis知道SQL语句


4. 事务自动提交问题

  • 手动提交: 当 sessionFactory.openSession() 中形参为空的时候就是手动提交, 当执行增删改的时候, 需要通过 session.commit() 进行手动提交

  • 自动提交: 当 sessionFactory.openSession(true) 中参数为true时, 就是自动提交, 每个DML语句都会自动提交


5. 自增问题

Mysql 本身支持自增

  • mysql 本身支持自增,如果在数据库表中主键支持自增, 在对应的Been中, 自增的类型不能是 int, 必须是 Integer, 因为Integer是对象, 可以为null

  • mysql 数据库自增的值回写:(在需要回写的SQL语句标签中增加)

    • 使用主键生成策略: useGeneratedKeys="true"

    • 指定回写的值: keyProperty="stuNo"

  • 示例

    <!-- 增加: mysql 自增回写主键 -->
    <insert id="addStudent" parameterType="Student" useGeneratedKeys="true" keyProperty="stuNo">
        insert into student(stuNo, stuName, stuAge, graName, stuSex) values(#{stuNo}, #{stuName}, #{stuAge}, #{graName}, #{stuSex})
    </insert>

Oracle 不支持自增

  • Oracle本身不支持自增, 但是可以通过序列来模拟自增

  • 序列自带两个属性:

    • nextval: 序列中的下一个值

    • currval: 当前值

  • Oracle 自增实现

	-- ORacle 创建自增序列
	create sequence myseq increment by 1 start with 1;

	-- 使用自增序列
	insert into student values(myseq.nestval, 'zs',23,'a1');
	insert into student values(myseq.nestval, 'ls',24,'a2');
	insert into student values(myseq.nestval, 'ww',25,'a1');
  • oracle 自增回写: 通过<\insert>的子标签<\selectKey>实现, 在<\selectKey>中查询下一个序列(自增后的值), 再将此值传入 keyProperty="stuNo"属性, 最后在真正执行时, 使用该属性值
    <!-- 增加: oracle 自增回写主键 第一种方式 -->
    <insert id="addStudent" parameterType="Student" databaseId="oracle">
    	<selectKey keyProperty="stuNo" resultType="Integer" order="BEFORE">
    		select myseq.nextval from dual
    	</selectKey>
        insert into student(stuNo, stuName, stuAge, graName, stuSex) values(#{stuNo}, #{stuName}, #{stuAge}, #{graName}, #{stuSex})
    </insert>

    <!-- 增加: oracle 自增回写主键 第二种方式 -->
    <insert id="addStudent" parameterType="Student" databaseId="oracle">
    	<selectKey keyProperty="stuNo" resultType="Integer" order="AFTER">
    		select myseq.currval from dual
    	</selectKey>
        insert into student(stuNo, stuName, stuAge, graName, stuSex) values(#{myseq.noetval}, #{stuName}, #{stuAge}, #{graName}, #{stuSex})
    </insert>

6. 参数问题

  • 以目前学的知识, 将多个参数封装成一个JavaBeen对象(pojo), 然后使用该对象传递参数(我觉得还是这种形式比较好)

  • 如果采用传入多个参数, 就不用再mapper.xml中写 parameterType

  • 以规定的形式传入多个简单形参(也可以通过注解解决这个问题): 只能是 arg0, arg1 ...; 或者 param0, param1, param2 ... 这样的形式

	<!-- insert SQL标签没有返回值, 如果不使用javaBeen来封装参数的化, 就不要写 parameterType属性; 并且使用规定的形参形式 -->
	<insert id="addStudentWithParams" databaseId="mysql">
		insert into student(stuNo, stuName, stuAge, graName) values (#{param1}, #{param2}, #{param3}, #{param4})
	</insert>
	// 增加学生的抽象方法, 这里可以定义返回值, 最好使用包装类类型
	int addStudentWithParams(Integer stuNo, String stuName, Integer stuAge, Stirng graName);
  • 命名参数(通过注解使用自定义形参)

    • 原理就是再接口方法的参数中通过 @Param("stuNo") 指定SQL中参数的名字
	// 增加学生的抽象方法, 这里可以定义返回值, 最好使用包装类类型
	int addStudentWithParams(@Param("stuNo")Integer stuNo, @Param("stuName")String stuName, @Param("stuAge")Integer stuAge, @Param("graName")Stirng graName);
	<!-- insert SQL标签没有返回值, 如果不使用javaBeen来封装参数的化, 就不要写 parameterType属性; 使用自定义形参形式 -->
	<insert id="addStudentWithParams" databaseId="mysql">
		insert into student(stuNo, stuName, stuAge, graName) values (#{stuNo}, #{stuName}, #{stuAge}, #{graName})
	</insert>
  • 综合使用(参数是简单形式加对象形式)
	// 增加学生的抽象方法, 参数的传递是通过简单类型传递stuNo, 通过Student传递其他参数
	int addStudentWithParamAndObject(@Param("stuNo")Integer stuNo, @Param("stu")Student student);
	<!-- insert SQL标签没有返回值, 如果不使用javaBeen来封装参数的化, 就不要写 parameterType属性; 拿的不是参数本身, 而是参数值里面的属性值-->
	<insert id="addStudentWithParams" databaseId="mysql">
		insert into student(stuNo, stuName, stuAge, graName) values (#{stuNo}, #{stu.stuName}, #{stu.stuAge}, #{stu.graName})
	</insert>

7. 增加 null

  • oracle: 如果插入的字段是Null, 提示错误: 应该是Other类型而不是null

  • mysql: (在不考虑非空约束的情况下)如果插入字段值是Null, 可以正常执行

  • 原因: 各个数据库再MyBatis中对各种数据类型的默认值不一致; MyBatis 中, jdbcTypeForNull(如果是Null), 则默认值是OTHER, 对于Other来说, MySQL可以处理, 但是Oracle不能处理

  • 解决Oracle数据库Null问题: 目的就是告诉Oracle, Other --就是--> Null(下面两种方式)

    • 方式一: 当某个数据类型oracle无法处理时, 告诉他默认值就是NULL; 即直接在可能出现Null的SQL形参中(#{})指定 jdbcType=NULL; 如: #{stuName, jdbcType=NULL}; 这句话的效果就是, 当Oracle无法解析这个参数时就使用NULL, 但是能正常解析的话就不管NULL(就是不会影响正常的值)

    • 方式二: 配置MyBatis全局配置文件conf.xml; 在settings 中增加 <\setting name="jdbcTypeForNull" value="NULL" />


8. 返回值为HashMap的情况

  • 已经使用过了, 就是在写SQL语句时, 查询的每个字段起个别名, 返回值resultType指定是HashMap形式; 并且一个实例对应一个Map,要获取多个实例的话, 就需要使用List; 从Map里面取值的时候通过别名来取值

    • 起别名的SQL: select stuNo 'id', stuName 'name', stuAge 'age' from student where stuNo=#
  • 如果在查询的时候不起别名, 那么Map获取到的值的key就是数据表的字段名

    • 不起别名的SQL: select stuNo, stuName, stuAge from student where stuNo=#
  • 使用HashMap接收返回的多个值, 并且 key 是指定的数据表字段名, value是对应的返回值; (关键就是在接口方法中通过@MapKey("stuNo")来指定Map的key; oracle中 stuNo需要全部大写)

    <!-- 返回值是HashMap, 并且key是stuNo, value是student -->
    <select id="queryStudentsByHashMap1" resultType="HashMap">
        select stuNo, stuName, stuAge from student
    </select>
    // 返回值是HashMap, 并且每个key是stuNo, value是Student
	// 这里要注意: Oracle中元数据都是大写(元数据就是字段名, 表名等)
    @MapKey("stuNo")
    HashMap<Integer, Object> queryStudentsByHashMap1();
  • 使用List<\HashMap> 接收返回的多个值, 每个值就对应List的一个元素
    <!-- 返回值是HashMap, 但接口的返回值是 List<HashMap<Integer, Object>> -->
    <select id="queryStudentsByListHashMap" resultType="HashMap">
        select stuNo, stuName, stuAge from student
    </select>
    // 返回值是  List<HashMap<Integer, Object>>
    List<HashMap<Integer, Object>> queryStudentsByListHashMap();

9. resultMap : 字段和属性的对应关系

  • 如果属性和字段的名字和类型都能匹配, 就使用 resultType; 否则(就是属性和字段名字或类型不匹配)就使用resultMap指定对应关系; 需要单独配置 resultMap

  • resultMap对应关系示例

<!--  
	type: 这条查询语句的返回值类型
	id(标签内): 唯一标识, 为了让SQL标签的resultMap属性来引用
	id(标签体): 用于指定主键的对应关系
	result: 用于指定一般字段的对应关系
	prpperty: been中的属性名,严格区分大小写
	column: 数据表的字段名
	jdbcType: 用于指定数据表的类型, 全部大写
	javaType: been属性的类型
	association: 用于指定一对一查询时另外一张表的对应关系; 下面是立即查询的示例;延迟查询时标签体内不写东西, 使用association标签的select=namespace.id来引入查询语句
	select(association标签内): 用来引入一对一查询时另外一张表的字段属性对关系
	ofType: 用于指定该属性的元素的类型
	collection: 用来指定一对多时的对应关系
-->
<resultMap type="Student" id="query_student_map">
	<id property="stuNo" column="sno" />
	<result property="stuName" column="sname" />
	<result property="stuAge" column="sage" />
	<result property="stuSex" column="sex" jdbcType="INTEGER" javaType="boolean"/>

	<!-- 一对一时, 对象成员使用 association映射, javaType指定该属性的类型
    <association property="studentCard" javaType="StudentCard" select="namespace.id" column="column" ofType="Student">
        <id property="cardId" column="cardId"/>
        <result property="cardInfo" column="cardInfo"/>
    </association>
	 -->

	 <!-- 一对多是 collection 
		  在collection的内部还可以接着配 Student 内的 studentCard的信息, 使用的标签是 association
	 <collection property="students" ofType="Student">
        <id property="stuNo" column="stuNo"/>
        <result property="stuName" column="stuName"/>
        <result property="stuAge" column="stuAge"/>
    </collection>
    -->
</resultMap>

resultMap 中配置鉴别器 discriminator

  • resultMap 鉴别器: 就是对查询结果进行分支处理; 即可以根据不同的case, 返回不同的值; 如: 若年级是class1, 显示真名, 年级是class2, 显示昵称

    • 理解就是: discriminator 完成的就是把不同的数据库表的字段根据某个条件映射到JavaBeen的一个属性上, 从而完成返回的值就是多字段对应一个属性
  • 修改数据库以及增加需要的数据

	-- 准备sql
	alter table student add column nickName varchar(20);
	update student set student.nickName = 'zxs' where stuNo = 1;
	update student set student.nickName = 'lxs' where stuNo = 2;
	update student set student.nickName = 'wxw', graName='class2' where stuNo = 3;
	insert into student values(4,'zl',20,'class1',0,'ts','lq',1,1,'zxl');
  • 在 mapper.xml 中配置SQL标签及resultMap
	<!-- 配置SQL标签以及resultMap -->
	<!-- 使用鉴别器: 若年级是class1, 显示真名, 年级是class2, 显示昵称 -->
    <resultMap id="query_student_with_disc_map" type="Student">
        <!-- 学生的信息 -->
        <id property="stuNo" column="stuNo"/>
        <result property="stuAge" column="stuAge"/>
        <result property="graName" column="graName"/>
        <discriminator javaType="String" column="graName">
            <case value="class1" resultType="Student">
                <result property="stuName" column="stuName" />
            </case>
            <case value="class2" resultType="Student">
                <result property="stuName" column="nickName" />
            </case>
        </discriminator>
    </resultMap>
    <select id="queryStudentsWithDisc" resultMap="query_student_with_disc_map">
        select stuNo, stuName, graName, nickName from student
    </select>
  • 接口以及测试类
	// 接口方法及测试类
    // 使用鉴别器显示stuName属性映射不同的数据库表
    List<Student> queryStudentsWithDisc();
    // 测试上面接口的方法
    public static void queryStudentsWithDisc() throws IOException {
	    SqlSession sqlSession = getSqlSession();
	    StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
	    List<Student> students = mapper.queryStudentsWithDisc();
	    for (Student student : students) {
	        System.out.println(student);
	    }
	}

	/**
	 * 使用鉴别器的输出结果
 	 * Student{stuNo=1, stuName='zs', stuAge=0, graName='class1'}
	 * Student{stuNo=2, stuName='lxs', stuAge=0, graName='class2'}
	 * Student{stuNo=3, stuName='wxw', stuAge=0, graName='class2'}
	 * Student{stuNo=4, stuName='zl', stuAge=0, graName='class1'}
  	 *
  	 * 未使用鉴别器的输出结果
  	 * Student{stuNo=1, stuName='zs', stuAge=18, graName='class1'}
  	 * Student{stuNo=2, stuName='LS', stuAge=14, graName='class2'}
  	 * Student{stuNo=3, stuName='ww', stuAge=27, graName='class2'}
	 * Student{stuNo=4, stuName='zl', stuAge=20, graName='class1'}
	*/

10. 别名问题

  • 给包起别名就是让该包的类可以直接使用不区分大小写的类名

  • 如果在批量起别名时,出现了冲突(包和子包含有同名的类), 可以使用@Alias("别名")来再设一个别名来区分

	<typeAliases>
		<!-- 给xyz.xmcs.Entity包(以及子包)中的所有类起了别名; 别名:不带包名的类名, 并且不区分大小写 -->
		<package name="xyz.xmcs.Entity"></package>
	</typeAliases>

11. 内置参数

  • _paramter: 代表MyBatis的输入参数值, 可以代替#{}花括号中的值

  • _databaseId: 代表当前数据库的id


12. 模糊查询问题

  • 模糊查询的三种方式: 使用${}; 或使用#{}但是传值时传%s%; 或者 bind参数

  • 第一种: '%$%': 缺点:不能防止SQL注入; 优点: 传进去的值会原样输出

  • 第二种: #, 传入值: %jef% : 优势: 有效防止安全问题

  • 第三种: <\bind name="_queryName" value=" '%' + stuName + '%' " />; 通过bind将传入的值进行处理, 加'%'; 也就是将第二种的传值提前到的SQL配置种


13. MyBatis框架及源码了解

MyBatis 框架

  • MyBatis 框架从上到下依次是: 接口层(广义的接口) -> 数据处理曾 -> 框架支撑层 -> 引导层

  • 接口层(广义的接口) : 增加接口(方法), 删除, 修改, 查询, 各种直接使用的配置方法

  • 数据处理层 : 用于 参数处理, SQL解析, SQL执行, 处理结果, 底层使用 Executor 来执行

  • 框架支撑层 : 用于事务管理, 缓存机制, 连接池管理等... (主要是各种辅助功能)

  • 引导层 : SQL语句的方式, XML配置, 注解方式

MyBatis 框架源码了解

  • 一条SQL执行时, 在MyBatis种的步骤

    • 获取 SqlSessionFactory 对象

    • 获取 SqlSession 对象

    • 获取XxxMapper 对象 (代理接口中的方法, 或mapper.xml中的<\select | update| insert | delete>标签)

    • 执行 <\select | update| insert | delete> 等标签中定义的SQL语句

  • 获取 SqlSessionFactory 对象

    • 通过 parseConfiguration()在configuration标签设置了properties, settings, environments 等属性标签, 将所有的配置信息放在了Configuration对象中

    • 解析所有的XxxMapper.xml 文件(分析其中的增删改查标签); <\select id="" resultType="" 等标签属性是通过parseStatementNode()解析后构建成MappedStatement对象

      • 即: MappedStatement 对象就是 <\select> 这样的标签
    • MappedStatement, environmemt 存在于 Configuration 中; 即: 所有的配置信息, 增删改标签全部存在于 Configuration 中; Configuration 又存在于 DefaultSqlSessionFactory 对象中 (也就是 SqlSessionFactory)

    • 总结: SqlSessionFactory对象 -> DefaultSqlSessionFactory -> Configuration -> 包含了一切配置信息

  • 获取 SqlSession 对象

    • configuration.newExecutor(tx, execType) -> SimpleExecutor

    • MyBatis 根据不同类型execType, 产生不同的Executor, 并且会对执行器进行拦截操作: Executor executor = (Executor)this.interceptorChain.pluginAll(executor);通过装饰模式, 将刚才产生的executor包装成一个更强大的executor

      • 作用: 如果要给MyBatis增加自己开发的插件, 就可以通过拦截器实现
    • 总结: SqlSession -> openSession() -> openSessionFromDataSource() -> DefaultSqlSession对象

  • 获取XxxMapper对象, 执行

    • 执行增删改查 -> MapperProxy/invoke() --> invocationHandler(JDK提供的动态代理对象)

      • 这里用到了动态代理模式: 将增删改查的执行交给了代理对象(MapperProxy对象) -> 代理对象帮我们"代理执行"增删改查方法
    • mapperMethod.execute(SqlSession, args): 实际调用增删改查方法, 依靠了 SqlSession中的 configuration 和 executor

    • 处理增删改查方法的参数: method.convertArgsToSqlCommonParam(args); 如果参数是0个, return null; 如果参数是1个, return 第一个, 如果参数是多个, 将参数放入Map中返回

    • 查询方法: selectOne() -> selectList() : configuration.getMappedStatement() 即获取到用于增删改查的对象

    • boundSql : 将我们写的SQL和参数值进行拼接后的对象, 这就是我们最后能执行的SQL

    • 如果缓存中没有要查询的内容, 则进入数据库真实查询: 调用的是 queryFromDatabase() 方法

    • MyBatis 底层使用的是PreparedStatement的execute()方法执行的, 真正执行SQL的是Executor

    • MyBatis底层在执行CRUD时, 可能会涉及到四个处理器: StatementHandler(处理PreparedStatement的控制器), ParameterHandler(处理参数的控制器), ResultSetHandler(处理结果集的控制器), TypeHandler(类型转换的控制器)

    • XxxMapper:重点就包含三个对象: SqlSession(Configuration, executor,事务), 代理接口的对象(MapperInterface), methodCache(存放查询缓存, 底层是 currentHashMap)


13. 自定义插件

  • 四大核心对象: StatementHandler, ParameterHandler, ResultSetHandler, Executor

    • 四大核心对象, 都涉及了拦截器用于增强

    • 四大核心对象都包含了 增强操作

  • 自定义插件的编写逻辑: 根据MyBatis规则编写一个拦截器, 在拦截器内部加入自定义的增强功能, MyBatis 会自动将该拦截器放入拦截器链

  • 编写多个拦截器时, 拦截器的执行顺序和在 <\plugins> 中的配置顺序一致

  • 多个拦截器的执行顺序: 调用方法时, 先进入拦截器1的plugin()和setPropertis()方法, 再进入拦截器2的plugin()和setPropertis()方法, 再返回进入拦截器1的interceptor()方法, 再进入拦截器2的interceptor()方法, 最后再执行方法

自定义插件步骤:

  • 编写一个类实现 org.apache.ibatis.plugin.Interceptor接口的方法
	package xyz.xmcs.my.interceptors;

	import org.apache.ibatis.executor.statement.StatementHandler;
	import org.apache.ibatis.plugin.*;
	import org.apache.ibatis.session.ResultHandler;
	import java.sql.Statement;
	import java.util.Properties;

	/**
	 * @author jefxff
	 * @date 2020/4/13 - 22:33
	 */

	@Intercepts({
	        @Signature(
	                type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class}
	        )
	})
	public class MyInterceptor implements Interceptor {
	    @Override
	    public Object intercept(Invocation invocation) throws Throwable {
	        System.out.println("拦截方法...intercept...");
	        Object proceed = invocation.proceed();  // 放行
	        return proceed;
	    }

	    @Override // 将拦截器中定义的增强功能和原来的核心对象合并起来, 称为最终的核心对象
	    public Object plugin(Object o) {
	        Object wrap = Plugin.wrap(o, this);
	        return wrap;
	    }

	    @Override
	    public void setProperties(Properties properties) {
	        System.out.println("设置属性: " + properties);
	    }
	}
  • 编写 @Intercepts({@Signature()}) 签名注解
	@Intercepts({
	        @Signature(
	                type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class}
	        )
	})
  • 在总配置 conf.xml 中配置
    <!-- 配置插件: 在这里配置自定义的插件 -->
    <plugins>
        <plugin interceptor="xyz.xmcs.my.interceptors.MyInterceptor">
            <property name="name" value="zs" />
            <property name="age" value="23"/>
        </plugin>
    </plugins>

编写插件修改SQL语句

  • 主要的工作就是在拦截器中拿到SQL的语句和参数, 修改后, 放行

  • 在拦截器实现类的intercept方法中, 通过 Object target = invocation.getTarget() 获取目标方法;

  • 通过 SystemMetaObject.forObject(target); 获取 MetaObject 类型的元数据

  • 通过MetaObject的实例调用setValue(String name, Object value) 来修改

    • 重点: name 的值就是 StatementHandler, ParameterHandler, ResultSetHandler, Executor 这四个核心对象的属性
  • 修改参数后, 就可以放行了 Object proceed = invocation.proceed(); // 放行

	package xyz.xmcs.my.interceptors;

	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 java.sql.Statement;
	import java.util.Properties;

	/**
	 * @author jefxff
	 * @date 2020/4/13 - 22:33
	 */

	@Intercepts({
	        @Signature(
	                // type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class}
	                // 这里修改了 method 的值为parameterize, 因为是要修改SQL语句的参数
	                type = StatementHandler.class, method = "parameterize", args = {Statement.class}
	        )
	})
	public class MyInterceptor implements Interceptor {
	    @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(value);
	//        metaObject.setValue("parameterHandler.parameterObject",2);
	//        Object value2 = metaObject.getValue("parameterHandler.parameterObject");
	//        System.out.println(value2);

	        // 获取SQL语句
	        Object value1 = metaObject.getValue("parameterHandler.boundSql.sql");
	        // 修改SQL语句
	        //metaObject.setValue("parameterHandler.boundSql.sql", "select stuName, stuAge from student where stuNo = ?");

	        // 获取参数
	        Object value = metaObject.getValue("parameterHandler.boundSql.parameterObject");
	        System.out.println(value);
	        // 修改参数
	        metaObject.setValue("parameterHandler.boundSql.parameterObject", 2);

	        Object proceed = invocation.proceed();  // 放行
	        return proceed;
	    }

	    @Override // 将拦截器中定义的增强功能和原来的核心对象合并起来, 称为最终的核心对象
	    public Object plugin(Object o) {
	        Object wrap = Plugin.wrap(o, this);
	//        System.out.println("plugin...            " + wrap);
	        return wrap;
	    }

	    @Override
	    public void setProperties(Properties properties) {
	        System.out.println("设置属性: " + properties);
	    }
	}


14. 批量增删改(DDl)

  • 批量操作就记住, 在获取SqlSessionFactory对象是使用sessionFactory.openSession(ExecuteType.BATCH), 就可以执行批量增删改; openSession() 形参为空默认就是SIMPLE

  • 使用BATCH之后, SQL预编译一次, 其余次数只需要设置参数值即可; 不使用BATCH, 预编译N次, 每次DML都需要执行完整的SQL

  • 其他形式的批量增删改(一个目的, 拼接SQL语句)

    • create table 表 select ... from 旧表;

    • insert into 表(...) select ... from 表;

    • begin ...(DML)... end;

    • (Oracle) 数据泵, SQL Loader, 外部表

  • MySQL 批量插入的例子(了解, 最好不要使用)

    • 没有用到MyBatis的BATCH批量插入

    • 这种写法不适合数据库迁移

    <!-- 批量增加数据, MySQL -->
    <insert id="insertStudentMySQL" >
        <foreach collection="list" item="stu" open="insert into student(stuNo, stuName) values " close=";" separator=", ">
            (#{stu.stuNo}, #{stu.stuName})
        </foreach>
    </insert>
    // 事务控制批量增加
    int insertStudentMySQL(List<Student> students);
    // 
    // 批量增加学生: MySQL
    public static void insertStudentMySQL() throws IOException {
        SqlSession sqlSession = getSqlSession();
        StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
        Student stu1 = new Student(1001,"zs1");
        Student stu2 = new Student(1002,"zs2");
        Student stu3 = new Student(1003,"zs3");
        Student stu4 = new Student(1004,"zs4");
        Student stu5 = new Student(1005,"zs5");
        List<Student> students = new ArrayList<>();
        students.add(stu1);
        students.add(stu2);
        students.add(stu3);
        students.add(stu4);
        students.add(stu5);
        int i = mapper.insertStudentMySQL(students);
        System.out.println("插入了 [" + i + "] 条数据");
        sqlSession.commit();
        sqlSession.close();
    }

15. 使用pageHelper插件

  • 下载两个jar包, pagehelper-5.0.1.jar, jsqlparser-0.9.5.jar

  • 在MyBatis总配置中配置plugin

    <!-- 配置插件: 在这里配置自定义的插件 -->
    <plugins>
    	<!-- 配置分页插件 -->
        <plugin interceptor="com.github.pagehelper.PageInterceptor">
        </plugin>
    </plugins>
  • 使用
    public static void queryAllStudent() throws IOException {
        SqlSession sqlSession = getSqlSession();

        // 下面两句代码是使用动态代理方式
        StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);

        // 这是第一种方法, 直接调用方法startPage(), 返回值是Page类型的, 里面包含了Student数据, 以及分页需要的数据
//        Page<Student> page = PageHelper.startPage(1, 10);
//        List<Student> studentss = studentMapper.queryAllStudent();

        // 这是第二种方法, 使用Lambda, 相当于上面第一种方法的两句代码; 返回值是Page类型的, 里面包含了Student数据, 以及分页需要的数据
        Page<Student> page = PageHelper.startPage(1, 10).doSelectPage(studentMapper::queryAllStudent); // 这是第二种方法
        // pageInfo 所包含的值(或功能)比pageInfo更多
        PageInfo<Student> pageInfo = PageHelper.startPage(1, 10).doSelectPageInfo(studentMapper::queryAllStudent);
        int nextPage = pageInfo.getNextPage();
        System.out.println("nextPage: " + nextPage);

        List<Student> list = page.getResult();
        System.out.println(page);
        System.out.println("当前页: " + page.getPageNum());
        System.out.println("总数据量: " + page.getTotal());
        System.out.println("总页码: " + page.getPages());
        System.out.println("页面大小: " + page.getPageSize());
        for (Student student : list) {
            System.out.println(student);
        }
        sqlSession.commit();
        sqlSession.close();
    }

# Java # mysql