Mybatisy有关的数据库操作

120 阅读17分钟

文章目录


1. 数据库连接池

Mybatis中的数据源分为如下几类:

  • org.apache.ibatis.datasource
  • org.apache.ibatis.datasource.jndi
  • org.apache.ibatis.datasource.pooled
  • org.apache.ibatis.datasource.unpooled

对应的数据库连接池分为三类UNPOOLED、POOLED和JNDI。Mybatis内部分别定义了实现java.sql.DataSource接口的UnpooledDataSource和PooledDataSource类来分别表示UNPOOLED和POOLED。
在这里插入图片描述

PooledDataSource和UnpooledDataSource都实现了java.sql.DataSource接口,并且PooledDataSource持有UnpooledDataSource的引用。当PooledDataSource需要创建java.sql.Connection实例对象时,还是通过UnpooledDataSource来创建,PooledDataSource只是提供一种缓存连接池机制

2. 数据源配置

数据源配置位于SqlMapConfig.xml文件的dataSource标签内,对应的type属性表示的就是采用何种连接技术。通常采用的是连接池POOLED,如下所示:

<dataSource type="POOLED">
	<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
    <property name="url" value="jdbc:mysql://localhost:3306/sql_store?serverTimezone=GMT"/>
    <property name="username" value="root"/>
    <property name="password" value="1234"/>
</dataSource>

当我们运行程序时,log信息中会显示如下信息:

DEBUG source.pooled.PooledDataSource  - PooledDataSource forcefully closed/removed all connections.
DEBUG ansaction.jdbc.JdbcTransaction  - Opening JDBC Connection
DEBUG source.pooled.PooledDataSource  - Created connection 1747352992.
DEBUG ansaction.jdbc.JdbcTransaction  - Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@68267da0]
DEBUG dao.AccountDao.findAll  - ==>  Preparing: select * from account 
DEBUG dao.AccountDao.findAll  - ==> Parameters: 
DEBUG dao.AccountDao.findAll  - <==      Total: 4
DEBUG ansaction.jdbc.JdbcTransaction  - Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@68267da0]
DEBUG ansaction.jdbc.JdbcTransaction  - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@68267da0]
DEBUG source.pooled.PooledDataSource  - Returned connection 1747352992 to pool.

从输出信息中可以看出,POOLED方式对应的类就是PooledDataSource。在使用连接池时,首先会从连接池中取一个连接,如connection 1747352992;事务提交默认为false。当获取到连接后,程序会执行SQL语句,并提交事务。程序运行结束后,程序会关闭数据库连接 ,并将其归还回连接池中。

如果采用的是UNPOOLED的方式,表示不采用连接池机制。当程序需要连接数据库时,它就会新建一条连接;当程序运行结束后,它就会释放连接,这和传统的JDBC技术是一致的。

dataSource type="UNPOOLED">
	<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
	<property name="url" value="jdbc:mysql://localhost:3306/sql_store?serverTimezone=GMT"/>
	<property name="username" value="root"/>
	<property name="password" value="1234"/>
</dataSource>

输出log信息为:

DEBUG ansaction.jdbc.JdbcTransaction  - Opening JDBC Connection
DEBUG ansaction.jdbc.JdbcTransaction  - Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@15ca7889]
DEBUG dao.AccountDao.findAll  - ==>  Preparing: select * from account 
DEBUG dao.AccountDao.findAll  - ==> Parameters: 
DEBUG dao.AccountDao.findAll  - <==      Total: 4
DEBUG ansaction.jdbc.JdbcTransaction  - Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@15ca7889]
DEBUG ansaction.jdbc.JdbcTransaction  - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@15ca7889

此时从输出中可以验证前面的说法,这里采用的就是随用随创,用完释放。

3. 源码分析

3.1 DataSourceFactory源码分析

MyBatis是通过工厂模式来创建数据源DataSource对象的,MyBatis定义了抽象的工厂接口org.apache.ibatis.datasource.DataSourceFactory,通过其getDataSource()方法返回数据源DataSource。当获取到DataSource实例后,将其放到Configuration的Environment对象中供使用。

package org.apache.ibatis.datasource;

import java.util.Properties;
import javax.sql.DataSource;

public interface DataSourceFactory {
  void setProperties(Properties props);
  DataSource getDataSource();
}

接口的实现工厂类分别对应了前面提到的三种方式。
在这里插入图片描述

其中PooledDataSourceFactory源码如下所示:

public class PooledDataSourceFactory extends UnpooledDataSourceFactory {
  public PooledDataSourceFactory() {
    // 实例化PooledDataSource对象
    this.dataSource = new PooledDataSource();
  }
}

它只有一个构造方法,并且继承了UnpooledDataSourceFactory这个工厂类。所以,它其实使用的仍然是UnpooledDataSourceFactory中定义的方法

UnpooledDataSourceFactory的源码如下:

package org.apache.ibatis.datasource.unpooled;

import java.util.Properties;
import javax.sql.DataSource;

import org.apache.ibatis.datasource.DataSourceException;
import org.apache.ibatis.datasource.DataSourceFactory;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;

public class UnpooledDataSourceFactory implements DataSourceFactory {

  private static final String DRIVER_PROPERTY_PREFIX = "driver.";
  private static final int DRIVER_PROPERTY_PREFIX_LENGTH = DRIVER_PROPERTY_PREFIX.length();

  protected DataSource dataSource;

  public UnpooledDataSourceFactory() {
    this.dataSource = new UnpooledDataSource();
  }

  @Override
  public void setProperties(Properties properties) {
    Properties driverProperties = new Properties();
    MetaObject metaDataSource = SystemMetaObject.forObject(dataSource);
    for (Object key : properties.keySet()) {
      String propertyName = (String) key;
      if (propertyName.startsWith(DRIVER_PROPERTY_PREFIX)) {
        String value = properties.getProperty(propertyName);
        driverProperties.setProperty(propertyName.substring(DRIVER_PROPERTY_PREFIX_LENGTH), value);
      } else if (metaDataSource.hasSetter(propertyName)) {
        String value = (String) properties.get(propertyName);
        Object convertedValue = convertValue(metaDataSource, propertyName, value);
        metaDataSource.setValue(propertyName, convertedValue);
      } else {
        throw new DataSourceException("Unknown DataSource property: " + propertyName);
      }
    }
    if (driverProperties.size() > 0) {
      metaDataSource.setValue("driverProperties", driverProperties);
    }
  }

  @Override
  public DataSource getDataSource() {
    return dataSource;
  }

  private Object convertValue(MetaObject metaDataSource, String propertyName, String value) {
    Object convertedValue = value;
    Class<?> targetType = metaDataSource.getSetterType(propertyName);
    if (targetType == Integer.class || targetType == int.class) {
      convertedValue = Integer.valueOf(value);
    } else if (targetType == Long.class || targetType == long.class) {
      convertedValue = Long.valueOf(value);
    } else if (targetType == Boolean.class || targetType == boolean.class) {
      convertedValue = Boolean.valueOf(value);
    }
    return convertedValue;
  }
}
3.2 PooledDataSource源码分析

前面提到PooledDataSourceFactory工厂类中,通过实例化PooledDataSource对象来提供实例。下面看一下PooledDataSource的实现类代码,其中构造方法如下:

public PooledDataSource() {
    dataSource = new UnpooledDataSource();
  }

  public PooledDataSource(UnpooledDataSource dataSource) {
    this.dataSource = dataSource;
  }

  public PooledDataSource(String driver, String url, String username, String password) {
    dataSource = new UnpooledDataSource(driver, url, username, password);
    expectedConnectionTypeCode = assembleConnectionTypeCode(dataSource.getUrl(), dataSource.getUsername(), dataSource.getPassword());
  }
	//其他构造方法
  }

从构造方法中可以验证前面的说法,PooledDataSource只是提供一种缓存连接池机制,当它想要创建DataSource实例时,仍然依赖于UnpooledDataSource。

然后重点看一下它是如何获取数据库连接的。PooledDataSource实现了DataSource接口,DataSource的源码如下:

package javax.sql;

import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Wrapper;

public interface DataSource  extends CommonDataSource, Wrapper {
  Connection getConnection() throws SQLException;
  Connection getConnection(String username, String password)
    throws SQLException;
}

因此,PooledDataSource通过重写getConnection()来获取连接。getConnection()源码如下:

@Override
  public Connection getConnection(String username, String password) throws SQLException {
    return popConnection(username, password).getProxyConnection();
  }

它又调用了popConnection(String username, String password),方法定义如下:

private PooledConnection popConnection(String username, String password) throws SQLException {
    boolean countedWait = false;
    PooledConnection conn = null;
    long t = System.currentTimeMillis();
    int localBadConnectionCount = 0;
	// 当PooledConnection对象为null
    while (conn == null) {
      // 首先建立同步机制,保证线程安全
      synchronized (state) {
        // 如果空闲池中有可用连接
        if (!state.idleConnections.isEmpty()) {
          // 直接取一个可用连接使用
          // List<PooledConnection> idleConnections = new ArrayList<>();
          conn = state.idleConnections.remove(0);
          if (log.isDebugEnabled()) {
            log.debug("Checked out connection " + conn.getRealHashCode() + " from pool.");
          }
        } else {
          // 如果没有可用连接,而且此时活动池中连接数未到达设定的最大值
          if (state.activeConnections.size() < poolMaximumActiveConnections) {
            // 则创建新连接
            conn = new PooledConnection(dataSource.getConnection(), this);
            if (log.isDebugEnabled()) {
              log.debug("Created connection " + conn.getRealHashCode() + ".");
            }
          } else {
            // 如果此时活动池中已满,不能创建新连接
            // 则从中取出“最老的”一个连接,经过处理后供程序使用
            PooledConnection oldestActiveConnection = state.activeConnections.get(0);
            long longestCheckoutTime = oldestActiveConnection.getCheckoutTime();
            if (longestCheckoutTime > poolMaximumCheckoutTime) {
              // Can claim overdue connection
              state.claimedOverdueConnectionCount++;
              state.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime;
              state.accumulatedCheckoutTime += longestCheckoutTime;
              state.activeConnections.remove(oldestActiveConnection);
              if (!oldestActiveConnection.getRealConnection().getAutoCommit()) {
                try {
                  oldestActiveConnection.getRealConnection().rollback();
                } catch (SQLException e) {
                  log.debug("Bad connection. Could not roll back");
                }
              }
              // 得到处理后的连接
              conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this);
              conn.setCreatedTimestamp(oldestActiveConnection.getCreatedTimestamp());
              conn.setLastUsedTimestamp(oldestActiveConnection.getLastUsedTimestamp());
              oldestActiveConnection.invalidate();
              if (log.isDebugEnabled()) {
                log.debug("Claimed overdue connection " + conn.getRealHashCode() + ".");
              }
            } else {
              // 如果前面的情况都不满足,则只能等待连接的释放
              try {
                if (!countedWait) {
                  state.hadToWaitCount++;
                  countedWait = true;
                }
                if (log.isDebugEnabled()) {
                  log.debug("Waiting as long as " + poolTimeToWait + " milliseconds for connection.");
                }
                long wt = System.currentTimeMillis();
                state.wait(poolTimeToWait);
                state.accumulatedWaitTime += System.currentTimeMillis() - wt;
              } catch (InterruptedException e) {
                break;
              }
            }
          }
        }
          
    // 其他代码
          
	// 最后返回一个连接
    return conn;
  }

其中conn = new PooledConnection(dataSource.getConnection(), this);中通过传入的dataSource.getConnection()来创建连接,而dataSource这里是UnpooledDataSource的对象实例。所以,PooledDataSource的连接的创建还要看UnpooledDataSource的具体实现。

3.3 UnpooledDataSource的源码实现

UnpooledDataSource的成员变量中包含有连接数据库的信息:

  private String driver;
  private String url;
  private String username;
  private String password;

上面调用的getConnection()实现为:

@Override
  public Connection getConnection() throws SQLException {
    return doGetConnection(username, password);
  }

其中又调用了doGetConnection(username, password),它的实现为:

private Connection doGetConnection(String username, String password) throws SQLException {
    Properties props = new Properties();
    if (driverProperties != null) {
      props.putAll(driverProperties);
    }
    if (username != null) {
      props.setProperty("user", username);
    }
    if (password != null) {
      props.setProperty("password", password);
    }
    return doGetConnection(props);
  }

它将传入的username和password保存到一个Properties对象中,然后又调用了doGetConnection(props),它的实现为:

private Connection doGetConnection(Properties properties) throws SQLException {
    // 初始化JDBC驱动
    initializeDriver();
    // 获取连接
    Connection connection = DriverDrivamaManager.getConnection(url, properties);
    configureConnection(connection);
    return connection;
  }

其中初始化驱动的方法实现如下,它和JDBC中注册驱动的原理是一致的:

  private synchronized void initializeDriver() throws SQLException {
    if (!registeredDrivers.containsKey(driver)) {
      Class<?> driverType;
      try {
        if (driverClassLoader != null) {
          driverType = Class.forName(driver, true, driverClassLoader);
        } else {
          driverType = Resources.classForName(driver);
        }

        Driver driverInstance = (Driver)driverType.getDeclaredConstructor().newInstance();
        DriverManager.registerDriver(new DriverProxy(driverInstance));
        registeredDrivers.put(driver, driverInstance);
      } catch (Exception e) {
        throw new SQLException("Error setting driver on UnpooledDataSource. Cause: " + e);
      }
    }
  }

因此,通过注册驱动和使用DriverDrivamaManager中的getConnection()来建立连接,程序就可以使用UNPOOLED或者POOLED来获取连接使用。

上面所涉及的程序执行过程如下所示:
在这里插入图片描述

4. 事务控制

JDBC中使用void setAutoCommit(boolean autoCommit)来控制事务的提交方式,如果参数为true,表示自动提交;如果参数为false,表示手动提交。如果连接处于自动提交模式,那么所有的SQL语句将被执行并作为单个事务提交。否则,它的SQL语句将聚集到事务中,直到调用commit()rollback()为止。

  • 对于DML语句和DDL语句,事务提交在执行完毕时完成
  • 对于select语句,事务提交在关联结果集关闭时提交

Mybatis中使用SqlSession中的commit()执行事务的提交操作。因此,Mybatis中事务的提交方式,本质上就是调用JDBC的setAutoCommit()来实现事务控制。为什么CUD过程中必须使用sqlSession.commit()提交事务?主要原因就是在连接池中取出的连接,都会将调用connection.setAutoCommit(false)方法,这样我们就必须使用sqlSession.commit()方法,相当于使用了JDBC中的connection.commit()方法实现事务提交。

通过在使用SqlSession的工厂来获取SqlSession对象时,设置参数为true,就可以设置为自动提交事务。

InputStream in = Resources.getResourceAsStream("SqlMapConfig.xml");
new SqlSessionFactoryBuilder().build(in).openSession(true);

5. 动态SQL语句

在这里插入图片描述

5.1 if 标签

我们根据实体类的不同取值,使用不同的SQL语句来进行查询。比如在id如果不为空时可以根据id查询,如果username不同空时还要加入用户名作为条件。这种情况在我们的多条件组合查询中经常会碰到。

编写持久层接口:

public interface AccountDao {
    List<Account> findByUser(Account account);
}

在对应的AccountDao.xml文件中添加<select>,以及在标签中添加相关的内容:

    <select id="findByUser" resultType="domain.Account" parameterType="domain.Account">
        <!--where 1=1 是为了拼接后面的判断条件-->
        select * from account where 1=1
        <!--判断条件设置-->
        <if test="username!=null and username!=''">
            and username like #{username}
        </if>
        <!--还可以添加其他的条件-->
    </select>

最后在测试类中增加相应的测试方法,查找username中包含F的用户:

    @Test
    public void testFindByUser(){
        Account a = new Account();
        a.setUsername("%F%");
        List<Account> byUser = mapper.findByUser(a);
        for (Account account : byUser) {
            System.out.println(account); 
        }
    }

输出为:

Account{id=1, username='Forlogen', password='123456'}

5.2 where 标签

使用<where>标签来简化上面AccountDao.xml中where 1=1的使用:

    <select id="findByUser" resultType="domain.Account" parameterType="domain.Account">
        select * from account
        <where>
            <if test="username!=null and username!=''">
                and username like #{username}
            </if>
        </where>
    </select>

使用同样的测试方法可以得到相同的结果。

5.3 foreach标签

当使用多个id来执行查询时,可以使用下面的SQL语句执行:

select * from account where id=1 or id=2 or id=3
select * from account where id in(1,2,3)

执行上面的两条SQL语句,我们可以得到相同的结果:

mysql> select * from account;
+----+----------+----------+
| id | username | password |
+----+----------+----------+
|  1 | Forlogen | 123456   |
|  2 | Kobe     | 88824    |
|  3 | James    | 232323   |
|  4 | Rose     | 44444    |
+----+----------+----------+
4 rows in set (0.00 sec)

mysql> select * from account where id=1 or id=2 or id=3;
+----+----------+----------+
| id | username | password |
+----+----------+----------+
|  1 | Forlogen | 123456   |
|  2 | Kobe     | 88824    |
|  3 | James    | 232323   |
+----+----------+----------+
3 rows in set (0.01 sec)

mysql> select * from account where id in(1,2,3);
+----+----------+----------+
| id | username | password |
+----+----------+----------+
|  1 | Forlogen | 123456   |
|  2 | Kobe     | 88824    |
|  3 | James    | 232323   |
+----+----------+----------+
3 rows in set (0.01 sec)

第二种是比较好的方式,这样我们在进行范围查询时,就要将一个集合中的值,作为参数动态添加进来。那么在Mybatis中如何使用呢?

首先创建在QueryVo中创建要传入的id集合

public class QueryVo {
    private Account user;
    private List<Integer> ids;
}

添加方法:

public interface AccountDao {
    List<Account> findByIds(QueryVo vo);
}

然后在对应的AccountDao.xml文件中添加标签,它对应的SQL语句就是select * from account where id in(?)

    <select id="findByIds" resultType="domain.Account" parameterType="domain.QueryVo">
        select * from account
        <where>
            <if test="ids!=null and ids.size()>0">
                <foreach collection="ids" open="id in (" close=")" item="id" separator=",">
                    #{id}
                </foreach>
            </if>
        </where>
    </select>

其中的<foreach>标签用于遍历集合,它的属性:

  • collection:代表要遍历的集合元素
  • open:代表语句的开始部分
  • close:代表语句的结束部分
  • item:代表遍历集合的内阁元素,生成的变量名
  • sperator:代表使用的分隔符

最后编写测试方法:

    @Test
    public void testFindByIds(){
        QueryVo vo = new QueryVo();
        List<Integer> list = new ArrayList<Integer>();
        list.add(1);
        list.add(2);
        list.add(3);
        vo.setIds(list);
        List<Account> byIds = mapper.findByIds(vo);
        for (Account ele : byIds) {
            System.out.println(ele);
        }
    }

执行单元测试,结果输出为:

Account{id=1, username='Forlogen', password='123456'}
Account{id=2, username='Kobe', password='88824'}
Account{id=3, username='James', password='232323'}

5.4 简化代码编写

Sql中可将重复的sql提取出来,使用时用<include>引用即可,最终达到sql重用的目的。

<mapper namespace="dao.AccountDao">
    <!--SQL语句中重复的部分-->
    <sql id="defaultUser">
        select * from account
    </sql>
    <select id="findAll" resultType="domain.Account">
        <include refid="defaultUser"></include>
    </select>
</mapper>

6. 多表查询 - 一对多

首先创建user表,并添加测试数据

CREATE TABLE `user` (
  `id` int(11) NOT NULL auto_increment,
  `username` varchar(32) NOT NULL COMMENT '用户名称',
  `birthday` datetime default NULL COMMENT '生日',
  `sex` char(1) default NULL COMMENT '性别',
  `address` varchar(256) default NULL COMMENT '地址',
  PRIMARY KEY  (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

然后创建account表,并向其中添加测试数据

CREATE TABLE `account` (
  `ID` int(11) NOT NULL COMMENT '编号',
  `UID` int(11) default NULL COMMENT '用户编号',
  `MONEY` double default NULL COMMENT '金额',
  PRIMARY KEY  (`ID`),
  KEY `FK_Reference_8` (`UID`),
  CONSTRAINT `FK_Reference_8` FOREIGN KEY (`UID`) REFERENCES `user` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

通过select语句可以查看表中添加完数据后的结果

mysql> select * from user;
+----+----------+---------------------+------+---------+
| id | username | birthday            | sex  | address |
+----+----------+---------------------+------+---------+
| 41 | Forlogen | 2018-02-27 17:47:08 | 男   | Beijing |
| 42 | Kobe     | 2018-03-02 15:09:37 | 男   | LA      |
| 43 | James    | 2018-03-04 11:34:34 | 男   | USA     |
| 45 | GIGI     | 2018-03-04 12:04:06 | 女   | LA      |
+----+----------+---------------------+------+---------+
4 rows in set (0.00 sec)

mysql> select * from account;
+----+------+-------+
| id | uid  | money |
+----+------+-------+
|  1 |   42 |  1000 |
|  2 |   43 |  1000 |
|  3 |   43 |  2000 |
+----+------+-------+
3 rows in set (0.00 sec)

一个人可以有多个账户,而一个账户只能属于一个人。因此,如果从查询账户信息出发关联查询用户信息为一对一查询;如果从查询用户信出发查询账户信息为一对多查询。

为了方便后续的代码实现,首先需要定义两张表对应的类:

public class User implements Serializable {
    private Integer id;
    private String username;
    private String address;
    private String sex;
    private Date birthday;

    //一对多关系映射:主表实体应该包含从表实体的集合引用
    private List<Account> accounts;

    public List<Account> getAccounts() {
        return accounts;
    }

    public void setAccounts(List<Account> accounts) {
        this.accounts = accounts;
    }
    
    // getter()
    // setter()
    //toString()
}
public class Account implements Serializable {
    private Integer id;
    private Integer uid;
    private Double money;
    
    // getter()
    // setter()
    //toString()
}

6.1 一对一

查询所有账户信息,关联查询下单用户信息。如果直接使用SQL语句查询可得:

mysql> SELECT account.*, user.username, user.address FROM account, user WHERE account.uid = user.id;
+----+------+-------+----------+---------+
| id | uid  | money | username | address |
+----+------+-------+----------+---------+
|  1 |   42 |  1000 | Kobe     | LA      |
|  2 |   43 |  1000 | James    | USA     |
|  3 |   43 |  2000 | James    | USA     |
+----+------+-------+----------+---------+
3 rows in set (0.00 sec)

这里对于用户信息来说,我们只想要username和address。因此,在SQL语句中只保留了这两部分信息。为了封装上面SQL语句的结果,还需要定义一个AccountUser类,成员变量只有username和address:

public class AccountUser extends Account {
    private String username;
    private String address;
    
	// getter()
    // setter()
    //toString()
}

然后在持久层接口中定义相关的方法,查询所有的账户同时获取账户对应用户的username和address信息。

public interface IAccountDao {
     List<AccountUser> findAllAccount();
}

在对应的AccountDao.xml文件中添加配置信息:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="dao.IAccountDao">
    <select id="findAllAccount" resultType="accountuser">
        select a.*,u.username,u.address from account a , user u where u.id = a.uid;
    </select>
</mapper>

编写测试方法:

    @Test
    public void testFindAllAccountUser(){
        List<AccountUser> aus = accountDao.findAllAccount();
        for(AccountUser au : aus){
            System.out.println(au);
        }
    }

执行单元测试,得到如下的结果:

Account{id=1, uid=42, money=1000.0}        AccountUser{username='Kobe', address='LA'}
Account{id=2, uid=43, money=1000.0}        AccountUser{username='James', address='USA'}
Account{id=3, uid=43, money=2000.0}        AccountUser{username='James', address='USA'}

另一种方式是使用xml文件中的resultMap来建立一对一查询结果之间的映射。为了建立account和user之间的映射,需要在Account类中添加User对象的映射

public class Account implements Serializable {
    private Integer id;
    private Integer uid;
    private Double money;

    //从表实体应该包含一个主表实体的对象引用
    private User user;
    
    // getter()
    // setter()
    //toString()
}

同时修改持久层接口中的方法,此时List的泛型就是Account:

public interface IAccountDao {
    List<Account> findAll();
}

在对应的AccountDao.xml文件中添加配置信息。首先需要添加resultMap建立对应关系:

<resultMap id="accountUserMap" type="account">
    <!--account表信息-->
    <id property="id" column="aid"></id>
    <result property="uid" column="uid"></result>
    <result property="money" column="money"></result>
    <!-- 一对一的关系映射:配置封装user的内容-->
    <association property="user" column="uid" javaType="user">
        <id property="id" column="id"></id>
        <result column="username" property="username"></result>
        <result column="address" property="address"></result>
    </association>
</resultMap>

此时的配置信息如下,不必写单独的封装类,只需要适用resultMap标签指定要使用的resultMap即可:

<select id="findAll" resultMap="accountUserMap">
	select u.*,a.id as aid,a.uid,a.money from account a , user u where u.id = a.uid;
</select>

编写测试方法:

    @Test
    public void testFindAll(){
        List<Account> accounts = accountDao.findAll();
        for(Account account : accounts){
            System.out.println(account + "  "+ account.getUser());
        }
    }

执行单元测试,可以得到和使用封装类相同的结果:

Account{id=1, uid=42, money=1000.0}  User{id=42, username='Kobe', address='LA', sex='null', birthday=null}
Account{id=2, uid=43, money=1000.0}  User{id=43, username='James', address='USA', sex='null', birthday=null}
Account{id=3, uid=43, money=2000.0}  User{id=43, username='James', address='USA', sex='null', birthday=null}

6.2 一对多

查询所有用户信息及用户关联的账户信息。此时查询结果需要输出user表中的所有信息和有的account表信息,因此应该使用左连接。

mysql> select u.*, acc.id id, acc.uid, acc.money from user u left join account acc on u.id=acc.uid;
+----+----------+---------------------+------+---------+------+------+-------+
| id | username | birthday            | sex  | address | id   | uid  | money |
+----+----------+---------------------+------+---------+------+------+-------+
| 41 | Forlogen | 2018-02-27 17:47:08 | 男   | Beijing | NULL | NULL |  NULL |
| 42 | Kobe     | 2018-03-02 15:09:37 | 男   | LA      |    1 |   42 |  1000 |
| 43 | James    | 2018-03-04 11:34:34 | 男   | USA     |    2 |   43 |  1000 |
| 43 | James    | 2018-03-04 11:34:34 | 男   | USA     |    3 |   43 |  2000 |
| 45 | GIGI     | 2018-03-04 12:04:06 | 女   | LA      | NULL | NULL |  NULL |
+----+----------+---------------------+------+---------+------+------+-------+
5 rows in set (0.00 sec)

因为从用户查询出发到账户查询,因此,User表中应该持有Account对象的引用。另外,由于每个用户可能有多个账户,因此对应类型应该是List<Accoutn>

public class User implements Serializable {
    private Integer id;
    private String username;
    private String address;
    private String sex;
    private Date birthday;

    //一对多关系映射:主表实体应该包含从表实体的集合引用
    private List<Account> accounts;
    
    // getter()
    // setter()
    //toString()
}

在持久层接口中添加相应的方法:

public interface IUserDao {
    List<User> findAll();
}

同时在对应的UserDao.xml文件中添加配置信息,这里同样需要使用resultMap来建立映射关系

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="dao.IUserDao">
    <resultMap id="userAccountMap" type="user">
        <id property="id" column="id"></id>
        <result property="username" column="username"></result>
        <result property="address" column="address"></result>
        <result property="sex" column="sex"></result>
        <result property="birthday" column="birthday"></result>
        <collection property="accounts" ofType="account">
            <id column="aid" property="id"></id>
            <result column="uid" property="uid"></result>
            <result column="money" property="money"></result>
        </collection>
    </resultMap>

    <select id="findAll" resultMap="userAccountMap">
        select * from user u left outer join account a on u.id = a.uid
    </select>
</mapper>

编写测试方法:

    @Test
    public void testFindAll(){
        List<User> users = userDao.findAll();
        for(User user : users){
            System.out.println(user + " " + user.getAccounts());
        }
    }

执行单元测试,得到结果:

User{id=41, username='Forlogen', address='Beijing', sex='男', birthday=Wed Feb 28 01:47:08 CST 2018} []
User{id=42, username='Kobe', address='LA', sex='男', birthday=Fri Mar 02 23:09:37 CST 2018} [Account{id=null, uid=42, money=1000.0}]
User{id=43, username='James', address='USA', sex='男', birthday=Sun Mar 04 19:34:34 CST 2018} [Account{id=null, uid=43, money=1000.0}, Account{id=null, uid=43, money=2000.0}]
User{id=45, username='GIGI', address='LA', sex='女', birthday=Sun Mar 04 20:04:06 CST 2018} []

7. 多表查询 - 多对多

再创建角色表

CREATE TABLE `role` (
  `ID` int(11) NOT NULL COMMENT '编号',
  `ROLE_NAME` varchar(30) default NULL COMMENT '角色名称',
  `ROLE_DESC` varchar(60) default NULL COMMENT '角色描述',
  PRIMARY KEY  (`ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

并建立中间表用于连接user和role:

CREATE TABLE `user_role` (
  `UID` int(11) NOT NULL COMMENT '用户编号',
  `RID` int(11) NOT NULL COMMENT '角色编号',
  PRIMARY KEY  (`UID`,`RID`),
  KEY `FK_Reference_10` (`RID`),
  CONSTRAINT `FK_Reference_10` FOREIGN KEY (`RID`) REFERENCES `role` (`ID`),
  CONSTRAINT `FK_Reference_9` FOREIGN KEY (`UID`) REFERENCES `user` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

最后插入几条测试数据。

mysql> select * from role;
+----+-----------+--------------+
| ID | ROLE_NAME | ROLE_DESC    |
+----+-----------+--------------+
|  1 | 院长      | 管理整个学院 |
|  2 | 总裁      | 管理整个公司 |
|  3 | 校长      | 管理整个学校 |
+----+-----------+--------------+
3 rows in set (0.01 sec)

mysql> select * from user_role;
+-----+-----+
| UID | RID |
+-----+-----+
|  42 |   1 |
|  43 |   1 |
|  43 |   2 |
+-----+-----+
3 rows in set (0.00 sec)

7.1 role到user的多对多

实现查询所有角色并且加载它所分配的用户信息。查询角色我们需要用到Role表,但角色分配的用户的信息我们并不能直接找到用户信息,而是要通过中间表(USER_ROLE表)才能关联到用户信息。

mysql> SELECT r.*,u.id uid, u.username username, u.birthday birthday, u.sex sex, u.address address FROM ROLE r INNER JOIN USER_ROLE ur ON ( r.id = ur.rid) INNER JOIN USER u ON (ur.uid = u.id);
+----+-----------+--------------+-----+----------+---------------------+------+---------+
| ID | ROLE_NAME | ROLE_DESC    | uid | username | birthday            | sex  | address |
+----+-----------+--------------+-----+----------+---------------------+------+---------+
|  1 | 院长      | 管理整个学院 |  42 | Kobe     | 2018-03-02 15:09:37 | 男   | LA      |
|  1 | 院长      | 管理整个学院 |  43 | James    | 2018-03-04 11:34:34 | 男   | USA     |
|  2 | 总裁      | 管理整个公司 |  43 | James    | 2018-03-04 11:34:34 | 男   | USA     |
+----+-----------+--------------+-----+----------+---------------------+------+---------+
3 rows in set (0.00 sec)

创建角色的实体类:

public class Role {
    private Integer roleId;
    private String roleName;
    private String roleDesc;
	
    private List<User> users;
    
	// getter()
    // setter()
    //toString()
}

创建爱你用户的实体类:

public class User {
    private Integer id;
    private String username;
    private String address;
    private String sex;
    private Date birthday;

    private List<Role> roles;
    
	// getter()
    // setter()
    // toString()
}

两个类中都包含对对方对象的引用,保持多对多的映射关系。

然后创建Role对应的持久层接口:

public interface IRoleDao {
    List<Role> findAll();
}

编写映射文件

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="dao.IRoleDao">
    <resultMap id="roleMap" type="role">
        <id property="roleId" column="rid"></id>
        <result property="roleName" column="role_name"></result>
        <result property="roleDesc" column="role_desc"></result>
        <collection property="users" ofType="user">
            <id column="id" property="id"></id>
            <result column="username" property="username"></result>
            <result column="address" property="address"></result>
            <result column="sex" property="sex"></result>
            <result column="birthday" property="birthday"></result>
        </collection>
    </resultMap>

    <select id="findAll" resultMap="roleMap">
        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 u.id = ur.uid
    </select>
</mapper>

最后编写测试方法:

@Test
    public void testFindAll(){
        List<Role> roles = roleDao.findAll();
        for(Role role : roles){
            System.out.println("---每个角色的信息----");
            System.out.println(role);
            System.out.println(role.getUsers());
        }
    }

执行单元测试,输出结果:

Role{roleId=1, roleName='院长', roleDesc='管理整个学院', users=[User{id=42, username='Kobe', address='LA', sex='男', birthday=Fri Mar 02 23:09:37 CST 2018, roles=null}, User{id=43, username='James', address='USA', sex='男', birthday=Sun Mar 04 19:34:34 CST 2018, roles=null}]}
[User{id=42, username='Kobe', address='LA', sex='男', birthday=Fri Mar 02 23:09:37 CST 2018, roles=null}, User{id=43, username='James', address='USA', sex='男', birthday=Sun Mar 04 19:34:34 CST 2018, roles=null}]
=======================
Role{roleId=2, roleName='总裁', roleDesc='管理整个公司', users=[User{id=43, username='James', address='USA', sex='男', birthday=Sun Mar 04 19:34:34 CST 2018, roles=null}]}
[User{id=43, username='James', address='USA', sex='男', birthday=Sun Mar 04 19:34:34 CST 2018, roles=null}]
=======================
Role{roleId=3, roleName='校长', roleDesc='管理整个学校', users=[]}
[]

7.2 user到role的多到多

mysql> 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;
+----+----------+---------------------+------+---------+------+-----------+--------------+
| id | username | birthday            | sex  | address | rid  | role_name | role_desc    |
+----+----------+---------------------+------+---------+------+-----------+--------------+
| 42 | Kobe     | 2018-03-02 15:09:37 | 男   | LA      |    1 | 院长      | 管理整个学院 |
| 43 | James    | 2018-03-04 11:34:34 | 男   | USA     |    1 | 院长      | 管理整个学院 |
| 43 | James    | 2018-03-04 11:34:34 | 男   | USA     |    2 | 总裁      | 管理整个公司 |
| 41 | Forlogen | 2018-02-27 17:47:08 | 男   | Beijing | NULL | NULL      | NULL         |
| 45 | GIGI     | 2018-03-04 12:04:06 | 女   | LA      | NULL | NULL      | NULL         |
+----+----------+---------------------+------+---------+------+-----------+--------------+
5 rows in set (0.00 sec)

首先编写持久层接口:

public interface IUserDao {
    List<User> findAll();

    User findById(Integer userId);
}

然后创建对应的IUserDao.xml文件,并添加配置信息:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="dao.IUserDao">
    <resultMap id="userMap" type="user">
        <id property="id" column="id"></id>
        <result property="username" column="username"></result>
        <result property="address" column="address"></result>
        <result property="sex" column="sex"></result>
        <result property="birthday" column="birthday"></result>
        <collection property="roles" ofType="role">
            <id property="roleId" column="rid"></id>
            <result property="roleName" column="role_name"></result>
            <result property="roleDesc" column="role_desc"></result>
        </collection>
    </resultMap>

    <select id="findAll" resultMap="userMap">
        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
    </select>
</mapper>

编写测试方法:

    @Test
    public void testFindAll(){
        List<User> users = userDao.findAll();
        for(User user : users){
            System.out.println("-----每个用户的信息------");
            System.out.println(user);
        }
    }

执行单元测试,结果输出:

==================
User{id=42, username='Kobe', address='LA', sex='男', birthday=Fri Mar 02 23:09:37 CST 2018, roles=[Role{roleId=1, roleName='院长', roleDesc='管理整个学院', users=null}]}
==================
User{id=43, username='James', address='USA', sex='男', birthday=Sun Mar 04 19:34:34 CST 2018, roles=[Role{roleId=1, roleName='院长', roleDesc='管理整个学院', users=null}, Role{roleId=2, roleName='总裁', roleDesc='管理整个公司', users=null}]}
==================
User{id=41, username='Forlogen', address='Beijing', sex='男', birthday=Wed Feb 28 01:47:08 CST 2018, roles=[]}
==================
User{id=45, username='GIGI', address='LA', sex='女', birthday=Sun Mar 04 20:04:06 CST 2018, roles=[]}

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
\


编写测试方法:

```java
    @Test
    public void testFindAll(){
        List<User> users = userDao.findAll();
        for(User user : users){
            System.out.println("-----每个用户的信息------");
            System.out.println(user);
        }
    }

执行单元测试,结果输出:

==================
User{id=42, username='Kobe', address='LA', sex='男', birthday=Fri Mar 02 23:09:37 CST 2018, roles=[Role{roleId=1, roleName='院长', roleDesc='管理整个学院', users=null}]}
==================
User{id=43, username='James', address='USA', sex='男', birthday=Sun Mar 04 19:34:34 CST 2018, roles=[Role{roleId=1, roleName='院长', roleDesc='管理整个学院', users=null}, Role{roleId=2, roleName='总裁', roleDesc='管理整个公司', users=null}]}
==================
User{id=41, username='Forlogen', address='Beijing', sex='男', birthday=Wed Feb 28 01:47:08 CST 2018, roles=[]}
==================
User{id=45, username='GIGI', address='LA', sex='女', birthday=Sun Mar 04 20:04:06 CST 2018, roles=[]}