第1章 JDBC操作数据库问题分析

1.1 jdbc 程序的回顾

public class Jdbc {
public static void main(String[] args) {
Connection connection = null;
PreparedStatement preparedStatement = null;
ResultSet resultSet = null;
try {
//加载数据库驱动
Class.forName("com.mysql.jdbc.Driver");
//通过驱动管理类获取数据库链接
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf-8", "root", "562644");
//定义 sql 语句 ?表示占位符
String sql = "select * from user where username = ?";
//获取预处理 statement
preparedStatement = connection.prepareStatement(sql);
//设置参数,第一个参数为 sql 语句中参数的序号(从 1 开始),第二个参数为设置的参数值
preparedStatement.setString(1, "王五");
//向数据库发出 sql 执行查询,查询出结果集
resultSet = preparedStatement.executeQuery();
//遍历查询结果集
while (resultSet.next()) {
System.out.println(resultSet.getString("id") + "" + resultSet.getString(" username"));
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//释放资源
if (resultSet != null) {
try {
resultSet.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (preparedStatement != null) {
try {
preparedStatement.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}

}
}

1.2 jdbc 问题分析

1、数据库链接创建、释放频繁造成系统资源浪费从而影响系统性能,如果使用数据库链接池可解决此问题。
2、Sql 语句在代码中硬编码,造成代码不易维护,实际应用 sql 变化的可能较大,sql 变动需要改变 java代码。
3、使用 preparedStatement 向占有位符号传参数存在硬编码,因为 sql 语句的 where 条件不一定,可能多也可能少,修改 sql 还要修改代码,系统不易维护。
4、对结果集解析存在硬编码(查询列名),sql 变化导致解析代码变化,系统不易维护,如果能将数据库记录封装成 pojo 对象解析比较方便。

第2章 Mybatis 框架快速入门

2.0 MyBatis 框架概述

mybatis 是一个优秀的基于 java 的持久层框架,它内部封装了 jdbc,使开发者只需要关注 sql 语句本身,而不需要花费精力去处理加载驱动、创建连接、创建 statement 等繁杂的过程。
mybatis 通过 xml 或注解的方式将要执行的各种 statement 配置起来,并通过 java 对象和 statement 中sql 的动态参数进行映射生成最终执行的 sql 语句,最后由 mybatis 框架执行 sql 并将结果映射为 java 对象并返回。
采用 ORM 思想解决了实体和数据库映射的问题,对 jdbc 进行了封装,屏蔽了 jdbc api 底层访问细节,使我们不用与 jdbc api 打交道,就可以完成对数据库的持久化操作。

2.1 Mybatis 框架开发的准备

创建数据库和插入测试数据:

DROP TABLE IF EXISTS `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;



insert into `user`(`id`,`username`,`birthday`,`sex`,`address`) values (41,'老王','2018-02-27 17:47:08','男','北京'),(42,'小二王','2018-03-02 15:09:37','女','北京金燕龙'),(43,'小二王','2018-03-04 11:34:34','女','北京金燕龙'),(45,'曾庆','2018-03-04 12:04:06','男','北京金燕龙'),(46,'老王','2018-03-07 17:37:26','男','北京'),(48,'小马宝莉','2018-03-08 11:44:00','女','北京修正');

创建Maven工程,添加pom依赖坐标:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>org.example</groupId>
<artifactId>QuickMyBatis</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>

<dependencies>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.6</version>
</dependency>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.6</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.12</version>
</dependency>

<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.10</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

2.2 搭建 Mybatis 开发环境

创建名为domain包,在该包下创建实体对象User.class

public class User implements Serializable {
private Integer id;
private String username;
private Date birthday;
private String sex;
private String address;
//省略get/Set/toString方法
}

创建名为dao包,在该包下创建接口类

/**
*
* 用户的持久层接口
*/
public interface IUserDao {

/**
* 查询所有操作
* @return
*/
List<User> findAll();
}

resource目录下创建包路径com.mybatis.dao(一次只创建一个包)。创建名为IUserDao.xml的Mapper文件:

<?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="com.mybatis.dao.IUserDao">
<!--配置查询所有-->
<select id="findAll" resultType="com.mybatis.domain.User">
select * from user
</select>
</mapper>

resource目录下创建名为log4j.properties的日志输出配置文件

# Set root category priority to INFO and its only appender to CONSOLE.
#log4j.rootCategory=INFO, CONSOLE debug info warn error fatal
log4j.rootCategory=debug, CONSOLE, LOGFILE

# Set the enterprise logger category to FATAL and its only appender to CONSOLE.
log4j.logger.org.apache.axis.enterprise=FATAL, CONSOLE

# CONSOLE is set to be a ConsoleAppender using a PatternLayout.
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
log4j.appender.CONSOLE.layout.ConversionPattern=%d{ISO8601} %-6r [%15.15t] %-5p %30.30c %x - %m\n

# LOGFILE is set to be a File appender using a PatternLayout.
log4j.appender.LOGFILE=org.apache.log4j.FileAppender
log4j.appender.LOGFILE.File=d:\axis.log
log4j.appender.LOGFILE.Append=true
log4j.appender.LOGFILE.layout=org.apache.log4j.PatternLayout
log4j.appender.LOGFILE.layout.ConversionPattern=%d{ISO8601} %-6r [%15.15t] %-5p %30.30c %x - %m\n

resource目录下创建Mybatis配置文件SqlMapConfig.xml:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<!-- mybatis的主配置文件 -->
<configuration>
<!-- 配置环境 -->
<environments default="mysql">
<!-- 配置mysql的环境-->
<environment id="mysql">
<!-- 配置事务的类型-->
<transactionManager type="JDBC"></transactionManager>
<!-- 配置数据源(连接池) -->
<dataSource type="POOLED">
<!-- 配置连接数据库的4个基本信息 -->
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/eesy_mybatis?characterEncoding=utf8"/>
<property name="username" value="root"/>
<property name="password" value="1234"/>
</dataSource>
</environment>
</environments>

<!-- 指定映射配置文件的位置,映射配置文件指的是每个dao独立的配置文件 -->
<mappers>
<mapper resource="com/mybatis/dao/IUserDao.xml"/><!-- XML配置方式 -->
</mappers>
</configuration>

2.3 环境搭建注意事项事项

第一个:创建IUserDao.xml 和 IUserDao.java时名称是为了和我们之前的知识保持一致。

  • 在Mybatis中它把持久层的操作接口名称和映射文件也叫做:Mapper
  • 所以:IUserDao 和 IUserMapper是一样的

第二个:在idea中创建目录的时候,它和包是不一样的

  • 包在创建时:com.mybatis.dao它是三级结构
  • 目录在创建时:com.mybatis.dao是一级目录

第三个:mybatis的映射配置文件位置必须和dao接口的包结构相同
第四个:映射配置文件的mapper标签namespace属性的取值必须是dao接口的全限定类名
第五个:映射配置文件的操作配置(select),id属性的取值必须是dao接口的方法名

当我们遵从了第三,四,五点之后,我们在开发中就无须再写dao的实现类。

2.4 入门案例(配置文件方式)

创建测试类:

public class MybatisTest {
/**
* 入门案例
*
* @param args
*/
public static void main(String[] args) throws Exception {
//1.读取配置文件
InputStream in = Resources.getResourceAsStream("SqlMapConfig.xml");
//2.创建SqlSessionFactory工厂
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
SqlSessionFactory factory = builder.build(in);
//3.使用工厂生产SqlSession对象
SqlSession session = factory.openSession();
//4.使用SqlSession创建Dao接口的代理对象
IUserDao userDao = session.getMapper(IUserDao.class);
//5.使用代理对象执行方法
List<User> users = userDao.findAll();
for (User user : users) {
System.out.println(user);
}
//6.释放资源
session.close();
in.close();
}
}

运行测试类会输出查询的结果:

User{id=41, username='老王', birthday=Tue Feb 27 17:47:08 CST 2018, sex='男', address='北京'}
User{id=42, username='小二王', birthday=Fri Mar 02 15:09:37 CST 2018, sex='女', address='北京金燕龙'}
User{id=43, username='小二王', birthday=Sun Mar 04 11:34:34 CST 2018, sex='女', address='北京金燕龙'}
User{id=45, username='曾庆', birthday=Sun Mar 04 12:04:06 CST 2018, sex='男', address='北京金燕龙'}
User{id=46, username='老王', birthday=Wed Mar 07 17:37:26 CST 2018, sex='男', address='北京'}
User{id=48, username='小马宝莉', birthday=Thu Mar 08 11:44:00 CST 2018, sex='女', address='北京修正'}

2.5 入门案例(注解方式)

把IUserDao.xml移除,在dao接口的方法上使用@Select注解,并且指定SQL语句.同时需要在SqlMapConfig.xml中的mapper配置时,使用class属性指定dao接口的全限定类名。

<!-- 指定映射配置文件的位置,映射配置文件指的是每个dao独立的配置文件
如果是用注解来配置的话,此处应该使用class属性指定被注解的dao全限定类名
-->
<mappers>
<mapper class="com.mybatis.dao.IUserDao"/><!-- 注解配置方式 -->
</mappers>

修改接口方法,改成注解方式

/**
*
* 用户的持久层接口
*/
public interface IUserDao {

/**
* 查询所有操作
* @return
*/
@Select("select * from user")
List<User> findAll();
}

运行测试类会输出查询的结果:

User{id=41, username='老王', birthday=Tue Feb 27 17:47:08 CST 2018, sex='男', address='北京'}
User{id=42, username='小二王', birthday=Fri Mar 02 15:09:37 CST 2018, sex='女', address='北京金燕龙'}
User{id=43, username='小二王', birthday=Sun Mar 04 11:34:34 CST 2018, sex='女', address='北京金燕龙'}
User{id=45, username='曾庆', birthday=Sun Mar 04 12:04:06 CST 2018, sex='男', address='北京金燕龙'}
User{id=46, username='老王', birthday=Wed Mar 07 17:37:26 CST 2018, sex='男', address='北京'}
User{id=48, username='小马宝莉', birthday=Thu Mar 08 11:44:00 CST 2018, sex='女', address='北京修正'}

2.6 编写Dao实现类的方式

我们在实际开发中,都是越简便越好,所以都是采用不写dao实现类的方式。不管使用XML还是注解配置。但是Mybatis它是支持写dao实现类的。

Dao目录下创建包impl,创建Dao的实现类,编写查询方法:

public class UserDaoImpl implements IUserDao {

private SqlSessionFactory factory;

//怎么确保这个factory有值呢?覆盖掉默认构造函数,一旦new一个对象,就会传入一个工厂进来
public UserDaoImpl(SqlSessionFactory factory){
this.factory=factory;
}

public List<User> findAll() {
//1.使用工厂创建SqlSession对象
SqlSession session=factory.openSession();
//2.使用session执行查询所有方法
List<User> users = session.selectList("com.mybatis.dao.IUserDao.findAll");
session.close();
//3.返回查询结果
return users;
}
}

修改测试类:

public class MybatisTest {
/**
* 入门案例
*
* @param args
*/
public static void main(String[] args) throws Exception {
//1.读取配置文件
InputStream in = Resources.getResourceAsStream("SqlMapConfig.xml");
//2.创建SqlSessionFactory工厂
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
SqlSessionFactory factory = builder.build(in);
//3.使用工厂创建dao对象
IUserDao userDao=new UserDaoImpl(factory);

//5.使用代理对象执行方法
List<User> users = userDao.findAll();
for (User user : users) {
System.out.println(user);
}
in.close();
}
}

2.7 入门案例中设计模式分析

第3章 基于代理Dao实现CRUD操作

3.0 环境搭建

创建Maven工程,添加pom依赖坐标:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>org.example</groupId>
<artifactId>QuickMyBatis</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>

<dependencies>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.6</version>
</dependency>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.6</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.12</version>
</dependency>

<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.10</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

创建名为domain包,在该包下创建实体对象User.class

public class User implements Serializable {
private Integer id;
private String username;
private Date birthday;
private String sex;
private String address;
//省略get/Set/toString方法
}

resource目录下创建名为log4j.properties的日志输出配置文件

# Set root category priority to INFO and its only appender to CONSOLE.
#log4j.rootCategory=INFO, CONSOLE debug info warn error fatal
log4j.rootCategory=debug, CONSOLE, LOGFILE

# Set the enterprise logger category to FATAL and its only appender to CONSOLE.
log4j.logger.org.apache.axis.enterprise=FATAL, CONSOLE

# CONSOLE is set to be a ConsoleAppender using a PatternLayout.
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
log4j.appender.CONSOLE.layout.ConversionPattern=%d{ISO8601} %-6r [%15.15t] %-5p %30.30c %x - %m\n

# LOGFILE is set to be a File appender using a PatternLayout.
log4j.appender.LOGFILE=org.apache.log4j.FileAppender
log4j.appender.LOGFILE.File=d:\axis.log
log4j.appender.LOGFILE.Append=true
log4j.appender.LOGFILE.layout=org.apache.log4j.PatternLayout
log4j.appender.LOGFILE.layout.ConversionPattern=%d{ISO8601} %-6r [%15.15t] %-5p %30.30c %x - %m\n

resource目录下创建Mybatis配置文件SqlMapConfig.xml:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<!-- mybatis的主配置文件 -->
<configuration>
<!-- 配置环境 -->
<environments default="mysql">
<!-- 配置mysql的环境-->
<environment id="mysql">
<!-- 配置事务的类型-->
<transactionManager type="JDBC"></transactionManager>
<!-- 配置数据源(连接池) -->
<dataSource type="POOLED">
<!-- 配置连接数据库的4个基本信息 -->
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/eesy_mybatis?characterEncoding=utf8"/>
<property name="username" value="root"/>
<property name="password" value="1234"/>
</dataSource>
</environment>
</environments>

<!-- 指定映射配置文件的位置,映射配置文件指的是每个dao独立的配置文件 -->
<mappers>
<mapper resource="com/mybatis/dao/IUserDao.xml"/><!-- XML配置方式 -->
</mappers>
</configuration>

查询所有

创建名为dao包,在该包下创建接口类

/**
*
* 用户的持久层接口
*/
public interface IUserDao {

/**
* 查询所有操作
* @return
*/
List<User> findAll();
}

resource目录下创建包路径com.mybatis.dao(一次只创建一个包)。创建名为IUserDao.xml的Mapper文件:

<?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="com.mybatis.dao.IUserDao">
<!-- 查询所有 -->
<select id="findAll" resultType="com.mybatis.domain.User">
select * from user
</select>
</mapper>

测试类:

public class MybatisTest {
/**
* 测试Mybatis的CRUD操作
*
* @param args
*/
public static void main(String[] args) throws Exception {
//1.读取配置文件
InputStream in = Resources.getResourceAsStream("SqlMapConfig.xml");
//2.创建SqlSessionFactory工厂
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
SqlSessionFactory factory = builder.build(in);
//3.使用工厂生产SqlSession对象
SqlSession session = factory.openSession();
//4.使用SqlSession创建Dao接口的代理对象
IUserDao userDao = session.getMapper(IUserDao.class);
//5.使用代理对象执行方法-查询所有
List<User> users = userDao.findAll();
for (User user : users) {
System.out.println(user);
}
//6.释放资源
session.close();
in.close();
}
}

执行测试类

User{id=41, username='老王', birthday=Tue Feb 27 17:47:08 CST 2018, sex='男', address='北京'}
User{id=42, username='小二王', birthday=Fri Mar 02 15:09:37 CST 2018, sex='女', address='北京金燕龙'}
User{id=43, username='小二王', birthday=Sun Mar 04 11:34:34 CST 2018, sex='女', address='北京金燕龙'}
User{id=45, username='曾庆', birthday=Sun Mar 04 12:04:06 CST 2018, sex='男', address='北京金燕龙'}
User{id=46, username='老王', birthday=Wed Mar 07 17:37:26 CST 2018, sex='男', address='北京'}
User{id=48, username='小马宝莉', birthday=Thu Mar 08 11:44:00 CST 2018, sex='女', address='北京修正'}

保存操作

Dao包下,IUserDao接口中添加保存方法

/**
* 保存用户
* @param user
*/
void saveUser(User user);

IUserDao.xml Mapper映射文件中,编写保存的配置方法

<!-- 保存用户 -->
<insert id="saveUser" parameterType="com.mybatis.domain.User">
insert into user(username,address,sex,birthday)values(#{username},#{address},#{sex},#{birthday});
</insert>

在编写测试类的时候,我们发现每写一个测试方法,我们都要读取配置文件、创建SqlSessionFactory工厂,获取对象、关闭资源。麻烦臃肿,我们来优化一下代码:
测试类:

 /**
* 测试Mybatis的CRUD操作
*/
public class MybatisTest {
private InputStream in;
private SqlSession sqlSession;
private IUserDao userDao;

@Before//用于在测试方法执行之前执行
public void init()throws Exception{
//1.读取配置文件,生成字节输入流
in = Resources.getResourceAsStream("SqlMapConfig.xml");
//2.获取SqlSessionFactory
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in);
//3.获取SqlSession对象
sqlSession = factory.openSession();
//4.获取dao的代理对象
userDao = sqlSession.getMapper(IUserDao.class);
}

@After//用于在测试方法执行之后执行
public void destroy()throws Exception{
//提交事务
sqlSession.commit();
//6.释放资源
sqlSession.close();
in.close();
}

/**
* 测试保存操作
*/
@Test
public void testSave(){
User user = new User();
user.setUsername("modify User property");
user.setAddress("北京市顺义区");
user.setSex("男");
user.setBirthday(new Date());
userDao.saveUser(user);
}
}

更新操作

Dao包下,IUserDao接口中添加更新方法

/**
* 更新用户
* @param user
*/
void updateUser(User user);

IUserDao.xml Mapper映射文件中,编写更新的配置方法

<!-- 更新用户 -->
<update id="updateUser" parameterType="com.mybatis.domain.User">
update user set username=#{username},address=#{address},sex=#{sex},birthday=#{birthday} where id=#{id}
</update>

测试类:

 /**
* 测试Mybatis的CRUD操作
*/
public class MybatisTest {
private InputStream in;
private SqlSession sqlSession;
private IUserDao userDao;

@Before//用于在测试方法执行之前执行
public void init()throws Exception{
//1.读取配置文件,生成字节输入流
in = Resources.getResourceAsStream("SqlMapConfig.xml");
//2.获取SqlSessionFactory
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in);
//3.获取SqlSession对象
sqlSession = factory.openSession();
//4.获取dao的代理对象
userDao = sqlSession.getMapper(IUserDao.class);
}

@After//用于在测试方法执行之后执行
public void destroy()throws Exception{
//提交事务
sqlSession.commit();
//6.释放资源
sqlSession.close();
in.close();
}

/**
* 测试更新操作
*/
@Test
public void testUpdate(){
User user = new User();
user.setId(50);
user.setUsername("mybastis update user");
user.setAddress("北京市顺义区");
user.setSex("女");
user.setBirthday(new Date());

//5.执行保存方法
userDao.updateUser(user);
}
}

删除操作

Dao包下,IUserDao接口中添加删除方法

/**
* 根据Id删除用户
* @param userId
*/
void deleteUser(Integer userId);

IUserDao.xml Mapper映射文件中,编写删除的配置方法

<!-- 删除用户-->
<delete id="deleteUser" parameterType="java.lang.Integer">
<!-- 接口参数只有一个时,且参数类型为Interger,占位符的名称可以随便写,例如:#{uid}-->
delete from user where id = #{uid}
</delete>

测试类:

 /**
* 测试Mybatis的CRUD操作
*/
public class MybatisTest {
private InputStream in;
private SqlSession sqlSession;
private IUserDao userDao;

@Before//用于在测试方法执行之前执行
public void init()throws Exception{
//1.读取配置文件,生成字节输入流
in = Resources.getResourceAsStream("SqlMapConfig.xml");
//2.获取SqlSessionFactory
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in);
//3.获取SqlSession对象
sqlSession = factory.openSession();
//4.获取dao的代理对象
userDao = sqlSession.getMapper(IUserDao.class);
}

@After//用于在测试方法执行之后执行
public void destroy()throws Exception{
//提交事务
sqlSession.commit();
//6.释放资源
sqlSession.close();
in.close();
}


/**
* 测试删除操作
*/
@Test
public void testDelete(){
//5.执行删除方法
userDao.deleteUser(49);
}
}

根据ID查询

Dao包下,IUserDao接口中添加根据ID方法

/**
* 根据id查询用户信息
* @param userId
* @return
*/
User findById(Integer userId);

IUserDao.xml Mapper映射文件中,编写对应Sql

 <!-- 根据id查询用户 -->
<select id="findById" parameterType="INT" resultType="com.mybatis.domain.User">
select * from user where id = #{uid}
</select>

测试类:

 /**
* 测试Mybatis的CRUD操作
*/
public class MybatisTest {
private InputStream in;
private SqlSession sqlSession;
private IUserDao userDao;

@Before//用于在测试方法执行之前执行
public void init()throws Exception{
//1.读取配置文件,生成字节输入流
in = Resources.getResourceAsStream("SqlMapConfig.xml");
//2.获取SqlSessionFactory
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in);
//3.获取SqlSession对象
sqlSession = factory.openSession();
//4.获取dao的代理对象
userDao = sqlSession.getMapper(IUserDao.class);
}

@After//用于在测试方法执行之后执行
public void destroy()throws Exception{
//提交事务
sqlSession.commit();
//6.释放资源
sqlSession.close();
in.close();
}


/**
* 根据ID查询
*/
@Test
public void testFindOne(){
//5.执行查询一个方法
User user = userDao.findById(48);
System.out.println(user);
}
}

模糊查询

Dao包下,IUserDao接口中添加模糊查询方法

/**
* 根据名称模糊查询用户信息
* @param username
* @return
*/
List<User> findByName(String username);

IUserDao.xml Mapper映射文件中,编写对应Sql

<!-- 根据名称模糊查询 -->
<select id="findByName" parameterType="string" resultType="com.mybatis.domain.User">
select * from user where username like #{name}
<!-- select * from user where username like '%${value}%'-->
</select>

测试类:

 /**
* 测试Mybatis的CRUD操作
*/
public class MybatisTest {
private InputStream in;
private SqlSession sqlSession;
private IUserDao userDao;

@Before//用于在测试方法执行之前执行
public void init()throws Exception{
//1.读取配置文件,生成字节输入流
in = Resources.getResourceAsStream("SqlMapConfig.xml");
//2.获取SqlSessionFactory
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in);
//3.获取SqlSession对象
sqlSession = factory.openSession();
//4.获取dao的代理对象
userDao = sqlSession.getMapper(IUserDao.class);
}

@After//用于在测试方法执行之后执行
public void destroy()throws Exception{
//提交事务
sqlSession.commit();
//6.释放资源
sqlSession.close();
in.close();
}


/**
* 测试模糊查询操作
*/
@Test
public void testFindByName(){
//5.执行查询一个方法
List<User> users = userDao.findByName("%小%");
//List<User> users = userDao.findByName("王");
for(User user : users){
System.out.println(user);
}
}
}

我们在配置文件中没有加入%来作为模糊查询的条件,所以在传入字符串实参时,就需要给定模糊查询的标识%。配置文件中的#{username}也只是一个占位符,所以 SQL 语句显示为“?”。

模糊查询的另一种配置方式

第一步:修改 SQL 语句的配置,配置如下:

<!-- 根据名称模糊查询 -->
<select id="findByName" parameterType="string" resultType="com.mybatis.domain.User">
select * from user where username like '%${value}%'
</select>

我们在上面将原来的#{}占位符,改成了${value}。注意如果用模糊查询的这种写法,那么${value}的写法就是固定的,不能写成其它名字。

第二步:测试,如下:

 /**
* 测试Mybatis的CRUD操作
*/
public class MybatisTest {
private InputStream in;
private SqlSession sqlSession;
private IUserDao userDao;

@Before//用于在测试方法执行之前执行
public void init()throws Exception{
//1.读取配置文件,生成字节输入流
in = Resources.getResourceAsStream("SqlMapConfig.xml");
//2.获取SqlSessionFactory
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in);
//3.获取SqlSession对象
sqlSession = factory.openSession();
//4.获取dao的代理对象
userDao = sqlSession.getMapper(IUserDao.class);
}

@After//用于在测试方法执行之后执行
public void destroy()throws Exception{
//提交事务
sqlSession.commit();
//6.释放资源
sqlSession.close();
in.close();
}


/**
* 测试模糊查询操作
*/
@Test
public void testFindByName(){
//5.执行查询一个方法
List<User> users = userDao.findByName("王");
for(User user : users){
System.out.println(user);
}
}
}

#{}与${}的区别
#{}表示一个占位符号
通过#{}可以实现 preparedStatement 向占位符中设置值,自动进行 java 类型和 jdbc 类型转换,#{}可以有效防止 sql 注入。 #{}可以接收简单类型值或 pojo 属性值。 如果 parameterType 传输单个简单类型值,#{}括号中可以是 value 或其它名称。
${}表示拼接 sql 串
通过${}可以将 parameterType 传入的内容拼接在 sql 中且不进行 jdbc 类型转换, ${}可以接收简单类型值或 pojo 属性值,如果 parameterType 传输单个简单类型值,${}括号中只能是 value。

聚合函数查询

Dao包下,IUserDao接口中添加查询总数方法

/**
* 查询总用户数
* @return
*/
int findTotal();

IUserDao.xml Mapper映射文件中,编写对应Sql

 <!-- 获取用户的总记录条数 -->
<select id="findTotal" resultType="int">
select count(id) from user;
</select>

测试类:

 /**
* 测试Mybatis的CRUD操作
*/
public class MybatisTest {
private InputStream in;
private SqlSession sqlSession;
private IUserDao userDao;

@Before//用于在测试方法执行之前执行
public void init()throws Exception{
//1.读取配置文件,生成字节输入流
in = Resources.getResourceAsStream("SqlMapConfig.xml");
//2.获取SqlSessionFactory
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in);
//3.获取SqlSession对象
sqlSession = factory.openSession();
//4.获取dao的代理对象
userDao = sqlSession.getMapper(IUserDao.class);
}

@After//用于在测试方法执行之后执行
public void destroy()throws Exception{
//提交事务
sqlSession.commit();
//6.释放资源
sqlSession.close();
in.close();
}


/**
* 测试查询总记录条数
*/
@Test
public void testFindTotal(){
//5.执行查询一个方法
int count = userDao.findTotal();
System.out.println(count);
}
}

获取保存数据的ID

新增用户后,同时还要返回当前新增用户的 id 值,因为 id 是由数据库的自动增长来实现的,所以就相当于我们要在新增后将自动增长 auto_increment 的值返回。

<insert id="saveUser" parameterType="com.mybatis.domain.User">
<!-- 配置保存时获取插入的 id -->
<selectKey keyColumn="id" keyProperty="id" resultType="int">
select last_insert_id();
</selectKey>
insert into user(username,birthday,sex,address) values(#{username},#{birthday},#{sex},#{address})
</insert>

测试方法:

/**
* 测试保存操作
*/
@Test
public void testSave(){
User user = new User();
user.setUsername("modify User property");
user.setAddress("北京市顺义区");
user.setSex("男");
user.setBirthday(new Date());
System.out.println("保存前:"+user);
int id=userDao.saveUser(user);
System.out.println(user.getId());
System.out.println("保存后:"+user);
}

第4章 Mybatis 的参数深入

我们在上一章节中已经介绍了 SQL 语句传参,使用标签的 parameterType 属性来设定。该属性的取值可以是基本类型引用类型(例如:String 类型),还可以是实体类类型(POJO 类)。同时也可以使用实体类的包装类,本章节将介绍如何使用实体类的包装类作为参数传递。

4.1 parameterType 配置参数

传递简单类型

基本类型和String我们可以直接写类型名称,也可以使用包名. 类名的方式,例如 :java.lang.String
实体类类型,目前我们只能使用全限定类名。
究其原因,是 mybaits 在加载时已经把常用的数据类型注册了别名,从而我们在使用时可以不写包名,而我们的是实体类并没有注册别名,所以必须写全限定类名。

传递POJO对象

MyBatis使用OGNL表达式解析对象字段的值,#{}或者${}括号中的值为pojo属性名称。

OGNL表达式:对象图导航语言(Object Graphic Navigation Language)

它是通过对象的取值方法来获取数据。在写法上把get给省略了。
比如:我们获取用户的名称

类中的写法:user.getUsername();
OGNL表达式写法:user.username

MyBatis中为什么能直接写username,而不用user.呢?
因为在parameterType中已经提供了属性所属的类,所以此时不需要写对象名。

4.2 传递 pojo 包装对象

开发中通过 pojo 传递查询条件 ,查询条件是综合的查询条件,不仅包括用户查询条件还包括其它的查询条件(比如将用户购买商品信息也作为查询条件),这时可以使用包装对象传递输入参数。
Pojo 类中包含 pojo。
需求:根据用户名查询用户信息,查询条件放到 QueryVo 的 user 属性中。

编写QueryVo:

public class QueryVo {

private User user;

public User getUser() {
return user;
}

public void setUser(User user) {
this.user = user;
}
}

Dao包下,IUserDao接口中添加queryVo查询方法

 /**
* 根据queryVo中的条件查询用户
* @param vo
* @return
*/
List<User> findUserByVo(QueryVo vo);

编写Mapper映射文件:

<!-- 根据queryVo的条件查询用户 -->
<select id="findUserByVo" parameterType="com.mybatis.domain.QueryVo" resultType="com.mybatis.domain.User">
select * from user where username like #{user.username}
<!--因为QueryVo中有user对象,所以直接写user。但是user也是个对象,所以写成user.(username)属性-->
</select>

测试方法:

/**
* 测试使用QueryVo作为查询条件
*/
@Test
public void testFindByVo(){
QueryVo vo = new QueryVo();
User user = new User();
user.setUsername("%王%");
vo.setUser(user);
//5.执行查询一个方法
List<User> users = userDao.findUserByVo(vo);
for(User u : users){
System.out.println(u);
}
}

第5章 Mybatis 的输出结果封装

5.1 resultType 配置结果类型

基本类型示例

  1. Dao接口

    /**
    * 查询总记录条数
    * @return
    */
    int findTotal();
  2. 映射配置

    <!-- 查询总记录条数 -->
    <select id="findTotal" resultType="int">
    select count(*) from user;
    </select>

实体类类型示例

  1. Dao接口

    /**
    * 查询所有用户
    * @return
    */
    List<User> findAll();
  2. 映射配置

    <!-- 查询总记录条数 -->
    <!-- 配置查询所有操作 -->
    <select id="findAll" resultType="com.mybatis.domain.User">
    select * from user
    </select>

特殊情况示例

以前我们写的对象属性和数据库列名都是一致的,那么要不一致怎么办?

修改实体类对象User:

public class User implements Serializable {
private Integer userId;
private String userName;
private String userAddress;
private String userSex;
private Date userBirthday;
//省略get/Set/toString方法
}

此时的实体类属性和数据库表的列名已经不一致了.

Dao接口方法:

/**
* 查询所有用户
* @return
*/
List<User> findAll();

Mapper映射文件:

<!-- 配置查询所有操作 -->
<select id="findAll" resultType="com.mybatis.domain.User">
select * from user
</select>

我们再次执行findAll方法:

/**
* 测试查询所有
*/
@Test
public void testFindAll(){
//5.执行查询所有方法
List<User> users = userDao.findAll();
for(User user : users){
System.out.println(user);
}
}

输出结果:

ser{userId=null, userName='老王', userAddress='null', userSex='null', userBirthday=null}
User{userId=null, userName='小二王', userAddress='null', userSex='null', userBirthday=null}
User{userId=null, userName='小二王', userAddress='null', userSex='null', userBirthday=null}
User{userId=null, userName='曾庆', userAddress='null', userSex='null', userBirthday=null}
User{userId=null, userName='老王', userAddress='null', userSex='null', userBirthday=null}
User{userId=null, userName='小马宝莉', userAddress='null', userSex='null', userBirthday=null}

为什么名称会有值呢?
因为Mysql 在 windows 系统中不区分大小写!

那我们有没有办法解决这个问题呢? 修改映射配置可以解决这个问题,使用别名查询。

<!-- 配置查询所有操作 -->
<select id="findAll" resultType="com.mybatis.domain.User">
select id as userId,username as userName,birthday as userBirthday,
sex as userSex,address as userAddress from user
</select>

这样数据库中别名和实体中别名保持了一致,运行后发现结果能正常显示了,证明可以封装到对象中,如果我们的查询很多,都使用别名的话写起来岂不是很麻烦,有没有别的解决办法呢?请看下一章节

5.2 resultMap 结果类型

resultMap 标签可以建立查询的列名和实体类的属性名称不一致时建立对应关系。从而实现封装。
在 select 标签中使用 resultMap 属性指定引用即可。同时 resultMap 可以实现将查询结果映射为复杂类型的 pojo,比如在查询结果映射对象中包括 pojo 和 list 实现一对一查询和一对多查询。

定义 resultMap

<!-- 配置 查询结果的列名和实体类的属性名的对应关系 -->
<resultMap id="userMap" type="com.mybatis.domain.User">
<!-- 主键字段的对应 -->
<id property="userId" column="id"></id>
<!--非主键字段的对应-->
<result property="userName" column="username"></result>
<result property="userAddress" column="address"></result>
<result property="userSex" column="sex"></result>
<result property="userBirthday" column="birthday"></result>
</resultMap>

id 标签:用于指定主键字段
result标签:用于指定非主键字段
column属性:用于指定数据库列名
property属性:用于指定实体类属性名称

映射配置

<!-- 配置查询所有操作 -->
<select id="findAll" resultMap="userMap"> <!-- 必须改成resultMap -->
select * from user
</select>

我们再次执行findAll方法:

/**
* 测试查询所有
*/
@Test
public void testFindAll(){
//5.执行查询所有方法
List<User> users = userDao.findAll();
for(User user : users){
System.out.println(user);
}
}

发现查询成功了。

第6章 SqlMapConfig.xml配置文件

SqlMapConfig.xml中配置的内容和顺序

-properties(属性)
  –property

-settings(全局配置参数)
  –setting

-typeAliases(类型别名)
  –typeAliase
  –package
-typeHandlers(类型处理器)
-objectFactory(对象工厂)
-plugins(插件)

-environments(环境集合属性对象)
  –environment(环境子属性对象)
   —transactionManager(事务管理)
   —dataSource(数据源)

-mappers(映射器)
  –mapper
  –package

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 配置properties
可以在标签内部配置连接数据库的信息。也可以通过属性引用外部配置文件信息
resource属性: 常用的
用于指定配置文件的位置,是按照类路径的写法来写,并且必须存在于类路径下。
url属性:
是要求按照Url的写法来写地址
URL:Uniform Resource Locator 统一资源定位符。它是可以唯一标识一个资源的位置。
它的写法:
http://localhost:8080/mybatisserver/demo1Servlet
协议 主机 端口 URI

URI:Uniform Resource Identifier 统一资源标识符。它是在应用中可以唯一定位一个资源的。
-->
<properties resource="jdbcConfig.properties">
<!-- <property name="driver" value="com.mysql.jdbc.Driver"></property>
<property name="url" value="jdbc:mysql://localhost:3306/eesy_mybatis"></property>
<property name="username" value="root"></property>
<property name="password" value="1234"></property>-->
</properties>

<!--使用typeAliases配置别名,它只能配置domain中类的别名 -->
<typeAliases>
<!--typeAlias用于配置别名。type属性指定的是实体类全限定类名。alias属性指定别名,当指定了别名就再区分大小写
<typeAlias type="com.mybatis.domain.User" alias="user"></typeAlias>-->

<!-- 用于指定要配置别名的包,当指定之后,该包下的实体类都会注册别名,并且类名就是别名,不再区分大小写-->
<package name="com.mybatis.domain"></package>
</typeAliases>

<!--配置环境-->
<environments default="mysql">
<!-- 配置mysql的环境-->
<environment id="mysql">
<!-- 配置事务 -->
<transactionManager type="JDBC"></transactionManager>

<!--配置连接池-->
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"></property>
<property name="url" value="${jdbc.url}"></property>
<property name="username" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>
</dataSource>
</environment>
</environments>
<!-- 配置映射文件的位置 -->
<mappers>
<!--<mapper resource="com/mybatis/dao/IUserDao.xml"></mapper>-->
<!-- package标签是用于指定dao接口所在的包,当指定了之后就不需要在写mapper以及resource或者class了 -->
<package name="com.mybatis.dao"></package>
</mappers>
</configuration>

6.1 properties(属性)

在使用 properties 标签配置时,我们可以采用两种方式指定属性配置。

第一种:在标签内部配置连接数据库的信息

<properties>
<property name="driver" value="com.mysql.jdbc.Driver"></property>
<property name="url" value="jdbc:mysql://localhost:3306/eesy_mybatis"></property>
<property name="username" value="root"></property>
<property name="password" value="1234"></property>
</properties>

那么,我们在下面就可以直接引用上面所配置了,方式: ${property name}

<!--配置环境-->
<environments default="mysql">
<!-- 配置mysql的环境-->
<environment id="mysql">
<!-- 配置事务 -->
<transactionManager type="JDBC"></transactionManager>

<!--配置连接池-->
<dataSource type="POOLED">
<property name="driver" value="${driver}"></property>
<property name="url" value="${url}"></property>
<property name="username" value="${username}"></property>
<property name="password" value="${password}"></property>
</dataSource>
</environment>
</environments>

肯定有人会说,这不是吃饱了撑的嘛,在下面写的好好的,非要写到上面去。其实这种写法是支持提取到外部的配置文件中,请看下面第二种方式

第二种:也可以通过属性引用外部配置文件信息
resources 下定义 jdbcConfig.properties 文件

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/eesy
jdbc.username=root
jdbc.password=1234

外部配置文件写好之后,我们需要在SqlMapConfig.xml进行引用了。配置properties有两个属性:
resource属性:常用的, 用于指定配置文件的位置,是按照类路径的写法来写,并且必须存在于类路径下。

<properties resource="jdbcConfig.properties">
<!-- <property name="driver" value="com.mysql.jdbc.Driver"></property>
<property name="url" value="jdbc:mysql://localhost:3306/eesy_mybatis"></property>
<property name="username" value="root"></property>
<property name="password" value="1234"></property>-->
</properties>

同时,我们环境配置也需要修改,不能只写name,需要写jdbc.name。因为我们在jdbcConfig.properties是以jdbc.name开头设置的。

<!--配置环境-->
<environments default="mysql">
<!-- 配置mysql的环境-->
<environment id="mysql">
<!-- 配置事务 -->
<transactionManager type="JDBC"></transactionManager>

<!--配置连接池-->
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"></property>
<property name="url" value="${jdbc.url}"></property>
<property name="username" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>
</dataSource>
</environment>
</environments>

url属性:是要求按照Url的写法来写地址。
  URL:Uniform Resource Locator 统一资源定位符。它是可以唯一标识一个资源的位置。
  写法:http://localhost:8080/mybatisserver/demo1Servlet
  URI:Uniform Resource Identifier 统一资源标识符。它是在应用中可以唯一定位一个资源的。

<properties url="file:///D:/Svideo/jdbcConfig.properties">
<!-- <property name="driver" value="com.mysql.jdbc.Driver"></property>
<property name="url" value="jdbc:mysql://localhost:3306/eesy_mybatis"></property>
<property name="username" value="root"></property>
<property name="password" value="1234"></property>-->
</properties>

6.2 typeAliases(类型别名)

Mybatis支持默认别名,比如xml中 parameterType属性中的intSting 想大写就大写想小写就小写。但是我们实体对象缺不能随便写。这是因为基本类型都配置默认别名。我们也可以采用自定义别名方式来开发。
自定义别名
在 SqlMapConfig.xml 中配置:

<!--使用typeAliases配置别名,它只能配置domain中类的别名 -->
<typeAliases>
<!--typeAlias用于配置别名。type属性指定的是实体类全限定类名。alias属性指定别名,当指定了别名就再区分大小写
<typeAlias type="com.mybatis.domain.User" alias="user"></typeAlias>-->

<!-- 用于指定要配置别名的包,当指定之后,该包下的实体类都会注册别名,并且类名就是别名,不再区分大小写-->
<package name="com.mybatis.domain"></package>
</typeAliases>

6.3 mappers(映射器)

1.<package name=””/>:注册指定包下的所有 mapper 接口如:

<package name="com.mybatis.dao"/>

示例:

<!-- 配置映射文件的位置 -->
<mappers>
<!--<mapper resource="com/mybatis/dao/IUserDao.xml"></mapper>-->
<!-- package标签是用于指定dao接口所在的包,当指定了之后就不需要在写mapper以及resource或者class了 -->
<package name="com.mybatis.dao"></package>
</mappers>

注意:此种方法要求 mapper 接口名称和 mapper 映射文件名称相同,且放在同一个目录中。

2.<mapper resource=” “ />:使用相对于类路径的资源如:

<mapper resource="com/mybatis/dao/IUserDao.xml" />

3.<mapper class=” “ />:使用 mapper 接口类路径如:

<mapper class="com.mybatis.dao.UserDao"/>

注意:此种方法要求 mapper 接口名称和 mapper 映射文件名称相同,且放在同一个目录中。

第7章 Mybatis 连接池与事务深入

连接池:我们在实际开发中都会使用连接池。我们在实际开发中都会使用连接池。

7.1 Mybatis 的连接池技术

我们在前面的 WEB 课程中也学习过类似的连接池技术,而在 Mybatis 中也有连接池技术,但是它采用的是自己的连接池技术。在 Mybatis 的 qlMapConfig.xml 配置文件中,通过来实现 Mybatis 中连接池的配置。

mybatis连接池提供了3种方式的配置,配置的位置在主配置文件SqlMapConfig.xml中的dataSource标签。type属性就是表示采用何种连接池方式。
type属性的取值:
POOLED:采用传统的javax.sql.DataSource规范中的连接池,mybatis中有针对规范的实现
UNPOOLED:采用传统的获取连接的方式,虽然也实现Javax.sql.DataSource接口,但是并没有使用池的思想。
JNDI:采用服务器提供的JNDI技术实现,来获取DataSource对象,不同的服务器所能拿到DataSource是不一样。

注意:如果不是web或者maven的war工程,是不能使用的。我们课程中使用的是tomcat服务器,采用连接池就是dbcp连接池。

7.2 Mybatis 的事务控制

什么是事务:数据库事务是指作为单个逻辑单元执行的一系列操作,要么完全执行,玩么完全地不执行。

事务的四大特性ACID:原子性、一致性、隔离性和持久性。

原子性(Atomicity):一个原子事务要么完整执行,要么干脆不执行。这意味着,工作单元中的每项任务都必须正确执行。如果有任一任务执行失败,则整个工作单元或事务就会被终止。即此前对数据所作的任何修改都将被撤销。如果所有任务都被成功执行,事务就会被提交,即对数据所作的修改将会是永久性的。

一致性(Consistency): 一致性代表了底层数据存储的完整性。它必须由事务系统和应用开发人员共同来保证。事务系统通过保证事务的原子性,隔离性和持久性来满足这一要求; 应用开发人员则需要保证数据库有适当的约束(主键,引用完整性等),并且工作单元中所实现的业务逻辑不会导致数据的不一致(即,数据预期所表达的现实业务情况不相一致)。例如,在一次转账过程中,从某一账户中扣除的金额必须与另一账户中存入的金额相等。

隔离性(Isolation):隔离性意味着事务必须在不干扰其他进程或事务的前提下独立执行。换言之,在事务或工作单元执行完毕之前,其所访问的数据不能受系统其他部分的影响。

持久性(Durability):持久性表示在某个事务的执行过程中,对数据所作的所有改动都必须在事务成功结束前保存至某种物理存储设备。这样可以保证,所作的修改在任何系统瘫痪时不至于丢失。

不考虑隔离性会产生的3个问题:解决办法四种隔离级别

数据库事务的隔离级别有4种,由低到高分别为Read uncommitted 、Read committed 、Repeatable read 、Serializable 。而且,在事务的并发操作中可能会出现脏读,不可重复读,幻读。下面通过事例一一阐述它们的概念与联系。

Read uncommitted:读未提交,顾名思义,就是一个事务可以读取另一个未提交事务的数据。
事例:老板要给程序员发工资,程序员的工资是3.6万/月。但是发工资时老板不小心按错了数字,按成3.9万/月,该钱已经打到程序员的户口,但是事务还没有提交,就在这时,程序员去查看自己这个月的工资,发现比往常多了3千元,以为涨工资了非常高兴。但是老板及时发现了不对,马上回滚差点就提交了的事务,将数字改成3.6万再提交。
分析:实际程序员这个月的工资还是3.6万,但是程序员看到的是3.9万。他看到的是老板还没提交事务时的数据。这就是脏读。

那怎么解决脏读呢?Read committed!读提交,能解决脏读问题。

Read committed:读提交,顾名思义,就是一个事务要等另一个事务提交后才能读取数据。
事例:程序员拿着信用卡去享受生活(卡里当然是只有3.6万),当他埋单时(程序员事务开启),收费系统事先检测到他的卡里有3.6万,就在这个时候!!程序员的妻子要把钱全部转出充当家用,并提交。当收费系统准备扣款时,再检测卡里的金额,发现已经没钱了(第二次检测金额当然要等待妻子转出金额事务提交完)。程序员就会很郁闷,明明卡里是有钱的…
分析:这就是读提交,若有事务对数据进行更新(UPDATE)操作时,读操作事务要等待这个更新操作事务提交后才能读取数据,可以解决脏读问题。但在这个事例中,出现了一个事务范围内两个相同的查询却返回了不同数据,这就是不可重复读

那怎么解决可能的不可重复读问题?Repeatable read !

Repeatable read:重复读,就是在开始读取数据(事务开启)时,不再允许修改操作
事例:程序员拿着信用卡去享受生活(卡里当然是只有3.6万),当他埋单时(事务开启,不允许其他事务的UPDATE修改操作),收费系统事先检测到他的卡里有3.6万。这个时候他的妻子不能转出金额了。接下来收费系统就可以扣款了。
分析:重复读可以解决不可重复读问题。写到这里,应该明白的一点就是,不可重复读对应的是修改,即UPDATE操作。但是可能还会有幻读问题。因为幻读问题对应的是插入INSERT操作,而不是UPDATE操作

什么时候会出现幻读?
事例:程序员某一天去消费,花了2千元,然后他的妻子去查看他今天的消费记录(全表扫描FTS,妻子事务开启),看到确实是花了2千元,就在这个时候,程序员花了1万买了一部电脑,即新增INSERT了一条消费记录,并提交。当妻子打印程序员的消费记录清单时(妻子事务提交),发现花了1.2万元,似乎出现了幻觉,这就是幻读。

那怎么解决幻读问题?Serializable!

Serializable 序列化:Serializable 是最高的事务隔离级别,在该级别下,事务串行化顺序执行,可以避免脏读、不可重复读与幻读。但是这种事务隔离级别效率低下,比较耗数据库性能,一般不使用。

值得一提的是:大多数数据库默认的事务隔离级别是Read committed,比如Sql Server , Oracle。Mysql的默认隔离级别是Repeatable read。

第8章 Mybatis 的动态 SQL 语句

Mybatis 的映射文件中,前面我们的 SQL 都是比较简单的,有些时候业务逻辑复杂时,我们的 SQL 是动态变化的,此时在前面的学习中我们的 SQL 就不能满足要求了。

8.1 动态 SQL 之<if>标签

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

编写持久层Dao接口

/**
* 根据传入参数条件
* @param user 查询的条件:有可能有用户名,有可能有性别,也有可能有地址,还有可能是都有
* @return
*/
List<User> findUserByCondition(User user);

持久层 Dao 映射配置:
注:这里已经配置 查询结果的列名和实体类的属性名的对应关系-userMap(配置了别名)

<select id="findUserByCondition" resultMap="userMap" parameterType="user">
select * from user where 1=1
<if test="userName != null">
and username = #{userName}
</if>
<if test="userSex != null">
and sex = #{userSex}
</if>
</select>

注:凡是sql语句中的属性名无关大小写,但是判断条件是需要大小写的,与实体属性名保持一致。如判断条件和中括号中的属性名。
这种方式有风险,如果一旦条件都不匹配会出现批量查询或者批量删除。应尽量避免此种方式。

8.2 动态 SQL 之<where>标签

为了简化上面 where 1=1 的条件拼装,我们可以采用<where>标签来简化开发。

持久层 Dao 映射配置

<select id="findUserByCondition" resultMap="userMap" parameterType="user">
select * from user
<where>
<if test="userName != null">
and username = #{userName}
</if>
<if test="userSex != null">
and sex = #{userSex}
</if>
</where>
</select>

8.3 动态标签之<foreach>标签

传入多个 id 查询用户信息,用下边两个 sql 实现:

SELECT * FROM USERS WHERE username LIKE '%张%' AND (id =10 OR id =89 OR id=16)
SELECT * FROM USERS WHERE username LIKE '%张%' AND id IN (10,89,16)

这样我们在进行范围查询时,就要将一个集合中的值,作为参数动态添加进来。
这样我们将如何进行参数的传递?

在 QueryVo 中加入一个 List 集合用于封装参数

public class QueryVo {

private User user;

private List<Integer> ids; //用于存放查询id的集合

//省略get/set方法

持久层Dao接口

/**
* 根据queryvo中提供的id集合,查询用户信息
* @param vo
* @return
*/
List<User> findUserInIds(QueryVo vo);

持久层 Dao 映射配置

<!-- 了解的内容:抽取重复的sql语句-->
<sql id="defaultUser">
select * from user
</sql>
<!-- 根据queryvo中的Id集合实现查询用户列表 -->
<select id="findUserInIds" resultMap="userMap" parameterType="queryvo">
<include refid="defaultUser"></include> <!--此处引用的是上面抽取的重复语句等同于 select * from user -->
<where>
<if test="ids != null and ids.size()>0">
<foreach collection="ids" open="and id in (" close=")" item="uid" separator=",">
#{uid} <!--此处的uid必须和item中定义的id名称一致,collection中的id名必须和实体中的保持一致-->
</foreach>
</if>
</where>
</select>

<foreach>标签用于遍历集合,它的属性:
  collection:代表要遍历的集合元素,注意编写时不要写#{}
  open:代表语句的开始部分
  close:代表结束部分
  item:代表遍历集合的每个元素,生成的变量名
  sperator:代表分隔符

测试方法:

@Test
public void testFindInIds() {
QueryVo vo = new QueryVo();
List<Integer> ids = new ArrayList<Integer>();
ids.add(41);
ids.add(42);
ids.add(43);
ids.add(46);
ids.add(57);
vo.setIds(ids);
//6.执行操作
List<User> users = userDao.findInIds(vo);
for(User user : users) {
System.out.println(user);
}
}

8.4 抽取重复的sql语句

<!-- 了解的内容:抽取重复的sql语句-->
<sql id="defaultUser">
select * from user
</sql>

引用:

<include refid="defaultUser"></include> <!--此处引用的是上面抽取的重复语句等同于  select * from user -->

第9章 Mybatis 多表查询之一对多

mybatis中的多表查询:
  示例:用户和账户
   一个用户可以有多个账户
   一个账户只能属于一个用户(多个账户也可以属于同一个用户)
  步骤:
   1、建立两张表:用户表,账户表,让用户表和账户表之间具备一对多的关系:需要使用外键在账户表中添加
   2、建立两个实体类:用户实体类和账户实体类,让用户和账户的实体类能体现出来一对多的关系
   3、建立两个配置文件,用户的配置文件和账户的配置文件
   4、实现配置:
    当我们查询用户时,可以同时得到用户下所包含的账户信息
    当我们查询账户时,可以同时得到账户的所属用户信息

Account数据库脚本

DROP TABLE IF EXISTS `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;

insert into `account`(`ID`,`UID`,`MONEY`) values (1,46,1000),(2,45,1000),(3,46,2000);

User数据库脚本

DROP TABLE IF EXISTS `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 AUTO_INCREMENT=57 DEFAULT CHARSET=utf8;

9.1 一对一查询(多对一)

方式一(通过Account子类方式查询)

需求:查询所有账户信息,关联查询下单用户信息。
注意:因为一个账户信息只能供某个用户使用,所以从查询账户信息出发关联查询用户信息为一对一查询。如果从用户信息出发查询用户下的账户信息则为一对多查询,因为一个用户可以有多个账户。

定义Account实体对象

public class Account implements Serializable {

private Integer id;
private Integer uid;
private Double money;
//省略get、set、toString方法
}

编写IAccountDao映射文件

<?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="com.mybatis.dao.IAccountDao">
<!-- 查询所有 -->
<select id="findAll" resultType="account">
select * from account
</select>
</mapper>

创建数据库IAccountDao接口

public interface IAccountDao {


/**
* 查询所有账户,并且带有用户名称和地址信息
* @return
*/
List<AccountUser> findAllAccount();

}

那么,问题来了,我们想查询所有账户,同时还要获取到当前账户的所属用户信息,需要先把sql语句写出来。

SELECT u.*,a.id as aid,a.uid,a.money from account a,user u where u.id=a.UID;

但是,目前Account类中是不能包含用户信息的,但是我们选择最直接最简单的方式,去创建一个AccountUser类,包含User对象的两个属性,并且继承父类Account。这样就可以查询到User对象的用户名和地址。也可以查询到Account的所有信息。

创建AccountUser类:

public class AccountUser extends Account {

private String username;
private String address;
//此处省略get、set方法
@Override
public String toString() {
return super.toString()+" AccountUser{" +
"username='" + username + '\'' +
", address='" + address + '\'' +
'}';
}
}

持久层IAccountDao.xml映射配置

<!--查询所有账户同时包含用户名和地址信息-->
<select id="findAllAccount" resultType="accountuser">
<!--因为AccountUser对象只定义了User的username和address,所以这里只查询user表中这个两个值-->
select a.*,u.username,u.address from account a , user u where u.id = a.uid;
</select>

定义专门的 pojo 类作为输出类型,其中定义了 sql 查询结果集所有的字段。此方法较为简单,企业中使用普遍。(说白了就是缺什么补什么)

方式二(建立实体类关系关系的方式)

顾名思义就是让用户和账户的实体类能体现出来一对多的关系,也就是从表实体应该包含一个主表实体的对象引用。
通过面向对象的(has a)关系可以得知,我们可以在 Account 类中加入一个 User 类的对象来代表这个账户是哪个用户的。

修改Account类

public class Account implements Serializable {

private Integer id;
private Integer uid;
private Double money;

//从表实体应该包含一个主表实体的对象引用
private User user; //因为一个账户只能对应一个用户,所以这个引用的是一个对象,而不是集合

//省略get、set、toString方法

}

以前mapper定义的resultType类型是Account,但是我们增加了主表实体对象引用,所以显然不能满足需求。我们需调定义封装account和user的resultMap映射。
修改IAccountDao.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="com.mybatis.dao.IAccountDao">

<!-- 定义封装account和user的resultMap -->
<resultMap id="accountUserMap" type="account"><!--此处使用类别名,未设置别名需要写全限定类名,com.mybatis.domain.Account-->
<!--配置Account数据封装-->
<id property="id" column="aid"></id> <!--因为查询是起了别名,所以这里写别名aid-->
<result property="uid" column="uid"></result>
<result property="money" column="money"></result>
<!-- 一对一的关系映射(用association标签):配置封装user的内容-->
<association property="user" column="uid" javaType="user"><!--此处使用类别名,未设置别名需要写全限定类名,com.mybatis.domain.User-->
<id property="id" column="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>
</association>
</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>
</mapper>

注:resultMap类型必须写成上边定义的accountUserMap,否则不能正常封装数据。

9.2 一对多查询

需求:查询所有用户信息及用户关联的账户信息。
分析:用户信息和他的账户信息为一对多关系,并且查询过程中如果用户没有账户信息,此时也要将用户信息查询出来,我们想到了左外连接查询比较合适。

编写sql

SELECT u.*, a.id id,a.uid, a.money FROM user u LEFT JOIN account a ON u.id = a.uid

修改User对象,在User对象中增加从表的集合引用

public class User implements Serializable {

private Integer id;
private String username;
private String address;
private String sex;
private Date birthday;

//一对多关系映射:主表实体应该包含从表实体的集合引用
private List<Account> accounts; //一个用户可能有多个账户,所以用集合
//省略get、set、toString方法

}

创建数据库IUserDao接口

/**
* 查询所有用户,同时获取到用户下所有账户的信息
* @return
*/
List<User> findAll();

编写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="com.mybatis.dao.IUserDao">

<!-- 定义User的resultMap-->
<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>
<!-- 配置user对象中accounts集合的映射(多对对用collection标签) -->
<collection property="accounts" ofType="account"> <!--ofType:集合中元素类型,此处使用类别名,未设置别名需要写全限定类名,com.mybatis.domain.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>

我们执行测试,会发现所有用户信息全部查出来,同时如果有账户信息的也会显示出来。

第10章 Mybatis 多表查询之多对多

mybatis中的多表查询:
  示例:用户和角色
   一个用户可以有多个角色
   一个角色可以赋予多个用户
  步骤:
   1、建立两张表:用户表,角色表,让用户表和角色表具有多对多的关系。需要使用中间表,中间表中包含各自的主键,在中间表中是外键
   2、建立两个实体类:用户实体类和角色实体类,让用户和角色的实体类能体现出来多对多的关系,各自包含对方一个集合引用
   3、建立两个配置文件,用户的配置文件和角色的配置文件
   4、实现配置:
    当我们查询用户时,可以同时得到用户所包含的角色信息
    当我们查询角色时,可以同时得到角色的所赋予的用户信息

角色Role数据库脚本:

DROP TABLE IF EXISTS `role`;

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;
insert into `role`(`ID`,`ROLE_NAME`,`ROLE_DESC`) values (1,'院长','管理整个学院'),(2,'总裁','管理整个公司'),(3,'校长','管理整个学校');

中间表user_role数据库脚本:

DROP TABLE IF EXISTS `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;

insert into `user_role`(`UID`,`RID`) values (41,1),(45,1),(41,2);

10.1 实现 Role 到 User 的多对多

通过前面的学习,我们使用 Mybatis 实现一对多关系的维护。多对多关系其实我们看成是双向的一对多关系。

创建Role实体对象:

public class User implements Serializable {

private Integer id;
private String username;
private String address;
private String sex;
private Date birthday;

//多对多的关系映射:一个用户可以具备多个角色
private List<Role> roles;
//省略get、set、toString方法
}

创建数据库Dao接口IRoleDao

public interface IRoleDao {

/**
* 查询所有角色
* @return
*/
List<Role> findAll();
}

分析对应关系:

编写对应的sql

select u.*,r.id as rid,r.role_name,r.role_desc from role r
left outer join user_role ur on r.id = ur.rid
left outer join user u on u.id = ur.uid

创建IRoleDao.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="com.mybatis.dao.IRoleDao">

<!--定义role表的ResultMap-->
<resultMap id="roleMap" type="role">
<id property="roleId" column="rid"></id><!--此处column原本应写id,但是我们在写查询sql时,起了别名rid,所以这里写rid。否则对不上-->
<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>

查询角色并获取角色下用户信息到这里就完成了。

10.2 实现 User 到 Role 的多对多

其实和上边原理差不多,正好相反,只需修改对应sql即可
修改数据库Dao接口IUserDao

public interface IUserDao {

/**
* 查询所有用户,同时获取到用户下所有账户的信息
* @return
*/
List<User> findAll();
}

修改User实体对象

public class User implements Serializable {

private Integer id;
private String username;
private String address;
private String sex;
private Date birthday;

//多对多的关系映射:一个用户可以具备多个角色
private List<Role> roles;
//省略get、set、toString方法
}

创建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="com.mybatis.dao.IUserDao">

<!-- 定义User的resultMap-->
<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>

第11章 JNDI数据源概述和原理

JNDI:Java Naming and Directory Interface。是SUN公司推出的一套规范,属于JavaEE技术之一。目的是模仿windows系统中的注册表。

创建一个Maven工程,并导入坐标

pom.xml下添加坐标

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.mybatis</groupId>
<artifactId>day03_eesy_05jndi</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>

<name>day03_eesy_05jndi Maven Webapp</name>
<!-- FIXME change it to the project's website -->
<url>http://www.example.com</url>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.7</maven.compiler.source>
<maven.compiler.target>1.7</maven.compiler.target>
</properties>

<dependencies>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.5</version>
</dependency>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.6</version>
</dependency>

<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.12</version>
</dependency>

<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.10</version>
</dependency>

<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
</dependency>
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>jsp-api</artifactId>
<version>2.0</version>
</dependency>
</dependencies>

<build>
<finalName>day03_eesy_05jndi</finalName>
<pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
<plugins>
<plugin>
<artifactId>maven-clean-plugin</artifactId>
<version>3.0.0</version>
</plugin>
<!-- see http://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_war_packaging -->
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>3.0.2</version>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.7.0</version>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.20.1</version>
</plugin>
<plugin>
<artifactId>maven-war-plugin</artifactId>
<version>3.2.0</version>
</plugin>
<plugin>
<artifactId>maven-install-plugin</artifactId>
<version>2.5.2</version>
</plugin>
<plugin>
<artifactId>maven-deploy-plugin</artifactId>
<version>2.8.2</version>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>

创建数据库Dao接口IUserDao

public interface IUserDao {

/**
* 查询所有用户
* @return
*/
List<User> findAll();

}

创建实体对象User

public class User implements Serializable {

private Integer userId;
private String userName;
private String userAddress;
private String userSex;
private Date userBirthday;

//省略get、set、toString方法
}

resources目录下车创建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="com.mybatis.dao.IUserDao">

<!-- 配置 查询结果的列名和实体类的属性名的对应关系 -->
<resultMap id="userMap" type="uSeR">
<!-- 主键字段的对应 -->
<id property="userId" column="id"></id>
<!--非主键字段的对应-->
<result property="userName" column="username"></result>
<result property="userAddress" column="address"></result>
<result property="userSex" column="sex"></result>
<result property="userBirthday" column="birthday"></result>
</resultMap>


<!-- 查询所有 -->
<select id="findAll" resultMap="userMap">
<!--select id as userId,username as userName,address as userAddress,sex as userSex,birthday as userBirthday from user;-->
select * from user;
</select>
</mapper>

创建log4j.properties

# Set root category priority to INFO and its only appender to CONSOLE.
#log4j.rootCategory=INFO, CONSOLE debug info warn error fatal
log4j.rootCategory=debug, CONSOLE, LOGFILE

# Set the enterprise logger category to FATAL and its only appender to CONSOLE.
log4j.logger.org.apache.axis.enterprise=FATAL, CONSOLE

# CONSOLE is set to be a ConsoleAppender using a PatternLayout.
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
log4j.appender.CONSOLE.layout.ConversionPattern=%d{ISO8601} %-6r [%15.15t] %-5p %30.30c %x - %m\n

# LOGFILE is set to be a File appender using a PatternLayout.
log4j.appender.LOGFILE=org.apache.log4j.FileAppender
log4j.appender.LOGFILE.File=d:\axis.log
log4j.appender.LOGFILE.Append=true
log4j.appender.LOGFILE.layout=org.apache.log4j.PatternLayout
log4j.appender.LOGFILE.layout.ConversionPattern=%d{ISO8601} %-6r [%15.15t] %-5p %30.30c %x - %m\n

创建SqlMapConfig.xml

<?xml version="1.0" encoding="UTF-8"?>
<!-- 导入约束 -->
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<typeAliases>
<package name="com.mybatis.domain"></package>
</typeAliases>
<!-- 配置mybatis的环境 -->
<environments default="mysql">
<!-- 配置mysql的环境 -->
<environment id="mysql">
<!-- 配置事务控制的方式 -->
<transactionManager type="JDBC"></transactionManager>
<!-- 配置连接数据库的必备信息 type属性表示是否使用数据源(连接池)-->
<dataSource type="JNDI">
<property name="data_source" value="java:comp/env/jdbc/eesy_mybatis"/>
</dataSource>
</environment>
</environments>

<!-- 指定mapper配置文件的位置 -->
<mappers>
<mapper resource="com/mybatis/dao/IUserDao.xml"/>
</mappers>
</configuration>

webapp文件下创建META-INF目录,在META-INF目录中建立一个名为context.xml的配置文件

<?xml version="1.0" encoding="UTF-8"?>
<Context>
<!--
<Resource
name="jdbc/eesy_mybatis" 数据源的名称
type="javax.sql.DataSource" 数据源类型
auth="Container" 数据源提供者
maxActive="20" 最大活动数
maxWait="10000" 最大等待时间
maxIdle="5" 最大空闲数
username="root" 用户名
password="1234" 密码
driverClassName="com.mysql.jdbc.Driver" 驱动类
url="jdbc:mysql://localhost:3306/eesy_mybatis" 连接url字符串
/>
-->
<Resource
name="jdbc/eesy_mybatis"
type="javax.sql.DataSource"
auth="Container"
maxActive="20"
maxWait="10000"
maxIdle="5"
username="root"
password="1234"
driverClassName="com.mysql.jdbc.Driver"
url="jdbc:mysql://localhost:3306/Mybatis"
/>
</Context>

webapp文件下创建index.jsp文件

<%@ page import="java.io.InputStream" %>
<%@ page import="org.apache.ibatis.io.Resources" %>
<%@ page import="org.apache.ibatis.session.SqlSessionFactoryBuilder" %>
<%@ page import="org.apache.ibatis.session.SqlSessionFactory" %>
<%@ page import="org.apache.ibatis.session.SqlSession" %>
<%@ page import="com.mybatis.dao.IUserDao" %>
<%@ page import="com.mybatis.domain.User" %>
<%@ page import="java.util.List" %>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<html>
<body>
<h2>Hello World!</h2>
<%
//1.读取配置文件
InputStream in = Resources.getResourceAsStream("SqlMapConfig.xml");
//2.根据配置文件构建SqlSessionFactory
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
SqlSessionFactory factory = builder.build(in);
//3.使用SqlSessionFactory创建SqlSession对象
SqlSession sqlSession = factory.openSession();
//4.使用SqlSession构建Dao的代理对象
IUserDao userDao = sqlSession.getMapper(IUserDao.class);
//5.执行dao中的findAll方法
List<User> users = userDao.findAll();
for(User user : users){
System.out.println(user);
}
//6.释放资源
sqlSession.close();
in.close();
%>
</body>
</html>

注:必须通过Tomcat方式才可以访问,单元测试不可以。Tomcat启动才会加载对应配置,如不启动,则无法找到。

第12章 Mybatis 延迟加载策略

通过前面的学习,我们已经掌握了 Mybatis 中一对一,一对多,多对多关系的配置及实现,可以实现对象的关联查询。实际开发过程中很多时候我们并不需要总是在加载用户信息时就一定要加载他的账户信息。此时就是我们所说的延迟加载。

12.1 何为延迟加载?

延迟加载:就是在需要用到数据时才进行加载,不需要用到数据时就不加载数据。延迟加载也称懒加载。

好处:先从单表查询,需要时再从关联表去关联查询,大大提高数据库性能,因为查询单表要比关联查询多张表速度要快。
坏处:因为只有当需要用到数据时,才会进行数据库查询,这样在大批量数据查询时,因为查询工作也要消耗时间,所以可能造成用户等待时间变长,造成用户体验下降。

12.2 何为立即加载?

不管用不用,只要一调用方法,马上发起查询。

12.3 实现需求

问题:在一对多中,当我们有一个用户,它有100个账户。
在查询用户的时候,要不要把关联的账户查出来? 在查询用户时,用户下的账户信息应该是,什么时候使用,什么时候查询的。
在查询账户的时候,要不要把关联的用户查出来? 在查询账户时,账户的所属用户信息应该是随着账户查询时一起查询出来。

在对应的四种表关系中:一对多,多对一,一对一,多对多
一对多,多对多:通常情况下我们都是采用延迟加载。
多对一,一对一:通常情况下我们都是采用立即加载。
我们使用了resultMap来实现一对一,一对多,多对多关系的操作。主要是通过 associationcollection 实现一对一及一对多映射。associationcollection 具备延迟加载功能。

12.4 使用 assocation 实现延迟加载(一对一)

创建数据库Dao层接口IAccountDao

public interface IAccountDao {

/**
* 查询所有账户,同时还要获取到当前账户的所属用户信息
* @return
*/
List<Account> findAll();
}

创建数据库Dao层接口IUserDao

public interface IUserDao {

/**
* 查询所有用户,同时获取到用户下所有账户的信息
* @return
*/
List<User> findAll();


/**
* 根据id查询用户信息
* @param userId
* @return
*/
User findById(Integer userId);


}

创建Account实体

public class Account implements Serializable {

private Integer id;
private Integer uid;
private Double money;

//从表实体应该包含一个主表实体的对象引用
private User user;
//省略get、set方法
}

创建Account实体

public class User implements Serializable {

private Integer id;
private String username;
private String address;
private String sex;
private Date birthday;

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

//省略get、set方法
}

编写IUserDao.xmlMapper映射文件

<?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="com.mybatis.dao.IUserDao">
<!-- 根据id查询用户 -->
<select id="findById" parameterType="INT" resultType="user">
select * from user where id = #{uid}
</select>

</mapper>

编写IAccountDao.xmlMapper映射文件

<?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="com.mybatis.dao.IAccountDao">

<!-- 定义封装account和user的resultMap -->
<resultMap id="accountUserMap" type="account">
<id property="id" column="id"></id>
<result property="uid" column="uid"></result>
<result property="money" column="money"></result>
<!-- 一对一的关系映射:配置封装user的内容
select属性指定的内容:查询用户的唯一标识:
column属性指定的内容:用户根据id查询时,所需要的参数的值
-->
<association property="user" column="uid" javaType="user" select="com.mybatis.dao.IUserDao.findById"></association>
</resultMap>

<!-- 查询所有 -->
<select id="findAll" resultMap="accountUserMap">
select * from account
</select>
</mapper>

执行测试方法:

 /**
* 测试查询所有
*/
@Test
public void testFindAll(){
List<Account> accounts = accountDao.findAll();
for(Account account : accounts){
System.out.println("--------每个account的信息------------");
System.out.println(account);
System.out.println(account.getUser());
}
}

当我们执行测试类时,发现一次性执行了3条语句,并没有真正实现延迟加载。想要实现真正的延迟加载,需要增加一个配置。

Mybatis官网,有如下说明:

打开SqlMapConfig.xml配置文件:

<!--配置参数-->
<settings>
<!--开启Mybatis支持延迟加载-->
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"></setting> <!--其实不用设置, 3.4.1 以后的版本默认为false-->
</settings>

完整的SqlMapConfig.xml配置文件:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 配置properties-->
<properties resource="jdbcConfig.properties"></properties>

<!--配置参数-->
<settings>
<!--开启Mybatis支持延迟加载-->
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"></setting>
</settings>

<!--使用typeAliases配置别名,它只能配置domain中类的别名 -->
<typeAliases>
<package name="com.mybatis.domain"></package>
</typeAliases>

<!--配置环境-->
<environments default="mysql">
<!-- 配置mysql的环境-->
<environment id="mysql">
<!-- 配置事务 -->
<transactionManager type="JDBC"></transactionManager>

<!--配置连接池-->
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"></property>
<property name="url" value="${jdbc.url}"></property>
<property name="username" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>
</dataSource>
</environment>
</environments>
<!-- 配置映射文件的位置 -->
<mappers>
<package name="com.mybatis.dao"></package>
</mappers>
</configuration>

再次执行测试方法:

 /**
* 测试查询所有
*/
@Test
public void testFindAll(){
List<Account> accounts = accountDao.findAll();
for(Account account : accounts){
System.out.println("--------每个account的信息------------");
System.out.println(account);
System.out.println(account.getUser());
}
}

当我执行测试方法时,发现和上次有些变化,未开启延迟加载是一次性输出,开启后是一个一个输出。

为什么还是执行了3条语句呢,因为我们在测试类里面输出了用户的信息,这样说明了有需要则会进行加载。

那我们把整个遍历都注释掉:

执行测试方法:
```java
/**
* 测试查询所有
*/
@Test
public void testFindAll(){
List<Account> accounts = accountDao.findAll();
/*for(Account account : accounts){
System.out.println("--------每个account的信息------------");
System.out.println(account);
System.out.println(account.getUser());
}*/
}

再次执行,发现就只会执行一条语句了。

12.5 使用 Collection 实现延迟加载(一对多)

同样我们也可以在一对多关系配置的<collection<结点中配置延迟加载策略。<collection<结点中也有 select 属性,column 属性。

因为我们延迟加载部分需要用用到根据id查询Account,所以新增根据id查询接口

修改数据库Dao层接口IAccountDao

public interface IAccountDao {

/**
* 查询所有账户,同时还要获取到当前账户的所属用户信息
* @return
*/
List<Account> findAll();

/**
* 根据用户id查询账户信息
* @param uid
* @return
*/
List<Account> findAccountByUid(Integer uid);

}

修改IAccountDao.xmlMapper映射文件

<?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="com.mybatis.dao.IAccountDao">

<!-- 定义封装account和user的resultMap -->
<resultMap id="accountUserMap" type="account">
<id property="id" column="id"></id>
<result property="uid" column="uid"></result>
<result property="money" column="money"></result>
<!-- 一对一的关系映射:配置封装user的内容
select属性指定的内容:查询用户的唯一标识:
column属性指定的内容:用户根据id查询时,所需要的参数的值
-->
<association property="user" column="uid" javaType="user" select="com.mybatis.dao.IUserDao.findById"></association>
</resultMap>
<!-- 根据用户id查询账户列表 -->
<select id="findAccountByUid" resultType="account">
select * from account where uid = #{uid}
</select>

</mapper>

修改IUserDao.xmlMapper映射文件

<?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="com.mybatis.dao.IUserDao">

<!-- 定义User的resultMap-->
<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>
<!-- 配置user对象中accounts集合的映射 -->
<collection property="accounts" ofType="account" select="com.mybatis.dao.IAccountDao.findAccountByUid" column="id"></collection>
</resultMap>

<!-- 查询所有 -->
<select id="findAll" resultMap="userAccountMap">
select * from user
</select>
</mapper>

编写测试类:

  /**
* 测试查询所有
*/
public void testFindAll(){
List<User> users = userDao.findAll();
/* for(User user : users){
System.out.println("-----每个用户的信息------");
System.out.println(user);
System.out.println(user.getAccounts());
}*/
}

再次执行,我们发现已经实现了延迟加载。延迟加载的思想就是,在用到的时候,去调用对方映射文件的一个配置来实现查询功能。

第13章 Mybatis 缓存

什么是缓存:存在于内存中的临时数据。
为什么使用缓存:减少和数据库的交互次数,提高执行效率。
同时问题来了,什么样数据适用于缓存,什么样数据不适应缓存。

适用于缓存:
   经常查询并且不经常改变的。
   数据的正确与否对最终结果影响不大的。(存在缓存和数据库中数据不同步问题)

不适用于缓存:
   经常改变的数据
   数据的正确与否对最终结果影响很大的。例如:商品的库存,银行的汇率,股市的牌价。

13.1 Mybatis 一级缓存

一级缓存:
   它指的是Mybatis中SqlSession对象的缓存。
   当我们执行查询之后,查询的结果会同时存入到SqlSession为我们提供一块区域中。
   该区域的结构是一个Map。当我们再次查询同样的数据,mybatis会先去sqlsession中
   查询是否有,有的话直接拿出来用。
   当SqlSession对象消失时,mybatis的一级缓存也就消失了。

编写用户持久层 Dao 接口IUserDao

public interface IUserDao {
/**
* 根据id查询用户信息
* @param userId
* @return
*/
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="com.mybatis.dao.IUserDao">
<!-- 根据id查询用户 -->
<select id="findById" parameterType="INT" resultType="user" useCache="true">
select * from user where id = #{uid}
</select>

编写测试类:

private InputStream in;
private SqlSessionFactory factory;
private SqlSession sqlSession;
private IUserDao userDao;

@Before//用于在测试方法执行之前执行
public void init()throws Exception{
//1.读取配置文件,生成字节输入流
in = Resources.getResourceAsStream("SqlMapConfig.xml");
//2.获取SqlSessionFactory
factory = new SqlSessionFactoryBuilder().build(in);
//3.获取SqlSession对象
sqlSession = factory.openSession(true);
//4.获取dao的代理对象
userDao = sqlSession.getMapper(IUserDao.class);
}

@After//用于在测试方法执行之后执行
public void destroy()throws Exception{
//提交事务
// sqlSession.commit();
//6.释放资源
sqlSession.close();
in.close();
}

/**
* 测试一级缓存
*/
@Test
public void testFirstLevelCache(){
User user1 = userDao.findById(41);
System.out.println(user1);
User user2 = userDao.findById(41);
System.out.println(user2);
System.out.println(user1 == user2);
}

通过执行测试类,发现输出结果为True,同时输出的对象名也是相同的。并且sql只执行了一次。换句话说,第一次是查询,第二次直接从缓存中取。

- Opening JDBC Connection
- Created connection 243745864.
- Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@e874448]
- ==> Preparing: select * from user where id = ?
- ==> Parameters: 41(Integer)
- <== Total: 1

如果这个时候session关闭了,缓存也会消失。

通过sqlSession.close()方法关闭一级缓存

/**
* 测试一级缓存
*/
@Test
public void testFirstLevelCache(){
User user1 = userDao.findById(41);
System.out.println(user1);

sqlSession.close();

再次获取SqlSession对象
sqlSession = factory.openSession();
//重新获取这个对象
userDao = sqlSession.getMapper(IUserDao.class);
User user2 = userDao.findById(41);
System.out.println(user2);
System.out.println(user1 == user2);
}

通过使用sqlSession.clearCache()方法关闭一级缓存
修改测试类:

/**
* 测试一级缓存
*/
@Test
public void testFirstLevelCache(){
User user1 = userDao.findById(41);
System.out.println(user1);

//sqlSession.close();

//再次获取SqlSession对象
//sqlSession = factory.openSession();
sqlSession.clearCache();//此方法也可以清空缓存
//重新获取这个对象
userDao = sqlSession.getMapper(IUserDao.class);
User user2 = userDao.findById(41);
System.out.println(user2);
System.out.println(user1 == user2);
}

通过执行测试类,发现输出结果为False,同时输出的对象名也是不相同的。并且sql执行了两次。这就表示没有再用缓存,重新进行了查询。

那么问题来了,以上是手动关闭的,如果数据库的数据跟一级缓存不一致了,它是如何做到自动同步的呢?

触发清空一级缓存的情况
修改用户持久层 Dao 接口IUserDao,增加更新方法:

/**
* 更新用户信息
* @param user
*/
void updateUser(User user);
<?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="com.mybatis.dao.IUserDao">
<!-- 更新用户信息-->
<update id="updateUser" parameterType="user">
update user set username=#{username},address=#{address} where id=#{id}
</update>
</mapper>

编写测试类:

/**
* 测试缓存的同步
*/
@Test
public void testClearlCache(){
//1.根据id查询用户
User user1 = userDao.findById(41);
System.out.println(user1);

//2.更新用户信息
//user1.setUsername("update user clear cache");
//user1.setAddress("北京市海淀区");
//userDao.updateUser(user1);

//3.再次查询id为41的用户
User user2 = userDao.findById(41);
System.out.println(user2);

System.out.println(user1 == user2);
}

我们先把更新用户信息这块代码注释掉,执行发现打印的是True,sql已经也执行一次,说明存在一级缓存。

修改测试类,增加更新对象操作:

/**
* 测试缓存的同步
*/
@Test
public void testClearlCache(){
//1.根据id查询用户
User user1 = userDao.findById(41);
System.out.println(user1);

//2.更新用户信息
user1.setUsername("update user clear cache");
user1.setAddress("北京市海淀区");
userDao.updateUser(user1);

//3.再次查询id为41的用户
User user2 = userDao.findById(41);
System.out.println(user2);

System.out.println(user1 == user2);
}

发现输出结果为false,查询语句也执行了两次,第二次查询没有从缓存里面取,而是发起了一次新查询,这是为什么呢?

一级缓存的分析
我们可以发现,虽然在上面的代码中我们查询了两次,但最后只执行了一次数据库操作,这就是 Mybatis 提供给我们的一级缓存在起作用了。因为一级缓存的存在,导致第二次查询 id 为 41 的记录时,并没有发出 sql 语句从数据库中查询数据,而是从一级缓存中查询。

一级缓存是 SqlSession 范围的缓存,当调用 SqlSession 的修改,添加,删除,commit()close(),clearCache()等方法时,就会清空一级缓存

第一次发起查询用户 id 为 41 的用户信息,先去找缓存中是否有 id 为 41 的用户信息,如果没有,从数据库查询用户信息。
得到用户信息,将用户信息存储到一级缓存中。
如果 sqlSession 去执行 commit 操作(执行插入、更新、删除),清空 SqlSession 中的一级缓存,这样做的目的为了让缓存中存储的是最新的信息,避免脏读。
第二次发起查询用户 id 为 41 的用户信息,先去找缓存中是否有 id 为 41 的用户信息,缓存中有,直接从缓存中获取用户信息。

所以,我们刚刚调用了Update()方法,导致一级缓存被清空,所以此时会再次发起查询。也就是不再会从缓存中获取。

13.2 Mybatis 二级缓存

二级缓存:它指的是Mybatis中SqlSessionFactory对象的缓存。由同一个SqlSessionFactory对象创建的SqlSession共享其缓存。

创建测试类:

private InputStream in;
private SqlSessionFactory factory;

@Before//用于在测试方法执行之前执行
public void init()throws Exception{
//1.读取配置文件,生成字节输入流
in = Resources.getResourceAsStream("SqlMapConfig.xml");
//2.获取SqlSessionFactory
factory = new SqlSessionFactoryBuilder().build(in);

}

@After//用于在测试方法执行之后执行
public void destroy()throws Exception{
in.close();
}

/**
* 测试一级缓存
*/
@Test
public void testFirstLevelCache(){
SqlSession sqlSession1 = factory.openSession();
IUserDao dao1 = sqlSession1.getMapper(IUserDao.class);
User user1 = dao1.findById(41);
System.out.println(user1);
sqlSession1.close();//一级缓存消失

SqlSession sqlSession2 = factory.openSession();
IUserDao dao2 = sqlSession2.getMapper(IUserDao.class);
User user2 = dao2.findById(41);
System.out.println(user2);
sqlSession2.close();

System.out.println(user1 == user2);
}

执行测试类,发现输出结果为False,并且执行了两次,明明是由同一个SqlSessionFactory对象创建的SqlSession共享其缓存,应该输出True才对,此时并没有二级缓存概念。这是为什么?

二级缓存的使用步骤:
   第一步:让Mybatis框架支持二级缓存(在SqlMapConfig.xml中配置)
   第二步:让当前的映射文件支持二级缓存(在IUserDao.xml中配置)
   第三步:让当前的操作支持二级缓存(在select标签中配置)

修改SqlMapConfig.xml,修改cacheEnabled为True,默认为True

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<!--第一步-->
<setting name="cacheEnabled" value="true"/>
</settings>
</configuration>

修改IUserDao.xml映射文件,开启user支持二级缓存

<?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="com.mybatis.dao.IUserDao">
<!--开启user支持二级缓存-->
<!--第二步-->
<cache/>

<!-- 根据id查询用户 -->
<!--第三步,useCache="true"-->
<select id="findById" parameterType="INT" resultType="user" useCache="true">
select * from user where id = #{uid}
</select>
</mapper>

再次执行测试类,发现只执行了一次,说明二级缓存生效了,但是输出结果为False,说明两个对象不一致。是因为二级缓存存放的是数据而不是对象

查看上图,发现二级环境存放是类似json格式的数据,谁需要就会把数据拿过来,创建一个新的对象,并把数据封装进去。虽然没有发起新的查询,但是却创建了一个新的对象。

第14章 Mybatis 注解开发

这几年来注解开发越来越流行,Mybatis 也可以使用注解开发方式,这样我们就可以减少编写 Mapper 映射文件了。本次我们先围绕一些基本的 CRUD 来学习,再学习复杂映射关系及延迟加载。
本章节注解开发只涉及Mapper映射问题,并不涉及SqlMapConfig.xml

14.1 环境搭建

创建一个Maven工程,修改pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.mybatis</groupId>
<artifactId>annotation_mybatis</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>

<dependencies>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.5</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.6</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.12</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.10</version>
</dependency>
</dependencies>
</project>

创建包com.mybatis.domain,在domain创建User实体对象:

public class User implements Serializable{

private Integer id;
private String username;
private String address;
private String sex;
private Date birthday;
//省略get、set方法
}

创建包com.mybatis.dao,在dao下创建数据库Dao层接口文件IUserDao

/**
* 在mybatis中针对,CRUD一共有四个注解
* @Select @Insert @Update @Delete
*/
public interface IUserDao {

/**
* 查询所有用户
* @return
*/
//注解只有一个value属性需要赋值时候,可以省略value属性 @Select(value="select * from user")
@Select("select * from user")
List<User> findAll();
}

resources目录下创建包com.mybatis.dao

resources目录下创建jdbcConfig.properties

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/eesy_mybatis
jdbc.username=root
jdbc.password=1234

resources目录下创建log4j.properties

# Set root category priority to INFO and its only appender to CONSOLE.
#log4j.rootCategory=INFO, CONSOLE debug info warn error fatal
log4j.rootCategory=debug, CONSOLE, LOGFILE

# Set the enterprise logger category to FATAL and its only appender to CONSOLE.
log4j.logger.org.apache.axis.enterprise=FATAL, CONSOLE

# CONSOLE is set to be a ConsoleAppender using a PatternLayout.
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
log4j.appender.CONSOLE.layout.ConversionPattern=%d{ISO8601} %-6r [%15.15t] %-5p %30.30c %x - %m\n

# LOGFILE is set to be a File appender using a PatternLayout.
log4j.appender.LOGFILE=org.apache.log4j.FileAppender
log4j.appender.LOGFILE.File=d:\axis.log
log4j.appender.LOGFILE.Append=true
log4j.appender.LOGFILE.layout=org.apache.log4j.PatternLayout
log4j.appender.LOGFILE.layout.ConversionPattern=%d{ISO8601} %-6r [%15.15t] %-5p %30.30c %x - %m\n

resources目录下创建SqlMapConfig.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 引入外部配置文件-->
<properties resource="jdbcConfig.properties"></properties>
<!--配置别名-->
<typeAliases>
<package name="com.mybatis.domain"></package>
</typeAliases>
<!-- 配置环境-->
<environments default="mysql">
<environment id="mysql">
<transactionManager type="JDBC"></transactionManager>
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"></property>
<property name="url" value="${jdbc.url}"></property>
<property name="username" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>
</dataSource>
</environment>
</environments>
<!-- 指定带有注解的dao接口所在位置 -->
<mappers>
<mapper class="com.mybatis.dao.IUserDao"></mapper>
</mappers>
</configuration>

环境到此搭建完成。

创建测试类:


/**
* 测试基于注解的mybatis使用
* @param args
*/
public static void main(String[] args) throws Exception{
//1.获取字节输入流
InputStream in = Resources.getResourceAsStream("SqlMapConfig.xml");
//2.根据字节输入流构建SqlSessionFactory
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in);
//3.根据SqlSessionFactory生产一个SqlSession
SqlSession session = factory.openSession();
//4.使用SqlSession获取Dao的代理对象
IUserDao userDao = session.getMapper(IUserDao.class);
//5.执行Dao的方法
//查询所有
List<User> users = userDao.findAll();
for(User user : users){
System.out.println(user);
}
//6.释放资源
session.close();
in.close();
}
}

注:我们在使用注解开发的时候,就不能再同一Dao下(目录)出现xml映射文件。

14.2 Mybatis 的常用注解说明

@Insert:实现新增
@Update:实现更新
@Delete:实现删除
@Select:实现查询
@Result:实现结果集封装
@Results:可以与@Result 一起使用,封装多个结果集
@ResultMap:实现引用@Results 定义的封装
@One:实现一对一结果集封装
@Many:实现一对多结果集封装
@SelectProvider: 实现动态 SQL 映射
@CacheNamespace:实现注解二级缓存的使用

14.3 使用注解实现基本 CRUD

@Insert
修改IUserDao,增加保存方法

/**
* 保存用户
* @param user
*/
@Insert("insert into user(username,address,sex,birthday)values(#{username},#{address},#{sex},#{birthday})")
void saveUser(User user);

测试类:

public class AnnotationCRUDTest {
private InputStream in;
private SqlSessionFactory factory;
private SqlSession session;
private IUserDao userDao;

@Before
public void init()throws Exception{
in = Resources.getResourceAsStream("SqlMapConfig.xml");
factory = new SqlSessionFactoryBuilder().build(in);
session = factory.openSession();
userDao = session.getMapper(IUserDao.class);
}

@After
public void destroy()throws Exception{
session.commit();
session.close();
in.close();
}


@Test
public void testSave(){
User user = new User();
user.setUsername("mybatis annotation");
user.setAddress("北京市昌平区");

userDao.saveUser(user);
}
}

@Update
修改IUserDao,增加更新方法

/**
* 更新用户
* @param user
*/
@Update("update user set username=#{username},sex=#{sex},birthday=#{birthday},address=#{address} where id=#{id}")
void updateUser(User user);

测试类:

public class AnnotationCRUDTest {
private InputStream in;
private SqlSessionFactory factory;
private SqlSession session;
private IUserDao userDao;

@Before
public void init()throws Exception{
in = Resources.getResourceAsStream("SqlMapConfig.xml");
factory = new SqlSessionFactoryBuilder().build(in);
session = factory.openSession();
userDao = session.getMapper(IUserDao.class);
}

@After
public void destroy()throws Exception{
session.commit();
session.close();
in.close();
}


@Test
public void testUpdate(){
User user = new User();
user.setId(57);
user.setUsername("mybatis annotation update");
user.setAddress("北京市海淀区");
user.setSex("男");
user.setBirthday(new Date());

userDao.updateUser(user);
}
}

@Delete
修改IUserDao,增加删除方法

/**
* 删除用户
* @param userId
*/
@Delete("delete from user where id=#{id} ")
void deleteUser(Integer userId);

测试类:

public class AnnotationCRUDTest {
private InputStream in;
private SqlSessionFactory factory;
private SqlSession session;
private IUserDao userDao;

@Before
public void init()throws Exception{
in = Resources.getResourceAsStream("SqlMapConfig.xml");
factory = new SqlSessionFactoryBuilder().build(in);
session = factory.openSession();
userDao = session.getMapper(IUserDao.class);
}

@After
public void destroy()throws Exception{
session.commit();
session.close();
in.close();
}


@Test
public void testDelete(){
userDao.deleteUser(51);
}
}

@Select
修改IUserDao,增加查询方法

   /**
* 根据用户名称模糊查询
* @param username
* @return
*/
//@Select("select * from user where username like #{username} ") //这里是没有百分号%%的,测试在传参时候需要传入 %占位符
@Select("select * from user where username like '%${value}%' ") //{}符号里面必须是固定值 value,这里不需要传入百分号,直接传参数
List<User> findUserByName(String username);

测试类:

public class AnnotationCRUDTest {
private InputStream in;
private SqlSessionFactory factory;
private SqlSession session;
private IUserDao userDao;

@Before
public void init()throws Exception{
in = Resources.getResourceAsStream("SqlMapConfig.xml");
factory = new SqlSessionFactoryBuilder().build(in);
session = factory.openSession();
userDao = session.getMapper(IUserDao.class);
}

@After
public void destroy()throws Exception{
session.commit();
session.close();
in.close();
}

//模糊查询两种方式
@Test
public void testFindByName(){
//List<User> users = userDao.findUserByName("%mybatis%");
List<User> users = userDao.findUserByName("mybatis");
for(User user : users){
System.out.println(user);
}
}
}

修改IUserDao,增加查询方法

/**
* 查询总用户数量
* @return
*/
@Select("select count(*) from user ")
int findTotalUser();

测试类:

public class AnnotationCRUDTest {
private InputStream in;
private SqlSessionFactory factory;
private SqlSession session;
private IUserDao userDao;

@Before
public void init()throws Exception{
in = Resources.getResourceAsStream("SqlMapConfig.xml");
factory = new SqlSessionFactoryBuilder().build(in);
session = factory.openSession();
userDao = session.getMapper(IUserDao.class);
}

@After
public void destroy()throws Exception{
session.commit();
session.close();
in.close();
}

@Test
public void testFindTotal(){
int total = userDao.findTotalUser();
System.out.println(total);
}
}

14.4 使用注解实现复杂关系映射开发

Mybatis是支持对象属性名可以和数据库不一致的,那么xml我们设置过,那么注解该怎么实现两者的对应关系呢?

实现复杂关系映射之前我们可以在映射文件中通过配置<resultMap>来实现,在使用注解开发时我们需要借助@Results 注解,@Result 注解,@One 注解,@Many 注解。

1.注解建立实体类属性和数据库表中列的对应关系

为了模拟实体类与列名不一致,我们修改实体User

public class User implements Serializable{

private Integer userId;
private String userName;
private String userAddress;
private String userSex;
private Date userBirthday;
//省略get/set方法

这时候查询的数据是无法正常封装的,只有UserName会封装成功,因为windows下Mysql不分大小写。有人会说,使用别名可以解决这个问题,但是如果查询的方法很多,我们就需要每个都改一遍,显然不太合适。MyBatis就为我们提供了一个注解。

@Results 注解:代替的是标签<resultMap>该注解中可以使用单个@Result 注解,也可以使用@Result 集合。@Results({@Result(),@Result()})或@Results(@Result())

@Resutl 注解:代替了 <id>标签和 <result> 标签
@Result 中属性介绍:
   id 是否是主键字段,True或False。默认为False
   column 数据库的列名
   property 需要装配的属性名
   one 需要使用的@One 注解(@Result([email protected])()))
   many 需要使用的@Many 注解(@Result([email protected])()))

修改数据库Dao接口IUserDao

public interface IUserDao {

/**
* 查询所有用户
* @return
*/
@Select("select * from user")
@Results(value={
@Result(id=true,column = "id",property = "userId"),
@Result(column = "username",property = "userName"),
@Result(column = "address",property = "userAddress"),
@Result(column = "sex",property = "userSex"),
@Result(column = "birthday",property = "userBirthday"),
})
List<User> findAll();

/**
* 根据id查询用户
* @param userId
* @return
*/
@Select("select * from user where id=#{id} ")
User findById(Integer userId);

这时候我们在执行findAll()方法,发现可以正常封装了。我们在执行findById()方法,发现怎么还是和第一次一样,无法正常封装数据。难道我们每条Sql都要配置对应关系吗?

当然不是,Mybatis在@Results中提供了一个id属性。这个id属性就是给定义(数据库与实体关系)提供一个标识,方便其它Sql引用。在需要引用的sql上加@ResultMap注解,注解value值为定义的id值。可以有多个value。也就是可以定义一个字段与数据库对应关系,也可以定义多个字段与数据库对应关系。

修改数据库Dao接口IUserDao

public interface IUserDao {

/**
* 查询所有用户
* @return
*/
@Select("select * from user")
@Results(id="userMap",value={
@Result(id=true,column = "id",property = "userId"),
@Result(column = "username",property = "userName"),
@Result(column = "address",property = "userAddress"),
@Result(column = "sex",property = "userSex"),
@Result(column = "birthday",property = "userBirthday"),
})
List<User> findAll();

/**
* 根据id查询用户
* @param userId
* @return
*/
@Select("select * from user where id=#{id} ")
//标准写法
//@ResultMap(value={"userMap"})
@ResultMap("userMap") //只有一个value值时,可以省略。如果数组中只有一个元素,大括号也是可以省略的。
User findById(Integer userId);


/**
* 根据用户名称模糊查询
* @param username
* @return
*/
@Select("select * from user where username like #{username} ")
@ResultMap("userMap")
List<User> findUserByName(String username);

同理,模糊查询也可以直接使用@ResultMap("userMap")注解。

测试类:

public class AnnotationCRUDTest {
private InputStream in;
private SqlSessionFactory factory;
private SqlSession session;
private IUserDao userDao;

@Before
public void init()throws Exception{
in = Resources.getResourceAsStream("SqlMapConfig.xml");
factory = new SqlSessionFactoryBuilder().build(in);
session = factory.openSession();
userDao = session.getMapper(IUserDao.class);
}

@After
public void destroy()throws Exception{
session.commit();
session.close();
in.close();
}

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

//根据id查询用户
@Test
public void testFindOne(){
User user = userDao.findById(57);
System.out.println(user);
}

//根据用户名称模糊查询
@Test
public void testFindByName(){
List<User> users = userDao.findUserByName("%mybatis%");
for(User user : users){
System.out.println(user);
}
}
}

2.一对一查询注解配置

@One 注解(一对一)
代替了<assocation>标签,是多表查询的关键,在注解中用来指定子查询返回单一对象。
@One 注解属性介绍:
   select 指定用来多表查询的 sqlMapper
   fetchType 会覆盖全局的配置参数 lazyLoadingEnabled。。
使用格式:
   @Result(column=" ",property="",[email protected](select=""))

需求:加载账户信息时并且加载该账户的用户信息,根据情况可实现延迟加载。(注解方式实现)我们还是用账户和用户为例,用户实体我们已经创建完成,只需要创建对应账户实体类。

public class Account implements Serializable {

private Integer id;
private Integer uid;
private Double money;

//多对一(mybatis中称之为一对一)的映射:一个账户只能属于一个用户
private User user;
//省略get、Set方法 已经toString
}

创建数据库Dao接口,IAccountDao,并添加方法

public interface IAccountDao {

/**
* 查询所有账户,并且获取每个账户所属的用户信息
* @return
*/
@Select("select * from account")
@Results(id="accountMap",value = {
@Result(id=true,column = "id",property = "id"),
@Result(column = "uid",property = "uid"),
@Result(column = "money",property = "money"),
//配置一对一关系
@Result(property = "user",column = "uid",one=@One(select="com.mybatis.dao.IUserDao.findById",fetchType= FetchType.EAGER))
//fetchType:LAZY(延迟加载),EAGER(立即加载),DEFAULT(默认)。一对一选择立即加载。一对多延迟加载。
})
List<Account> findAll();
}

测试类:

public class AccountTest {
private InputStream in;
private SqlSessionFactory factory;
private SqlSession session;
private IAccountDao accountDao;

@Before
public void init()throws Exception{
in = Resources.getResourceAsStream("SqlMapConfig.xml");
factory = new SqlSessionFactoryBuilder().build(in);
session = factory.openSession();
accountDao = session.getMapper(IAccountDao.class);
}

@After
public void destroy()throws Exception{
session.commit();
session.close();
in.close();
}

@Test
public void testFindAll(){
List<Account> accounts = accountDao.findAll();
for(Account account : accounts){
System.out.println("----每个账户的信息-----");
System.out.println(account);
System.out.println(account.getUser());
}
}
}

3.一对多查询注解配置

@Many 注解(多对一)
代替了<Collection>标签,是是多表查询的关键,在注解中用来指定子查询返回对象集合。
注意:聚集元素用来处理“一对多”的关系。需要指定映射的 Java 实体类的属性,属性的 javaType(一般为 ArrayList)但是注解中可以不定义;
使用格式:@Result(property="",column="",[email protected](select=""))

需求:查询用户信息时,也要查询他的账户列表。使用注解方式实现。
分析:一个用户具有多个账户信息,所以形成了用户(User)与账户(Account)之间的一对多关系。

修改User实体对象:

public class User implements Serializable{

private Integer userId;
private String userName;
private String userAddress;
private String userSex;
private Date userBirthday;

//一对多关系映射:一个用户对应多个账户
private List<Account> accounts;
//省略get、set方法

IAccountDao,新增一个方法。供一对多查询

/**
* 根据用户id查询账户信息
* @param userId
* @return
*/
@Select("select * from account where uid = #{userId}")
List<Account> findAccountByUid(Integer userId);

修改IUserDao,增加一对多注解配置

public interface IUserDao {

/**
* 查询所有用户
* @return
*/
@Select("select * from user")
@Results(id="userMap",value={
@Result(id=true,column = "id",property = "userId"),
@Result(column = "username",property = "userName"),
@Result(column = "address",property = "userAddress"),
@Result(column = "sex",property = "userSex"),
@Result(column = "birthday",property = "userBirthday"),
@Result(property = "accounts",column = "id",
many = @Many(select = "com.mybatis.dao.IAccountDao.findAccountByUid",
fetchType = FetchType.LAZY)) //延迟加载
})
List<User> findAll();

/**
* 根据id查询用户
* @param userId
* @return
*/
@Select("select * from user where id=#{id} ")
@ResultMap("userMap")
User findById(Integer userId);
}
public class AnnotationCRUDTest {
private InputStream in;
private SqlSessionFactory factory;
private SqlSession session;
private IUserDao userDao;

@Before
public void init()throws Exception{
in = Resources.getResourceAsStream("SqlMapConfig.xml");
factory = new SqlSessionFactoryBuilder().build(in);
session = factory.openSession();
userDao = session.getMapper(IUserDao.class);
}

@After
public void destroy()throws Exception{
session.commit();
session.close();
in.close();
}

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

14.5 Mybatis 基于注解的二级缓存

关于一级缓存,我们用IUserDao下的findById()方法演示

@Test
public void testFindOne(){
User user = userDao.findById(57);
System.out.println(user);

User user2 = userDao.findById(57);
System.out.println(user2);
System.out.println(user==user2);
}

执行两次,发现两个对象是一致的,且只执行了一次,所以一级缓存我们无需过多关心。

SqlMapConfig开启二级缓存支持

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!--配置开启二级缓存-->
<settings>
<setting name="cacheEnabled" value="true"/><!--可以不配置,默认也是开启的-->
</settings>
</configuration>

在持久层接口中使用注解配置二级缓存:@CacheNamespace(blocking = true)

@CacheNamespace(blocking = true) //默认为false
public interface IUserDao {

/**
* 查询所有用户
* @return
*/
@Select("select * from user")
@Results(id="userMap",value={
@Result(id=true,column = "id",property = "userId"),
@Result(column = "username",property = "userName"),
@Result(column = "address",property = "userAddress"),
@Result(column = "sex",property = "userSex"),
@Result(column = "birthday",property = "userBirthday"),
@Result(property = "accounts",column = "id",
many = @Many(select = "com.itheima.dao.IAccountDao.findAccountByUid",
fetchType = FetchType.LAZY))
})
List<User> findAll();

/**
* 根据id查询用户
* @param userId
* @return
*/
@Select("select * from user where id=#{id} ")
@ResultMap("userMap")
User findById(Integer userId);

/**
* 根据用户名称模糊查询
* @param username
* @return
*/
@Select("select * from user where username like #{username} ")
@ResultMap("userMap")
List<User> findUserByName(String username);
}

运行测试类:

public class SecondLevelCatchTest {

private InputStream in;
private SqlSessionFactory factory;


@Before
public void init()throws Exception{
in = Resources.getResourceAsStream("SqlMapConfig.xml");
factory = new SqlSessionFactoryBuilder().build(in);

}

@After
public void destroy()throws Exception{

in.close();
}

@Test
public void testFindOne(){
SqlSession session = factory.openSession();
IUserDao userDao = session.getMapper(IUserDao.class);
User user = userDao.findById(57);
System.out.println(user);

session.close();//释放一级缓存

SqlSession session1 = factory.openSession();//再次打开session
IUserDao userDao1 = session1.getMapper(IUserDao.class);
User user1 = userDao1.findById(57);
System.out.println(user1);


session1.close();

}
}

执行测试类,发现虽然打印了两次,但是只执行了一次,说明开始使用了二级缓存。