文章目錄
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=[]}