• Java之MyBatis


    MyBatis中文文档:https://mybatis.org/mybatis-3/zh/index.html
    MyBatis是一款优秀的持久化框架,它支持定制化SQL、存储过程以及高级映射。
    MyBatis避免了几乎所有的JDBC代码和手动配置参数以及结果集。
    MyBatis可以使用简单的XML或注解来配置和映射原生类型、接口和Java的POJO(Plain Old Java Objects,普通老式Java对象)为数据库中的记录。

    MyBatis是java中的一个ORM框架。

    MyBatis是优秀的持久层框架。所谓持久,就是将内存中的数据保存在数据库中,以便数据的丢失。

    MyBatis使用XML将SQL与程序解耦,便于维护。

    MyBatis学习简单,执行高效,是JDBC的延伸。

    1.基本使用

    MyBatis使用xml(mybatis-config.xml)的形式保存配置信息。
    MyBatis环境配置标签<environment>。
    environment包含数据库驱动、URL、用户名和密码。

    (1)创建项目,引入依赖

    推荐使用maven来构建项目。

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
    
        <groupId>com.ikidana</groupId>
        <artifactId>mybatis</artifactId>
        <version>1.0-SNAPSHOT</version>
    
        <!--使用阿里云包源,提高下载速度-->
        <repositories>
            <repository>
                <!--创建私服的地址-->
                <id>aliyun</id>
                <name>aliyun</name>
                <url>https://maven.aliyun.com/repository/public</url>
            </repository>
        </repositories>
    
        <dependencies>
            <!--引入mybatis框架-->
            <dependency>
                <groupId>org.mybatis</groupId>
                <artifactId>mybatis</artifactId>
                <version>3.5.1</version>
            </dependency>
            <!--底层使用mysql驱动,所以需要使用mysql-JDBC驱动-->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>5.1.47</version>
            </dependency>
            <!--单元测试组件-->
            <dependency>
                <groupId>junit</groupId>
                <artifactId>junit</artifactId>
                <version>4.12</version>
            </dependency>
        </dependencies>
    
    </project>

    (2)在resource下面,创建核心配置文件mybatis-config.xml

    这个文件的名称是固定,这样命名才可以自动加载,否则需要修改配置。

    <?xml version="1.0" encoding="UTF-8" ?>  <!--XML申明,每一个XML文件都需要-->
    <!--MyBatis DTD文档约束-->
    <!DOCTYPE configuration
            PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-config.dtd">
    <configuration>
        <!--在MyBatis中可以配置多套环境,然后通过default来控制采用哪套环境,让配置变得灵活-->
        <environments default="dev">
            <!--配置测试环境,不同的环境不同的id名字-->
            <environment id="dev">
                <!--采取JDBC方式对数据库事务进行commit/rollback-->
                <transactionManager type="JDBC"/>
                <!--采用连接池方式管理数据库连接-->
                <dataSource type="POOLED">
                    <!--数据库驱动-->
                    <property name="driver" value="com.mysql.jdbc.Driver"/>
                    <!--IP、端口、库、字符集-->
                    <!--需要注意的是&在XML中是有意义的,需要使用amp;进行转义-->
                    <property name="url" value="jdbc:mysql://148.70.251.10:3306/babytun?useUnicode=yes&amp;characterEncoding=utf8"/>
                    <!--用户名和密码-->
                    <property name="username" value="root"/>
                    <property name="password" value="123456"/>
                </dataSource>
            </environment>
            <environment id="prod">
                <transactionManager type="JDBC"/>
                <dataSource type="POOLED">
                    <property name="driver" value="com.mysql.jdbc.Driver"/>
                    <property name="url" value="jdbc:mysql://148.70.251.110:3306/babytun?useUnicode=yes&amp;characterEncoding=utf8"/>
                    <property name="username" value="root"/>
                    <property name="password" value="931548241"/>
                </dataSource>
            </environment>
        </environments>
    </configuration>

    (3)引入测试类

    package com.ikidana.mybatis;
    
    import org.apache.ibatis.io.Resources;
    import org.apache.ibatis.session.SqlSession;
    import org.apache.ibatis.session.SqlSessionFactory;
    import org.apache.ibatis.session.SqlSessionFactoryBuilder;
    import org.junit.Test;
    
    import java.io.IOException;
    import java.io.Reader;
    import java.sql.Connection;
    
    
    public class MyBatisTestor {
        @Test
        public void testSqlSessionFactory() throws IOException {
            //getResource读取配置文件  AsReader按照字符流的方式进行读取
            //getResourceAsReader返回Reader,Reader包含XML的文本信息
            Reader reader = Resources.getResourceAsReader("mybatis-config.xml");
            //利用构造者模式来初始化SqlSessionFactory对象,同时解析mybatis-config.xml配置文件
            SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
            //测试SqlSessionFactory是否初始化成功,并不意味着已经连接数据库
            //System.out.println("加载成功");
            SqlSession sqlSession = null;
            //创建SqlSession对象,SqlSession是JDBC的扩展类,用于与数据库进行交互
            try {
                sqlSession = sqlSessionFactory.openSession();
                //在正常开发时,MyBatis会自动帮我们完成来连接动作,此处是测试使用
                Connection connection = sqlSession.getConnection();
                System.out.println(connection);
                //com.mysql.jdbc.JDBC4Connection@370736d9,连接已创建
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                if (sqlSession != null) {
                    //如果type="POOLED",代表使用连接池,close则是将连接回收到连接池中
                    //如果type="UNPOOLED",代表直连,close则会调用Connection.close()来关闭连接
                    sqlSession.close();
                }
            }
        }
    }

    (4)封装工具类

    package com.ikidana.mybatis.utils;
    
    import org.apache.ibatis.io.Resources;
    import org.apache.ibatis.session.SqlSession;
    import org.apache.ibatis.session.SqlSessionFactory;
    import org.apache.ibatis.session.SqlSessionFactoryBuilder;
    
    import java.io.IOException;
    import java.io.Reader;
    
    /**
     * MyBatisUtilsgoon工具类,创建全局唯一的SqlSessionFactory对象
     * */
    
    public class MyBatisUtils {
        //利用static(静态)属于类不属于对象,且全局唯一,static属性本身就属于全局唯一
        private static SqlSessionFactory sqlSessionFactory = null;
        //利用静态块在初始化时实例化sqlSessionFactory
        static {
            Reader reader = null;
            try {
                reader = Resources.getResourceAsReader("mybatis-config.xml");
            } catch (IOException e) {
                e.printStackTrace();
                //初始化遇到错误时,将异常抛给调用者
                throw new ExceptionInInitializerError(e);
            }
            sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
        }
    
        //定义返回SqlSession对象的方法
        public static SqlSession openSession(){
            return sqlSessionFactory.openSession();
        }
        //释放SqlSession对象
        public static void closeSession(SqlSession session){
            if (session != null) {
                session.close();
            }
        }
    }

    调用示例:

    @Test
    public void testMyBatisUtils() throws Exception {
        SqlSession sqlSession = null;
        try {
            //一句话完成SqlSession的初始化工作
            sqlSession = MyBatisUtils.openSession();
            //执行数据库操作
            //sqlSession.insert()
            //sqlSession.update()
            Connection connection = sqlSession.getConnection();
            System.out.println(connection);
        } catch (Exception e) {
            throw e;
        } finally {
            //关闭数据连接
            MyBatisUtils.closeSession(sqlSession);
        }   
    }

    2.增删改查

    1)MyBatis数据查询

    MyBatis数据查询步骤
      • 创建实体类(Entity)
      • 创建Mapper XML
      • 编写<select>SQL标签
      • 开启驼峰命名映射
      • 新增<mapper>
      • SqlSession执行select语句
    之前我们我们已经通过SqlSessionFactory来创建SqlSession连接对象,但是我们该如何查找数据了?

    (1)创建实体类

    实体类对应SQL语句:

    create table t_goods
    (
        goods_id         int auto_increment comment '商品编号'
            primary key,
        title            varchar(128)  not null comment '商品名称',
        sub_title        varchar(256)  null comment '子标题',
        original_cost    float         not null comment '原价',
        current_price    float         not null comment '折后价',
        discount         float         not null comment '折扣(0~1)',
        is_free_delivery int           not null comment '是否包邮',
        category_id      int default 0 not null
    )
        charset = utf8;

    实体类:

    package com.ikidana.mybatis.entity;
    
    public class Goods {
        private Integer goodsId;
        private String title;
        private String subTitle;
        private Float originalCost;
        private Float currentPrice;
        private Float discount;
        private Integer isFreeDelivery;
        private Integer categoryId;
    
        public Integer getGoodsId() {
            return goodsId;
        }
    
        public void setGoodsId(Integer goodsId) {
            this.goodsId = goodsId;
        }
    
        public String getTitle() {
            return title;
        }
    
        public void setTitle(String title) {
            this.title = title;
        }
    
        public String getSubTitle() {
            return subTitle;
        }
    
        public void setSubTitle(String subTitle) {
            this.subTitle = subTitle;
        }
    
        public Float getOriginalCost() {
            return originalCost;
        }
    
        public void setOriginalCost(Float originalCost) {
            this.originalCost = originalCost;
        }
    
        public Float getCurrentPrice() {
            return currentPrice;
        }
    
        public void setCurrentPrice(Float currentPrice) {
            this.currentPrice = currentPrice;
        }
    
        public Float getDiscount() {
            return discount;
        }
    
        public void setDiscount(Float discount) {
            this.discount = discount;
        }
    
        public Integer getIsFreeDelivery() {
            return isFreeDelivery;
        }
    
        public void setIsFreeDelivery(Integer isFreeDelivery) {
            this.isFreeDelivery = isFreeDelivery;
        }
    
        public Integer getCategoryId() {
            return categoryId;
        }
    
        public void setCategoryId(Integer categoryId) {
            this.categoryId = categoryId;
        }
    }

    实体类的属性与表字段名一一对应。同时需要创建每个属性的getter和setter方法。
    实体类存在的价值在于,以对象的形式对SQL语句的查询结果进行封装,
    以便后续可以像操作类一样操作数据。

    创建实体类主要是为了将查询结果封装成对象,以便后续操作的遍历。

    (2)创建Mapper XML文件

    对于Mybatis这个持久化框架来说,本质就是保存SQL语句的地方,方便我们将SQL代码与逻辑代码分开,方便管理。

    一般会在resourses目录下面,创建mappers目录,专门管理映射文件。

    常规做法是,对于常用实例类都会一一创建mapper文件。

    下面创建一条查询t_goods这个表的所有数据的SQL。

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
            PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="goods">
        <!--namespace对应某个命名空间,这样就知道去那个文件查找关联-->
        <!--resultType:在SQL语句执行完成之后,会将每一个结果包装成指定的对象-->
        <select id="selectAll" resultType="com.ikidana.mybatis.entity.Goods">
            select * from t_goods order by goods_id desc limit 10
        </select>
    </mapper>

    这个是MyBatis的核心文件,在这里会完成SQL查询,并将结果进行封装

    (3)在mybatis-config.xml中注册Mapper映射文件

    由于这个目录和文件都是我们自定义的,因此需要注册。

    <mappers>
        <mapper resource="mappers/goods.xml"/>
    </mappers>

    (4)在逻辑代码中就可以直接通过id来调用结果

    public void testSelectAll() throws Exception{
        SqlSession session = null;
        try {
            session = MyBatisUtils.openSession();
            List<Goods> list = session.selectList("goods.selectAll");
            for (Goods g:list) {
                System.out.println(g.getTitle());
            }   
        } catch (Exception e) {
            throw e;
        } finally {
            MyBatisUtils.closeSession(session);
        }   
    }

    (5)开启驼峰命名映射

    在创建实例类的使用,我们会将字段名(带下划线)original_cost转化为字段名originalCost驼峰语法。
    Java并不是不允许带下划线的变量名,更多的是使用习惯的问题。
    如果强制将带下划线的字段名,转化为驼峰语法,那么可能会无法获取信息。
    因为你在查询的时候,去数据库中查找originalCost根本找不到。

     

     需要在mybatis-config.xml开启驼峰语法,进行自动转换。

    <settings>
        <!--如果遇到下划线,就会将下划线后的第一个字母转换为大写,并删除下划线-->
        <setting name="mapUnderscoreToCamelCase" value="true"/>
    </settings>

    设置之后就可以获取全部数据:

    2)SQL传参

    (1)传单个参数

    <select id="selectById" parameterType="Integer" resultType="com.ikidana.mybatis.entity.Goods">
        select * from t_goods where goods_id = #{value}
    </select>

    parameterType是传参的类型,#{value}是SQL语句中固定的写法.

    public void testSelectId() throws Exception {
        SqlSession session = null;
        try {
            session = MyBatisUtils.openSession();
            Goods goods = session.selectOne("goods.selectById",1602);
            System.out.println(goods.getCategoryId());
        } catch (Exception e) {
            throw e;
        } finally {
            MyBatisUtils.closeSession(session);
        }   
    }

    (2)传多个参数

    parameterType虽然只允许出现一次,但是我们可以传入一个引用数据类型.

    <select id="selectByPriceRange" parameterType="java.util.Map" resultType="com.ikidana.mybatis.entity.Goods">
        select * from t_goods
        where current_price between #{min} and #{max}
        order by current_price limit 0,#{limit}
    </select>

    Map属于字典类型,使用键值对来存储数据.

    public void testSelectByPriceRange() throws Exception{    SqlSession session = null;
        try {
            session = MyBatisUtils.openSession();
            Map param = new HashMap();
            param.put("min",100);
            param.put("max",500);
            param.put("limit",10);
            List<Goods> list = session.selectList("goods.selectByPriceRange",param);
            for (Goods g:list){
                System.out.println(g.getTitle());
            }   
        } catch (Exception e) {
            throw e;
        } finally {
            MyBatisUtils.closeSession(session);
        }   
    }

    3)数据插入

    在数据插入的时候最好关闭事务的自动提交。

    public static SqlSession openSession(){
        //默认SqlSession对自动提交事务数据(commit)
        //设置false代表关闭自动提交,改为手动提交事务
        return sqlSessionFactory.openSession(false);
    }

    XML需要添加的代码:

    <insert id="insert" parameterType="com.ikidana.mybatis.entity.Goods">    INSERT INTO t_goods(title,sub_title,original_cost,current_price,discount,is_free_delivery,category_id)    VALUES (#{title}, #{subTitle}, #{originalCost}, #{currentPrice}, #{discount}, #{isFreeDelivery}, #{categoryId});    <!--如果需要获取最后插入数据的ID,需要添加如下代码-->
        <!--resultType返回值类型,keyProperty返回值绑定属性,order执行顺序,在插入语句之前还是之后-->
        <selectKey resultType="Integer" keyProperty="goodsId" order="AFTER">
            <!--当前连接最后产生的ID号,回填到goodsId属性中-->
            select last_insert_id() 
        </selectKey>
    </insert>

    测试调用:

    public void testInsert() throws Exception{
        SqlSession session = null;
        try {
            session = MyBatisUtils.openSession();
            Goods goods = new Goods();
            goods.setTitle("测试商品");
            goods.setSubTitle("测试子标题");
            goods.setOriginalCost(200f);
            goods.setCurrentPrice(100f);
            goods.setDiscount(0.5f);
            goods.setIsFreeDelivery(1);
            goods.setCategoryId(43);
            int num = session.insert("goods.insert",goods);
            session.commit();  //提交事务数据
            System.out.println(goods.getGoodsId());
        } catch (Exception e) {
            if (session != null) {
                session.rollback();  //如果出现异常,回滚事务
            }
            throw e;
        } finally {
            if (session != null) {
                session.close();
            }
        }
    }

    需要注意的是:
      a.手动提交事务
      b.如果出现异常,需要回滚数据

    4)selectKey与useGeneratedKeys的区别

    selectKey与useGeneratedKeys都用于在插入数据之后返回最新的主键值.
    (1)selectKey属于insert标签的子标签,必须写在insert标签内

    <insert id="insert" parameterType="com.ikidana.mybatis.entity.Goods">
        INSERT INTO t_goods(title,sub_title,original_cost,current_price,discount,is_free_delivery,category_id)
        VALUES (#{title}, #{subTitle}, #{originalCost}, #{currentPrice}, #{discount}, #{isFreeDelivery}, #{categoryId});
        <selectKey resultType="Integer" keyProperty="goodsId" order="AFTER">
            <!--当前连接最后产生的ID号,回填到goodsId属性中-->
            select last_insert_id()
        </selectKey>
    </insert>

    (2)useGeneratedKeys

    <!--useGeneratedKeys是否会自动的获取主键,默认为false-->
    <!--keyProperty代表那个属性对应主键-->
    <!--keyColumn字段名-->
    <insert id="insert"
            parameterType="com.ikidana.mybatis.entity.Goods"
            useGeneratedKeys="true"
            keyProperty="goodsId"
            keyColumn="goods_id">
        INSERT INTO t_goods(title,sub_title,original_cost,current_price,discount,is_free_delivery,category_id)
        VALUES (#{title}, #{subTitle}, #{originalCost}, #{currentPrice}, #{discount}, #{isFreeDelivery}, #{categoryId});
    </insert>

    二者区别:
      selectKey标签需要明确编写获取最新主键的SQL语句.
      useGeneratedKeys属性会自动根据驱动生成对应的SQL语句.

    应用场景不同:
      selectKey适用于所有的关系型数据库.
      useGeneratedKeys只支持"自增主键"类型的数据库,使用简单.

    selectKey标签是通用方案,适用于所有数据库,但编写麻烦.

    5)改

    <update id="update" parameterType="com.ikidana.mybatis.entity.Goods">
        UPDATE t_goods SET 
        title = #{title},
        sub_title = #{subTitle},
        original_cost = #{originalCost},
        current_price = #{currentPrice},
        discount = #{discount},
        is_free_delivery = #{isFreeDelivery},
        category_id = #{categoryId}
        where goods_id = #{goodsId}
    </update>

    虽然更新可能需要传入多个参数,但是并不需要像select那样,使用map,因为更新就是以对象传入
    测试:

    public void testUpdate() throws Exception{
        SqlSession session = null;
        try {
            session = MyBatisUtils.openSession();
            Goods goods = session.selectOne("goods.selectById",739);
            goods.setSubTitle("更新测试商品"); 
            int num = session.update("goods.update",goods);
            session.commit();  //提交
        } catch (Exception e) {
            if (session != null) {
                session.rollback();  //回滚
            }   
            throw e;
        } finally {
            MyBatisUtils.closeSession(session);
        }   
    }

    稍微比较麻烦的是,更新之前需要先找出需要被更新的对象,不能一步到位.

    6)删除

    <delete id="delete" parameterType="Integer">
        delete from t_goods where goods_id = #{value}
    </delete>

    测试:

    public void testDelete() throws Exception{
        SqlSession session = null;
        try {
            session = MyBatisUtils.openSession();
            int num = session.delete("goods.delete", 739);
            session.commit();  //提交
        } catch (Exception e) {
            if (session != null) {
                session.rollback();  //回滚
            }   
            throw e;
        } finally {
            MyBatisUtils.closeSession(session);
        }   
    }

    7)SQL注入攻击

    SQL注入是指攻击者利用SQL漏洞,绕过系统约束,越权获取数据的攻击方式.

    MyBatis两种传值方式
      ${}文本替换,未经任何处理对SQL文本替换.
      #{}预编译传值,使用预编译传值可以预防SQL注入.

    意思就是使用${}可能会带之SQL注入

    <select id="selectByTitle" parameterType="java.util.Map" resultType="com.ikidana.mybatis.entity.Goods">
        select * from t_goods where title = ${title}
    </select>

    正常传参应该是这样:
      param.put("title","'测试商品'"); //select * from t_goods where title = 测试商品
    如果修改参数:
      param.put("title","''or 1=1 or title='测试商品'");
    那么就会返回数据库中的所有数据。
    因为SQL语句已经变成这样:
      select * from t_goods where title = ‘’ or 1 = 1 or title = ‘测试商品’;
    这条语句在任何情况下都成立。

    但是如果使用#{},就会不存在这个问题。
    之所以需要提供${}文本替换这种方式,主要是有时候你希望传入的是SQL语句,而不是字符串。
    比如 select * from t_goods ${order}
    此时你希望传入的是order by goods_id desc,此时你希望传入SQL子句。

    8)MyBatis工作流程

    3.MyBatis动态SQL

    动态SQL是指根据参数数据动态组织SQL的技术。

    <select id="dynamicSQL" parameterType="java.util.Map" resultType="com.ikidana.mybatis.entity.Goods">
        select * from t_goods
        <where>
            <if test="categoryId != null">
                and category_id = #{categoryId}
            </if>
            <if test="currentPrice != null">
                and current_price &lt; #{currentPrice}
            </if>
        </where>
    </select>

    动态SQL会根据传入的参数来动态的组建SQL语句。

    public void testDynamicSQL() throws Exception{
        SqlSession session = null;
        try {
            session = MyBatisUtils.openSession();
            Map param = new HashMap();
            param.put("categoryId", 44);
            param.put("currentPrice", 500);
            List<Goods> list = session.selectList("goods.dynamicSQL",param);
            for (Goods g: list) {
                System.out.println(g.getTitle() + " : " + g.getCategoryId() + " : " + g.getCurrentPrice());
            }   
        } catch (Exception e){ 
            throw e;
        } finally {
            MyBatisUtils.closeSession(session);
        }   
    }

    <where>标签是可以组建动态SQL语句的核心。

    4.MyBatis二级缓存

    一级缓存默认开启,缓存范围SqlSession会话。
    二级缓存手动开启,属于范围Mapper Namespace。

    二级缓存运行规则:
      二级开启后默认所有查询操作均使用缓存。
      写操作commit提交时对该namespace缓存强制清空。
      配置useCache=false可以使某一条SQL不用缓存。
      配置flushCache=true代表执行某一条SQL之后,对该namespace下所有缓存强制清空。

    public void testLvCache() throws Exception {
        SqlSession session = null;
        try {
            session = MyBatisUtils.openSession();
            Goods goods = session.selectOne("goods.selectById",1602);
            Goods goods1 = session.selectOne("goods.selectById",1602);
            System.out.println(goods.hashCode() + " : " +goods1.hashCode());  //867148091 : 867148091
        } catch (Exception e) {
            throw e;
        } finally {
            MyBatisUtils.closeSession(session);
        }
        //查看执行过程可以看出,其实只执行了一次SQL语句,由于缓存的缘故
        //从内存地址一致,可以知道只查询了一次
        try {
            session = MyBatisUtils.openSession();
            Goods goods = session.selectOne("goods.selectById",1602);
            Goods goods1 = session.selectOne("goods.selectById",1602);
            System.out.println(goods.hashCode() + " : " +goods1.hashCode());  //815674463 : 815674463
        } catch (Exception e) {
            throw e;
        } finally {
            MyBatisUtils.closeSession(session);
        }
        //这是另外一个SqlSession对象,因此又执行了一次查询
        //因此可以看出,一级缓存的生命周期就在一个SqlSession之内
    }

    下面实例验证commit会清空当前namespace下所有缓存:

    public void testLvCache() throws Exception {
        SqlSession session = null;
        try {
            session = MyBatisUtils.openSession();
            Goods goods = session.selectOne("goods.selectById",1602);
            session.commit();
            Goods goods1 = session.selectOne("goods.selectById",1602);
            System.out.println(goods.hashCode() + " : " +goods1.hashCode());  //2051853139 : 815674463
        } catch (Exception e) {
            throw e;
        } finally {
            MyBatisUtils.closeSession(session);
        }
    }

    可以看出一级缓存的缓存时间很短,因此MyBatis提供了二级缓存。

    public void testLvCache2() throws Exception {
        SqlSession session = null;
        try {
            session = MyBatisUtils.openSession();
            Goods goods = session.selectOne("goods.selectById",1602);  //733943822
            System.out.println(goods.hashCode());
        } catch (Exception e) {
            throw e;
        } finally {
            MyBatisUtils.closeSession(session);
        }
    
        try {
            session = MyBatisUtils.openSession();
            Goods goods = session.selectOne("goods.selectById",1602);  //733943822
            System.out.println(goods.hashCode());
        } catch (Exception e) {
            throw e;
        } finally {
            MyBatisUtils.closeSession(session);
        }
    }

    可以看出,即使是不同的对象也会调用一个namespace下面的缓存。
    二级缓存会将对象存储在namespace下面,而不会保存在SqlSession下面,
    这样就延长对象的声明周期,降低了数据库的查询频率,这样降低了数据库的压力。
    在mapper文件中,添加如下配置可以开启二级缓存:

    <mapper namespace="goods">
        <cache eviction="LRU" flushInterval="600000" size="512" readOnly="true"/>
        <!--省略-->
    </mapper>

    参数解释:
      eviction:缓存的清楚策略,当缓存对象梳理达到上限之后,自动触发对应算法对缓存对象清楚。
        (1)LRU:最近最少使用的,移除最长时间不被使用的对象。
        (2)FIFO:先进先出,按对象进入缓存的顺序来移除它们。
        (3)SOFT:软引用,移除基于垃圾回收器和软引用规则的对象。
        (4)WEAK:弱引用,更积极地移除基于垃圾收集器状态和弱引用规则的对象。

      flushInterval:代表间隔多长时间自动清除缓存,单位毫秒,60000毫秒=10分钟。利用这个配置可以有效的让我们对内存进行回收。

      siez:缓存存储上限,用于保存对象或集合(1个集合算1个对象)的数量上限。size的不能太小,不然命中率很低。

      readOnly:设置为true,代表返回只读缓存,每次从缓存中取出的是缓存对象本身,这种执行效率高。
           设置为flase,代表每次取出的是缓存对象的“副本”,每一次取出的对象都是不同的,这种安全性较高,因为缓存的值可能被修改。

    上面的参数是每个实例类的空间设置,还有一些针对某一条SQL的设置:

    (1)useCache是否使用缓存

    <select id="selectByTitle" parameterType="java.util.Map" resultType="com.ikidana.mybatis.entity.Goods" useCache="flase">
        select * from t_goods where title = #{title}
    </select>

    对于这种返回一个表的所有数据,如果全部存储到内存,可能会非常占用资源,而且命中率不会很高。

    (2)flushCache是否立即清空缓存

    <insert id="insert"
            parameterType="com.ikidana.mybatis.entity.Goods"
            useGeneratedKeys="true"
            keyProperty="goodsId"
            keyColumn="goods_id" flushCache="true">
        INSERT INTO t_goods(title,sub_title,original_cost,current_price,discount,is_free_delivery,category_id)
        VALUES (#{title}, #{subTitle}, #{originalCost}, #{currentPrice}, #{discount}, #{isFreeDelivery}, #{categoryId});
    </insert>

    虽然commit能够提交事务,然后清除缓存,但是有时候我们想立即清除缓存,比如新插入一条语句。
    设置了flushcache之后,这条SQL的查询结果也不会被放入缓存。

    5.oneToMang对象关联查询

    表级联查询:通过一个对象,获取与其关联的另一个对象,执行多条SQL语句。
    多表关联查询:两个表通过主外键在一条SQL语句中完成数据的提取。
    数据库中常见的表结构对应关系:

     (1)一对多

     可以看到一个商品对应多个商品信息图片,那么如何获取一个对象的多个关联信息了。

    创建实体类:

    public class GoodsDetail {
        private Integer gdId;
        private Integer goodsId;
        private String gdPicUrl;
        private Integer goOrder;
        //set和get方法省略  
    }

    一对多查询,所以我们需要给商品实例类添加一个属性,好存储这个多个关联对象:

    public class Goods {
        private Integer goodsId;
        private String title;
        private String subTitle;
        private Float originalCost;
        private Float currentPrice;
        private Float discount;
        private Integer isFreeDelivery;
        private Integer categoryId;
        private List<GoodsDetail> goodsDetails;   //创建一个集合类
    }

    创建goods_detail的mapper文件,设置通过goods_id(外键)查询商品信息的语句。

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
            PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="goodsDetail">
        <select id="selectByGoodsId" parameterType="Integer" resultType="com.ikidana.mybatis.entity.GoodsDetail">
            select * from t_goods_detail where goods_id = #{value}
        </select>
    </mapper>

    注册:

    <mapper resource="mappers/goods_detail.xml"/>

    下面是关键部分,如何关联到一起了?

    <resultMap id="rmGoods" type="com.ikidana.mybatis.entity.Goods">
        <!--column主键字段 property对应对象中属性名-->
        <id column="goods_id" property="goodsId"></id>
        <!--collection集合的意思,描述数据的来源-->
        <!--property填充到goods对象的那个属性-->
        <!--select使用那个关联查询语句-->
        <!--column关联列-->
        <collection property="goodsDetails" select="goodsDetail.selectByGoodsId" column="goods_Id"/>
    </resultMap>
    <!--resultMap结果映射,然后找个这个resultMap-->
    <select id="selectOneToMany" resultMap="rmGoods">
        select * from t_goods limit 0,5
    </select>

    其实中间涉及到了两条SQL语句,拿到goods_id再去查询对应的商品信息。最后存储到goodsDetails这个集合属性中。
    最后调用测试:

    public void testOneToMany() throws Exception {
        SqlSession session = null;
        try {
            session = MyBatisUtils.openSession();
            List<Goods> list = session.selectList("goods.selectOneToMany");
            for (Goods goods: list) {
                System.out.println(goods.getTitle() + goods.getGoodsDetails().size());
            }
        } catch (Exception e) {
            throw e;
        } finally {
            MyBatisUtils.closeSession(session);
        }
    }

    调用这一个selectOneToMany方法,就可以获取到其关联的信息了。
    查看执行过程:

    [main] 00:52:40.295 DEBUG goods.selectOneToMany - ==>  Preparing: select * from t_goods limit 0,5 
    [main] 00:52:40.328 DEBUG goods.selectOneToMany - ==> Parameters: 
    [main] 00:52:40.475 DEBUG goodsDetail.selectByGoodsId - ====>  Preparing: select * from t_goods_detail where goods_id = ? 
    [main] 00:52:40.477 DEBUG goodsDetail.selectByGoodsId - ====> Parameters: 740(Integer)
    [main] 00:52:40.570 DEBUG goodsDetail.selectByGoodsId - <====      Total: 11
    [main] 00:52:40.571 DEBUG goodsDetail.selectByGoodsId - ====>  Preparing: select * from t_goods_detail where goods_id = ? 
    [main] 00:52:40.571 DEBUG goodsDetail.selectByGoodsId - ====> Parameters: 741(Integer)
    [main] 00:52:40.656 DEBUG goodsDetail.selectByGoodsId - <====      Total: 6
    [main] 00:52:40.658 DEBUG goodsDetail.selectByGoodsId - ====>  Preparing: select * from t_goods_detail where goods_id = ? 
    [main] 00:52:40.659 DEBUG goodsDetail.selectByGoodsId - ====> Parameters: 742(Integer)
    [main] 00:52:40.759 DEBUG goodsDetail.selectByGoodsId - <====      Total: 22
    [main] 00:52:40.760 DEBUG goodsDetail.selectByGoodsId - ====>  Preparing: select * from t_goods_detail where goods_id = ? 
    [main] 00:52:40.760 DEBUG goodsDetail.selectByGoodsId - ====> Parameters: 743(Integer)
    [main] 00:52:40.852 DEBUG goodsDetail.selectByGoodsId - <====      Total: 14
    [main] 00:52:40.854 DEBUG goodsDetail.selectByGoodsId - ====>  Preparing: select * from t_goods_detail where goods_id = ? 
    [main] 00:52:40.855 DEBUG goodsDetail.selectByGoodsId - ====> Parameters: 744(Integer)
    [main] 00:52:40.945 DEBUG goodsDetail.selectByGoodsId - <====      Total: 12
    [main] 00:52:40.946 DEBUG goods.selectOneToMany - <==      Total: 5
    爱恩幼 孕妇护肤品润养颜睡眠面膜 100g11
    斯利安 孕妈专用 洗发水 氨基酸表面活性剂 舒缓头皮 滋养发根 让你的秀发会喝水 品质孕妈6
    亲恩 孕妇护肤品 燕窝补水保湿6件套 孕期安全温和 补水保湿套装22
    优美孕 补水保湿 黄金果水润嫩肤三件套(中样装 洁面乳50g 水50ml 乳液50ml)14
    雅滋美特 孕妇护肤品天然叶酸补水保湿三件化妆品套装12

    我们不难发现,获取到goods_id之后,再通过另一条SQL语句获取查询目标信息。

    (2)多对一

    对象多对一的关联查询与一对多是类似的。
    在GoodsDetail实例类中添加属性private Goods goods;
    在goods_detail.xml文件中添加selectManyToOne多对一查询方法。

    <resultMap id="rmGoodsDetail" type="com.ikidana.mybatis.entity.GoodsDetail">
        <id column="gd_id" property="gdId"/>  <!--主键-->
        <association property="goods" select="goods.selectById" column="goods_id"/>  <!--通过那个字段来关联-->
    </resultMap>
    <select id="selectManyToOne" resultMap="rmGoodsDetail">
        select * from t_goods_detail limit 20,21
    </select>

    测试:

    public void testManyToOne() throws Exception {
        SqlSession session = null;
        try {
            session = MyBatisUtils.openSession();
            List<GoodsDetail> list = session.selectList("goodsDetail.selectManyToOne");
            for (GoodsDetail gd:list){
                System.out.println(gd.getGdPicUrl() + " : " + gd.getGoods().getTitle());
            }
        } catch (Exception e) {
            throw e;
        } finally {
            MyBatisUtils.closeSession(session);
        }
    }

    5.PageHelper分页

    在之前的代码编写中,分页多是根据前端参数参数来计算起始点。
    而PageHelper直接在后端逻辑层面帮我们完成了分页的事项。

    PageHelper使用流程:
      maven引入PageHelper与jsqlparser
      mybatis-config.xml增加Plugin配置
      代码中使用PageHelper.startPage()自动分页

    (1)在pom.xml中引入项目依赖

    <!--分页组件-->
    <!--pagehelper是在原有SQL基础上,进行分析,自动生成分页等语句-->
    <!--jsqlparser用于对原始SQL的解析工作-->
    <dependency>
        <groupId>com.github.pagehelper</groupId>
        <artifactId>pagehelper</artifactId>
        <version>5.1.10</version>
    </dependency>
    <dependency>
        <groupId>com.github.jsqlparser</groupId>
        <artifactId>jsqlparser</artifactId>
        <version>2.0</version>
    </dependency>

    (2)在mybatis-config.xml中增加项目配置

    <!--配置拦截器-->
    <plugins>
        <plugin interceptor="com.github.pagehelper.PageInterceptor">
            <!--helperDialect:分页插件会自动检测当前的数据库链接,自动选择合适的分页方式。此处配置使用mysql数据库-->
            <!--即使不配置这样一项,也是自动分页,因为JDBC已经配置了-->
            <property name="helperDialect" value="mysql"/>
            <!--分页合理化-->
            <property name="reasonable" value="true"/>
        </plugin>
    </plugins>

    (3)使用PageHelper.startPage()自动分页

    增加SQL查询方法:

    <select id="selectPage" resultType="com.ikidana.mybatis.entity.Goods">
        select * from t_goods where current_price &lt; 1000
    </select>

    添加Java查询逻辑:

    public void testSelectPage() throws Exception {
        SqlSession session = null;
        try {
            session = MyBatisUtils.openSession();
            PageHelper.startPage(2,10);
            Page<Goods> page = (Page) session.selectList("goods.selectPage");
            System.out.println("总页数:" + page.getPages());
            System.out.println("总记录数:" + page.getTotal());
            System.out.println("开始行号:" + page.getStartRow());
            System.out.println("结束行号:" + page.getEndRow());
            System.out.println("当前页面:" + page.getPageNum());
            List<Goods> data = page.getResult();  //当前页数据
            for (Goods g: data) {
                System.out.println(g.getTitle());
            }
        } catch (Exception e) {
            throw e;
        } finally {
            MyBatisUtils.closeSession(session);
        }
    }

    查看执行过程:

    [main] 18:41:24.755 DEBUG goods.selectPage_COUNT - ==>  Preparing: SELECT count(0) FROM t_goods WHERE current_price < 1000 
    [main] 18:41:24.807 DEBUG goods.selectPage_COUNT - ==> Parameters: 
    [main] 18:41:24.889 DEBUG goods.selectPage_COUNT - <==      Total: 1
    [main] 18:41:24.898 DEBUG goods - Cache Hit Ratio [goods]: 0.0
    [main] 18:41:24.898 DEBUG goods.selectPage - ==>  Preparing: select * from t_goods where current_price < 1000 LIMIT ?, ? 
    [main] 18:41:24.899 DEBUG goods.selectPage - ==> Parameters: 10(Integer), 10(Integer)
    [main] 18:41:24.986 DEBUG goods.selectPage - <==      Total: 10

    其实底层自己帮我们做了分页查询。

    6.MyBatis整合C3P0连接池

    MyBatis提供了自带的连接池,但是其使用效果并不像C3P0那样优异。下面讲述如何在MyBatis引用C3P0

    (1)添加C3P0依赖包

    <dependency>
        <groupId>com.mchange</groupId>
        <artifactId>c3p0</artifactId>
        <version>0.9.5.4</version>
    </dependency>

    (2)添加工厂类

    package com.ikidana.mybatis.datasource;
    
    import com.mchange.v2.c3p0.ComboPooledDataSource;
    import org.apache.ibatis.datasource.unpooled.UnpooledDataSourceFactory;
    
    /*
    * C3P0与MyBatis兼容使用的数据源工厂类
    * */
    public class C3P0DataSourceFactory extends UnpooledDataSourceFactory {
        public C3P0DataSourceFactory(){
            //数据源由C3P0负责创建
            this.dataSource = new ComboPooledDataSource();
        }
    }

    (3)修改mybatis-config.xml配置文件

    <!--            <dataSource type="POOLED">-->
    <!--                <property name="driver" value="com.mysql.jdbc.Driver"/>-->
    <!--                <property name="url"-->
    <!--                          value="jdbc:mysql://148.70.251.10:3306/babytun?useUnicode=yes&amp;characterEncoding=utf8"/>-->
    <!--                <property name="username" value="root"/>-->
    <!--                <property name="password" value="123456"/>-->
    <!--            </dataSource>-->
                <dataSource type="com.ikidana.mybatis.datasource.C3P0DataSourceFactory">
                    <property name="driverClass" value="com.mysql.jdbc.Driver"/>
                    <property name="jdbcUrl"
                              value="jdbc:mysql://148.70.251.10:3306/babytun?useUnicode=yes&amp;characterEncoding=utf8"/>
                    <property name="user" value="root"/>
                    <property name="password" value="123456"/>
                    <property name="initialPoolSize" value="5"/>  <!--初始数据库连接数量-->
                    <property name="maxPoolSize" value="20"/>     <!--最大连接数量-->
                    <property name="minPoolSize" value="5"/>      <!--最小连接数量-->
                </dataSource>

    上面其实修改了datasource的应用地址

    随意执行一个SQL语句,查看日志:

    [main] 20:19:22.784 DEBUG com.mchange.v2.cfg.MConfig - The configuration file for resource identifier '/mchange-commons.properties' could not be found. Skipping.
    [main] 20:19:22.784 DEBUG com.mchange.v2.cfg.MConfig - The configuration file for resource identifier '/mchange-log.properties' could not be found. Skipping.
    [main] 20:19:22.784 DEBUG com.mchange.v2.cfg.MConfig - The configuration file for resource identifier '/c3p0.properties' could not be found. Skipping.
    [main] 20:19:22.784 DEBUG com.mchange.v2.cfg.MConfig - The configuration file for resource identifier 'hocon:/reference,/application,/c3p0,/' could not be found. Skipping.
    [main] 20:19:22.786 DEBUG c.m.v.resourcepool.BasicResourcePool - com.mchange.v2.resourcepool.BasicResourcePool@4135c3b config: [start -> 5; min -> 5; max -> 20; inc -> 3; num_acq_attempts -> 30; acq_attempt_delay -> 1000; check_idle_resources_delay -> 0; max_resource_age -> 0; max_idle_time -> 0; excess_max_idle_time -> 0; destroy_unreturned_resc_time -> 0; expiration_enforcement_delay -> 0; break_on_acquisition_failure -> false; debug_store_checkout_exceptions -> false; force_synchronous_checkins -> false]
    [main] 20:19:22.786 DEBUG c.m.v.c.i.C3P0PooledConnectionPoolManager - Created new pool for auth, username (masked): 'ro******'.
    [main] 20:19:22.786 DEBUG c.m.v.resourcepool.BasicResourcePool - acquire test -- pool size: 0; target_pool_size: 5; desired target? 1
    [main] 20:19:22.786 DEBUG c.m.v.resourcepool.BasicResourcePool - awaitAvailable(): [unknown]
    [main] 20:19:23.711 DEBUG o.a.i.t.jdbc.JdbcTransaction - Setting autocommit to false on JDBC Connection [com.mchange.v2.c3p0.impl.NewProxyConnection@2a4fb17b [wrapping: com.mysql.jdbc.JDBC4Connection@5c6648b0]]
    [main] 20:19:23.802 DEBUG goods.selectPage_COUNT - ==>  Preparing: SELECT count(0) FROM t_goods WHERE current_price < 1000 

    7.MyBatis批处理

    如果我们需要向数据库中添加一大批数据,一条的进行插入肯定会非常缓慢,所以可以使用一些批处理的技巧。
    (1)添加批量删除的SQL语句

    <insert id="batchInsert" parameterType="java.util.List">
        INSERT INTO t_goods(title,sub_title,original_cost,current_price,discount,is_free_delivery,category_id)
        VALUES
        <foreach collection="list" item="item" index="index" separator=",">
            (#{item.title}, #{item.subTitle}, #{item.originalCost}, #{item.currentPrice}, #{item.discount}, #{item.isFreeDelivery}, #{item.categoryId})
        </foreach>
    </insert>

    (2)逻辑语句

        public void testBatchInsert() throws Exception {
            SqlSession session = null;
            try {
                long st = new Date().getTime();
                session = MyBatisUtils.openSession();
                List list = new ArrayList();
                for (int i = 0; i < 10000; i++) {
                    Goods goods = new Goods();
                    goods.setTitle("测试商品");
                    goods.setSubTitle("测试子标题");
                    goods.setOriginalCost(200f);
                    goods.setCurrentPrice(100f);
                    goods.setDiscount(0.5f);
                    goods.setIsFreeDelivery(1);
                    goods.setCategoryId(43);
                    //insert()方法返回值代表本次成功插入的记录总数
                    list.add(goods);
                }
                session.insert("goods.batchInsert", list);
                session.commit();//提交事务数据
                long et = new Date().getTime();
                System.out.println("执行时间:" + (et - st) + "毫秒");
    //            System.out.println(goods.getGoodsId());
            } catch (Exception e) {
                if (session != null) {
                    session.rollback();//回滚事务
                }
                throw e;
            } finally {
                MyBatisUtils.closeSession(session);
            }
        }

    批量插入数据的局限:
      (1)无法获得插入数据的id
      (2)批量生成的SQL太长,可能被服务器拒绝

    8.MyBatis注解开发

     在Java中合理使用注解,可以不需要编写XML文件,从而就可以操作数据。
    比如我们现在需要查询某一范围内的数据,流程如下:

    (1)编辑接口文件GoodsDAO.java

    public interface GoodsDAO {
        @Select("select * from t_goods where current_price between  #{min} and #{max} order by current_price limit 0,#{limt}")
        public List<Goods> selectByPriceRange(@Param("min") Float min ,@Param("max") Float max ,@Param("limt") Integer limt);
    }

    (2)在mybatis-config.xml中注册

    <mappers>
        <!--以下两种注册方式都有效-->
        <!--<mapper class="com.imooc.mybatis.dao.GoodsDAO"/>-->
        <package name="com.imooc.mybatis.dao"/>
    </mappers>

    (3)调用

    public void testSelectByPriceRange() throws Exception {
        SqlSession session = null;
        try{
            session = MyBatisUtils.openSession();
            //获取映射器,GoodsDAO是接口
            //这里的GoodsDAO虽然是接口,在实际运行的时候,session会根据goodsDAO里面的配置信息来动态生成goodsDAO的实现类
            //之后直接用goodsDAO里面的方式来操作数据即可
            GoodsDAO goodsDAO = session.getMapper(GoodsDAO.class);
            List<Goods> list = goodsDAO.selectByPriceRange(100f, 500f, 20);
            System.out.println(list.size());
        }catch (Exception e){
            throw e;
        } finally {
            MyBatisUtils.closeSession(session);
    
        }
    }

    如果需要插入数据,可以这样定义:

    @Insert("INSERT INTO t_goods(title, sub_title, original_cost, current_price, discount, is_free_delivery, category_id) VALUES (#{title} , #{subTitle} , #{originalCost}, #{currentPrice}, #{discount}, #{isFreeDelivery}, #{categoryId})")
    //<selectKey>
    @SelectKey(statement = "select last_insert_id()" , before = false , keyProperty = "goodsId" , resultType = Integer.class)
    public int insert(Goods goods);

    然后这样调用:

    public void testInsert() throws Exception {
        SqlSession session = null;
        try{
            session = MyBatisUtils.openSession();
            Goods goods = new Goods();
            goods.setTitle("测试商品");
            goods.setSubTitle("测试子标题");
            goods.setOriginalCost(200f);
            goods.setCurrentPrice(100f);
            goods.setDiscount(0.5f);
            goods.setIsFreeDelivery(1);
            goods.setCategoryId(43);
            GoodsDAO goodsDAO = session.getMapper(GoodsDAO.class);
            //insert()方法返回值代表本次成功插入的记录总数
            int num = goodsDAO.insert(goods);
            session.commit();//提交事务数据
            System.out.println(goods.getGoodsId());
        }catch (Exception e){
            if(session != null){
                session.rollback();//回滚事务
            }
            throw e;
        }finally {
            MyBatisUtils.closeSession(session);
        }
    }

    那么结果映射该如何实现了?

    @Select("select * from t_goods")
    //<resultMap>
    @Results({
            //<id>
          @Result(column = "goods_id" ,property = "goodsId" , id = true) ,
            //<result>
            @Result(column = "title" ,property = "title"),
            @Result(column = "current_price" ,property = "currentPrice")
    })
    public List<GoodsDTO> selectAll();

    调用:

    public void testSelectAll() throws Exception {
        SqlSession session = null;
        try{
            session = MyBatisUtils.openSession();
            GoodsDAO goodsDAO = session.getMapper(GoodsDAO.class);
            List<GoodsDTO> list = goodsDAO.selectAll();
            System.out.println(list.size());
        }catch (Exception e){
            throw e;
        } finally {
            MyBatisUtils.closeSession(session);
    
        }
    }
  • 相关阅读:
    直播分享| 腾讯云 MongoDB 智能诊断及性能优化实践
    解决方案| MongoDB PSA 架构痛点以及如何应对?
    行业案例 | MongoDB 在 QQ 小世界 Feed 云系统中的应用及业务架构优化实践
    活动报名 | MongoDB 5.0 时序存储特性介绍
    行业报告| 请查收 2022 MongoDB 数据与创新报告
    解决方案、线上活动| MongoDB Page Fault 的发现与解决、腾讯云 MongoDB 智能诊断及优化实践
    行业案例| 千亿级高并发MongoDB集群在某头部金融机构中的应用及性能优化实践(上)
    公司的午休神器,随时关闭电脑屏幕
    golang项目中使用异步任务
    统计程序耗时神器
  • 原文地址:https://www.cnblogs.com/yangmingxianshen/p/12518455.html
Copyright © 2020-2023  润新知