Mybatisy有關的數據庫操作


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=[]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章