文章目录
1. 数据库连接池
Mybatis中的数据源分为如下几类:
org.apache.ibatis.datasourceorg.apache.ibatis.datasource.jndiorg.apache.ibatis.datasource.pooledorg.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=[]}