2020:0605 --- mybatis(三)

239 阅读15分钟

今日内容

1.mybatis中的连接池以及事务控制     ----    原理了解,会用即可
    mybatis中连接池使用及分析
    mybatis事务控制的分析
2. mybatis基于XML配置的动态SQL语句使用  ----  会用即可
    mappers配置文件中的几个标签
        <if>
        <where>
        <foreach>
        <sql>
3. mybatis中的多表操作      -----       掌握应用
    一对多
    一对一
    多对多

1.连接池

1.1 连接池概述

    我们在实际开发中都会使用连接池,因为它可以减少我们获取连接所消耗的时间。
    连接池就好比一个容器一样,把连接都初始化出来放到一个容器中。
    我们从容器中取一个用一个,那么怎么取就成为一个很关键的事情?
    
    为什么要一开始就初始化这么多连接?到的时候再初始化一个不行吗?
        因为我们在实际开发中,获取连接的操作是很费时的。
        
    * 连接池
        连接池就是用于存储连接的一个容器,这个容器是一个集合对象,而且该集合对象必须是线程安全的,
    不能两个线程拿到同一个连接。
        该集合还必须实现队列的特性:先进先出。
        而且连接用完之后,还会返回给连接池。

1.2 mybatis中的连接池

    mybatis连接池提供了三种方式的配置:
        * 配置的位置:
            主配置文件SqlMapConfig.xml中的dataSource标签
        type属性表示采用何种连接池方式
        
        * Mybatis连接池的分类
        POOLED:采用传统的javax.sql.DataSource规范中的连接池,mybatis中有针对规范的实现。
                使用连接池的数据源
        UNPOOLED:采用传统的获取连接的方式,虽然也实现Javax.sql.DataSource接口,但是并没有使用池的思想。
                  意味着没有池的思想,每次获取连接都要重新初始化一个连接。
                  不使用连接池的数据源。
        JNDI:采用服务器提供的JNDI技术实现,来获取DataSource对象,不同的服务器所能拿到的DataSource是不一样。
              使用JNDI实现的数据源。
              注意:如果不是web或者maven的war工程,是不能使用的。
              我们课程中使用的是tomcat服务器,采用的连接池就是dabcp连接池

        * Mybatis中和连接池有关的相应的类
            package javax.sql;
                这个就是JDBC规范中的连接池定义,这个连接池定义里面一定有一个方法:
                    Connection getConnection()
            package org.apache.ibatis.datasource.pooled;
                会重写Connection getConnection(){}方法
            package org.apache.ibatis.datasource.unpooled;
                会重写Connection getConnection(){}方法。
                走的还是注册驱动,获取连接这一套。

        ```
        <configuration>
            <environments default="mysql">
                <environment id="mysql">
                    <!--配置事务-->
                    <transactionManager type="JDBC"></transactionManager>
                    <dataSource type="POOLED">
                        <property name="driver" value="${jdbc.driver}"/>
                        <property name="url" value="${jdbc.url}"/>
                        <property name="username" value="${jdbc.username}"/>
                        <property name="password" value="${jdbc.password}"/>
                    </dataSource>
                </environment>
            </environments>
        </configuration>
        ```

1.3 mybatis提供连接池的思想

1.3.1 UNPOOLED
    * 采用传统的获取连接的方式,虽然也实现Javax.sql.DataSource接口,但是并没有使用池的思想。
      意味着没有池的思想,每次获取连接都要重新初始化一个连接,不使用连接池的数据源。
    * 走的还是注册驱动,获取连接这一套。
1.3.2 POOLED
    * 采用的是javax.sql.DataSource规范中的连接池,mybatis中有针对规范的实现。
      使用连接池的数据源
    * Mybatis提供POOLED连接池的过程

    * 既没有用c3p0也没有druid等,而是自己实现了一套连接池的思想。也是使用的jdbc规范中的javax.sql.datasource;
      接口而实现的方式

1.4. mybatis中的事务

    什么是事务
    事务的四大特性ACID
    不考虑隔离性会产生的3个问题
    解决办法:四种隔离级别
    (在面试的时候要能达的上来)
    
    mybatis中的事务,是通过sqlsession对象的commit方法和rollback方法实现事务的提交和回滚。
    而且走到最后,还是走到connection.commit/connection.rollback
    
    * Transaction  - Setting autocommit to false :
        Transaction将自动提交设为false,导致我们每次要自己手动提交sqlsession.commit
        那么如何实现自动提交呢?
        
        我们发现在public interface SqlSessionFactory中的 SqlSession openSession()方法另一种重写形式:

        所以我们在创建SqlSession对象时,用的是factory.openSession(true),就可以实现自动提交事务。

2. mybatis基于XML配置的动态SQL语句使用

        mybatis支持一些动态的sql语句,这些语句都是和查询相关的。我们在写查询的时候,会面临一些查询条件。
    这些条件有可能有也可能没有,而且条件的数量也是不固定的。那么我们怎么设定这种sql语句呢?

2.1 动态SQL之标签

    * <if>标签就可以根据不同的情况帮我们拼接查询条件。
    ```
        private Integer id;
        private String username;
        private String address;
        private String sex;
        private Date birthday;
        
        <!--根据条件查询-->
        <select id="findUserByCondition" resultType="com.itheima.domain.User" parameterType="com.itheima.domain.User">
            select * from user where 1 = 1
            <!--判断传入的参数user中是否有username属性-->
            <if test="username != null">
                and username like #{username}
            </if>
            <!--判断传入的参数user中是否有sex属性-->
            <if test="sex != null">
                and sex = #{sex}
            </if>
            <if test="address != null">
                and address = #{address}
            </if>
            <if test="id != null">
                and id = #{id}
            </if>
            <if test="birthday != null">
                and birthday = #{birthday}
            </if>
        </select>
    ```
    * 这在一定程度上达到了动态SQL的目的,但是不好的一点就是:如果实体类有许多许多属性,那么就要写许多许多<if>标签。

2.2 动态SQL之标签 : 不用写where 1 = 1

将where 1 = 1去掉了
```
    <select id="findUserByCondition" resultType="com.itheima.domain.User" parameterType="com.itheima.domain.User">
        select * from user
        <where>
            <if test="username != null">
                and username like #{username}
            </if>
            <if test="sex != null">
                and sex = #{sex}
            </if>
            <if test="address != null">
                and address = #{address}
            </if>
            <if test="id != null">
                and id = #{id}
            </if>
            <if test="birthday != null">
                and birthday = #{birthday}
            </if>
        </where>
    </select>
```

2.3 动态SQL之标签

    解决这种子查询:select * from user where id in(41, 42, 43, 45)
    ```
        <!--根据QueryVo中提供的id集合,查询用户信息-->
        <select id="findUserInIds" resultType="com.itheima.domain.User" parameterType="com.itheima.domain.QueryVo">
            select * from user
            <where>
                <if test="ids != null and ids.size() > 0">
                    <foreach collection="ids" open="and id in (" close=")" item="uid" separator=",">
                        <!--和item="uid"要一致-->
                        #{uid}
                    </foreach>
                </if>
            </where>
        </select>
    ```

3. mybatis中的多表查询

    表之间的关系有几种:
        一对多
        多对一
        一对一
        多对多
    举例:
        一个用户可以下多个订单  : 一对多
        多个订单属于同一个用户  : 多对一
        一个人只能有一张身份证  : 一对一
        
        多个学生可以被多个老师教过  : 多对多
        多个老师可以教多个学生
    特例:
        如果拿出每一个订单,他都属于每一个用户
        所以Mybatis就把多对一看成了一对一。
    
    mybatis中的多表查询:
        示例:用户和账户
                一个用户可以有多个账户
                一个账户只能属于一个用户(多个账户也可以属于同一个用户)
        步骤:
            1.建立两张表:用户表,账户表
                让用户表和账户表之间具备一对多的关系:需要使用外键在账户表中添加
            2.建立两个实体类:用户实体类和账户实体类
                让用户和账户的实体类能体现出来一对多的关系
            3.建立两个配置文件
                用户的配置文件
                账户的配置文件
            4.实现配置
                当我们查询用户时,可以同时得到用户下所包含的账户信息
                当我们查询账户时,可以同时得到账户的所属用户信息。

3.1 需求 查询所有账户,并且带有所属用户的用户名称和地址信息

3.1.1 代码实现1:不常用的方式
    * sql:select a.*, u.username, u.address from user u, account a where u.id = a.uid
    ```
    * 1.Account表对应的实体类
    public class Account implements Serializable {

        private Integer id;
        private Integer uid;
        private Double money;
    
        public Integer getId() {
            return id;
        }
    
        public void setId(Integer id) {
            this.id = id;
        }
    
        public Integer getUid() {
            return uid;
        }
    
        public void setUid(Integer uid) {
            this.uid = uid;
        }
    
        public Double getMoney() {
            return money;
        }
    
        public void setMoney(Double money) {
            this.money = money;
        }
    
        @Override
        public String toString() {
            return "Account{" +
                    "id=" + id +
                    ", uid=" + uid +
                    ", money=" + money +
                    '}';
        }
    }
    
    * 2.查询得到的所属用户的用户名称和地址信息封装成到:AccountUser类
    public class AccountUser extends Account {

        //AccountUser继承Account只是为了能打印出Account表的信息
        //将查询得到的username和address封装到这个类中
        private String username;
        private String address;
    
        public String getUsername() {
            return username;
        }
    
        public void setUsername(String username) {
            this.username = username;
        }
    
        public String getAddress() {
            return address;
        }
    
        public void setAddress(String address) {
            this.address = address;
        }
    
        @Override
        public String toString() {
            return super.toString() + "              AccountUser{" +
                    "username='" + username + '\'' +
                    ", address='" + address + '\'' +
                    '}';
        }
    }
    
    3. IAccountDao
    List<AccountUser> findAllAccount();
    
    4. AccountTest
        /**
         * 测试查询所有账户,并且带有所属用户的用户名称和地址信息
         *    select a.*, u.username, u.address from user u, account a where u.id = a.uid
         * @return
         */
        @Test
        public void testFindAllAccountUser(){
            List<AccountUser> accountUsers = accountDao.findAllAccount();
            for (AccountUser accountUser : accountUsers) {
                System.out.println(accountUser);
            }
        }
    
    5.IAccountDao.xml
       <!--查询所有账户,并且带有用户名称和地址信息-->
        <select id="findAllAccount" resultType="com.itheima.domain.AccountUser">
            select a.*, u.username, u.address from user u, account a where u.id = a.uid
        </select>
    ```
3.1.2 代码实现2:常用的方式
    写一个映射标签<resultMap>
    <resultMap id="accountUserMap" type="com.itheima.domain.Account">
        <!--主键字段的对应-->
        <!--这里account的id列在下面的sql中起了别名,所以这里也用别名-->
        <id property="id" column="aid"></id>
        <!--非主键字段的对应-->
        <result property="uid" column="uid"></result>
        <result property="money" column="money"></result>
        <!--一对一的关系映射:配置封装user表中的信息-->
        <!--javaType指明信息封装到哪个对象-->
        <association property="user" column="uid" javaType="com.itheima.domain.User">
            <id property="id" column="id"></id>
            <result column="username" property="username"></result>
            <result column="sex" property="sex"></result>
            <result column="address" property="address"></result>
            <result column="birthday" property="birthday"></result>
        </association>
    </resultMap>
    再把<resultMap>标签放到<select>标签里面。
    
    * 从表实体应该包含一个主表实体的对象引用
      使用resultMap在IAccountDao.xml定义专门的resultMap用于映射一对一查询结果
      我们可以在Account类中加入一个User类的对象来代表这个账户是哪个用户的。
    ```
    * 1.Account表对应的实体类
        private Integer id;
        private Integer uid;
        private Double money;
    
        //从表实体应该包含一个主表实体的对象引用
        private User user;
        
    * 2.IAccountDao
        List<AccountUser> findAllAccount();
        
    * 3.IAccountDao.xml
        <resultMap id="accountUserMap" type="com.itheima.domain.Account">
            <!--主键字段的对应-->
            <!--这里account的id列在下面的sql中起了别名,所以这里也用别名-->
            <id property="id" column="aid"></id>
            <!--非主键字段的对应-->
            <result property="uid" column="uid"></result>
            <result property="money" column="money"></result>
            <!--一对一的关系映射:配置封装user表中的信息-->
            <!--javaType指明信息封装到哪个对象-->
            <association property="user" column="uid" javaType="com.itheima.domain.User">
                <id property="id" column="id"></id>
                <result column="username" property="username"></result>
                <result column="sex" property="sex"></result>
                <result column="address" property="address"></result>
                <result column="birthday" property="birthday"></result>
            </association>
        </resultMap>
    
    
        <!--查询所有方法-->
        <select id="findAll" resultMap="accountUserMap">
            <!--因为这里的结果:user表中出现了id列,account表中也有id列,所以我们为其中一个id列起别名a.id as aid-->
            select u.*, a.id as aid, a.uid, a.money from account a, user u where u.id = a.uid;
        </select>
    ```

3.2 一对多

    需求:查询所有用户,同时获取用户下所有账户的信息
    1.SQL语句:不能用内连接,要用左外连接(将LEFT OUTER JOIN左边的数据都查询出来)。
        内连接:必行
        SELECT * FROM USER u, account a WHERE u.id = a.uid
        左外连接:
        SELECT u.*, a.`ID` AS aid, a.`MONEY`, a.`UID` FROM USER u LEFT OUTER JOIN account a ON u.id = a.`UID`
        或者右连接也行:
        SELECT a.`ID` AS aid, a.`MONEY`, a.`UID`, u.* FROM account a RIGHT OUTER JOIN USER u ON u.id = a.`UID`
    
    2.  User实体类
        //一对多关系映射:每个用户可能有多个账户
        //主表实体应该包含从表实体的集合引用(因为是一对多)
        private List<Account> accounts;

    ```
        <!--定义User的resultMap-->
        <resultMap id="userAccountMap" type="com.itheima.domain.User">
            <id property="id" column="id"></id>
            <result property="username" column="username"></result>
            <result property="sex" column="sex"></result>
            <result property="address" column="address"></result>
            <result property="birthday" column="birthday"></result>
            <!--配置 user对象中account集合的映射-->
            <collection property="accounts" ofType="com.itheima.domain.Account">
                <id property="id" column="aid"></id>
                <result property="uid" column="uid"></result>
                <result property="money" column="money"></result>
            </collection>
        </resultMap>
        
        <!--查询所有方法-->
        <select id="findAll" resultMap="userAccountMap">
            SELECT u.*, a.id as aid, a.uid, a.money FROM USER u LEFT OUTER JOIN account a ON u.id = a.`UID`
        </select>
    ```

3.3 多对多关系

    示例:用户和角色
        一个用户可以有多个角色:用户老王可以是院长角色也可以是总裁角色
        一个角色可以赋予多个用户:可以同时有多个院长角色:老王和老李
    步骤:
        1.建立两张表:用户表,角色表
            让用户表和角色表具有多对多的关系。
            数据库中实现多对多的关系,需要使用中间表,中间表中包含各自的主键,在中间表中是外键。
        2.建立两个实体类:用户实体类和角色实体类
            让用户和角色的实体类能体现出多对多的关系
            各自包含对方一个集合引用
        3.建立两个配置文件
            用户的配置文件
            角色的配置文件
        4.实现配置
            当我们查询用户时,可以同时得到用户所包含的角色信息
            当我们查询角色时,可以同时得到角色的所赋予的用户信息。
3.3.1 从角色表查,得到全部的角色的信息,同时得到所赋予的用户信息。

    思路:

    * 在完善一下想要得到的结果:
    sql: 
        SELECT u.*, r.id AS rid, r.role_name, r.role_desc FROM role r LEFT OUTER JOIN user_role ur ON r.id = ur.rid 
        LEFT OUTER JOIN USER u ON ur.uid = u.id

3.3.2 从用户表查,得到全部的用户信息,并同时得到用户所包含的角色信息
    并不是所有用户都带着角色:有角色带着角色,没有就null
    思路:同4.1一样,所以只要sql改一下就行了
    sql:
    SELECT u.*, r.id AS rid, r.role_name, r.role_desc FROM USER u LEFT OUTER JOIN user_role ur ON u.id = ur.uid LEFT OUTER JOIN role r ON r.id = ur.rid

4.JNDI

    概述:Java Naming and Directory Interface。
        是SUN公司推出的一套规范,属于JavaEE技术之一。目的是模仿windows系统中的注册表。
        示例:在服务器中注册数据源
        1.他在我们这里边是基于tomcat服务器的:在服务器中注册数据源
            
        2.当tomcat一启动(所以必须是war工程),也会为我们准备这样一个map:那我们按照规定向里面写一写东西即可
            key:字符串 
                directory:固定的
                name:可以指定的
            value:Object  
                存放什么对象是我们自己制定的,指定的方式是通过配置文件的方式。/webapp/META-INF/context.xml
        3.JNDI是以这种形式存放的(map:键值对)

        4.这里我们SqlMapConfig.xml:也有相应的改变
            以前是引入外部连接的配置,
            改成直接指定数据源:配置在context.xml
            原来我们是通过写在外部的properties.xml文件读取的,现在我们是通过指定一个dataSource,
        并且value就是context.xml对应的路径+名称(url+name)

    思考: 在java工程中能不能实现这个功能呢?
           很显然是不能的,只能在war(web)工程。
           
    想用JNDI那么必须做下面这些:
        1.创建一个mavenweb工程
        2.在webapp目录下创建META-INF目录,并在里面创建配置文件      context.xml
            context.xml:
            ```
            <?xml version="1.0" encoding="UTF-8"?>
            <Context>
            <!-- 
            <Resource 
            name="jdbc/eesy_mybatis"						数据源的名称
            type="javax.sql.DataSource"						数据源类型
            auth="Container"								数据源提供者
            maxActive="20"									最大活动数
            maxWait="10000"									最大等待时间
            maxIdle="5"										最大空闲数
            username="root"									用户名
            password="1234"									密码
            driverClassName="com.mysql.jdbc.Driver"			驱动类
            url="jdbc:mysql://localhost:3306/eesy_mybatis"	连接url字符串
            />
             -->
            <Resource 
            name="jdbc/txl_mybatis"     ---------------  是我们自己指定的名称
            type="javax.sql.DataSource" ---------------  要存的对象,这里指定是一个数据源
            auth="Container"            --------------   提供者(作者),由容器来提供数据源,这里指的是tomcat
            maxActive="20"              -------------    最大活动链接
            maxWait="10000"             --------------   最大等待时长
            maxIdle="5"                 --------------   最大空闲数
            username="root"
            password="1234"
            driverClassName="com.mysql.jdbc.Driver"
            url="jdbc:mysql://localhost:3306/eesy_mybatis"
            />
            </Context>
            ```
            
          3. SqlMapConfig.xml:也有相应的改变  
            1.SqlMapConfig.xml(原来的)    
            <configuration>
    
                <properties resource="jdbcConfig.properties"></properties>
            
                <typeAliases>
                    <package name="com.itheima.domain"/>
                </typeAliases>
            
                <!--配置环境-->
                <environments default="mysql">
                    <!--配置mysql的环境-->
                    <environment id="mysql">
                        <!--配置事务-->
                        <transactionManager type="JDBC"></transactionManager>
                        <dataSource type="UNPOOLED">
                            <property name="driver" value="${jdbc.driver}"/>
                            <property name="url" value="${jdbc.url}"/>
                            <property name="username" value="${jdbc.username}"/>
                            <property name="password" value="${jdbc.password}"/>
                        </dataSource>
                    </environment>
                </environments>
                <!--映射文件的配置-->
                <mappers>
                    <package name="com.itheima.Dao"/>
                </mappers>
            </configuration>   
            
            2.SqlMapConfig.xml(JNDI) 
            <configuration>
                <typeAliases>
                    <package name="com.itheima.domain"></package>
                </typeAliases>
                <!-- 配置mybatis的环境 -->
                <environments default="mysql">
                    <!-- 配置mysql的环境 -->
                    <environment id="mysql">
                        <!-- 配置事务控制的方式 -->
                        <transactionManager type="JDBC"></transactionManager>
                        <!-- 配置连接数据库的必备信息  type属性表示是否使用数据源(连接池)-->
                        <dataSource type="JNDI">
                            <property name="data_source" value="java:comp/env/jdbc/txl_mybatis"/>
                        </dataSource>
                    </environment>
                </environments>
            
                <!-- 指定mapper配置文件的位置 -->
                <mappers>
                    <mapper resource="com/itheima/dao/IUserDao.xml"/>
                </mappers>
            </configuration>

                            标红的是不能改的:

        4.将测试文件 写在jsp中
          注意:jsp中SqlSession对象的变量名不能起session,因为jsp中有一个隐式对象也是session
        ```
        <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
        <html>
        <body>
        <h2>Hello World!</h2>
        
        <%
            InputStream in = Resources.getResourceAsStream("SqlMapConfig.xml");
            SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
            SqlSessionFactory factory = builder.build(in);
            SqlSession sqlSession = factory.openSession();
            IUserDao userDao = sqlSession.getMapper(IUserDao.class);
        
            List<User> users = userDao.findAll();
            for (User user : users) {
                System.out.println(user);
            }

            sqlSession.close();
            in.close();
        %>
        </body>
        </html>
        ```
        
        5.为什么在单元测试中用不了,必须放在jspo中访问呢?
            当我们访问jsp时,经过了tomcat服务器,jsp在执行时先翻译成.java再编译成.class再去运行,这些都是tomcat来做的。
        当你经过了tomcat服务器,这时候tomcat服务器为你准备的数据源就可以使用的。
            而如果用单元测试,根本没有经过tomcat,就是一个java的程序在运行。那么此时tomcat为我们准备的数据源,就不能使用。
            这就是JNDI数据源的使用。