zl程序教程

您现在的位置是:首页 >  数据库

当前栏目

数据库访问框架 - Mybatis

2023-06-13 09:17:49 时间

Mybatis简介

MyBatis本是apache的一个开源项目iBatis,2010年这个项目由apache software foundation迁移到了google code,并且改名为Mybatis。2013年11月迁移到Github。 iBatis一词源于”internet”和”abatis”的组合,是一个基于Java的持久层框架。iBatis提供的持久层框架包括SQL Maps和Data Access Objects(DAOS)。 mybatis 内部封装了 jdbc,使开发者只需要关注 sql 语句本身,而不需要花费精力去处理加载驱动、创建连接、创建 statement 等繁杂的过程。 mybatis 通过 xml 或注解的方式将要执行的各种 statement 配置起来,并通过 java 对象和 statement 中 sql 的动态参数进行映射生成最终执行的 sql 语句,最后由 mybatis 框架执行 sql 并将结果映射为 java 对象并返回。 采用 ORM 思想解决了实体和数据库映射的问题,对 jdbc 进行了封装,屏蔽了 jdbc api 底层访问细节,使我们不用与 jdbc api 打交道,就可以完成对数据库的持久化操作。

Mybatis快速入门

基于dao的代理实现(默认方式)

1.创建mavan工程

2.添加Mybatis坐标 在项目的pom.xml文件中添加Mybatis3.4.5坐标:

<dependencies>
    <!-- mybatis -->
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>3.4.5</version>
    </dependency>
    <!-- mybatis依赖mysql驱动,必选 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.6</version> <scope>runtime</scope>
    </dependency>
    <!-- 单元测试依赖,可选 -->
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.10</version>
        <scope>test</scope>
    </dependency>
    <!-- 日志依赖,可选 -->
    <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
        <version>1.2.12</version>
    </dependency>
</dependencies>

3.添加实体类Userdomain包下创建实体类User

public class User implements Serializable {
	private Integer id;
	private String username;
	private Date birthday;
	private String sex;
	private String address;
    
	public Integer getId() {
		return id; 
    }
	public void setId(Integer id) {
		this.id = id; 
    }
	public String getUsername() {
		return username; }
	public void setUsername(String username) {
		this.username = username; 
    }
	public Date getBirthday() {
		return birthday;
    }
	public void setBirthday(Date birthday) {
		this.birthday = birthday; 
    }
	public String getSex() {
		return sex; 
    }
	public void setSex(String sex) {
		this.sex = sex; 
    }
	public String getAddress() {
		return address; 
    }
	public void setAddress(String address) {
  this.address = address;
    }
	@Override
	public String toString() {
		return "User [id=" + id + ", username=" + username + ", birthday=" + birthday
		+ ", sex=" + sex + ", address="
		+ address + "]"; 
    } 
}

4.编写持久层接口IUserDaodao包下创建实体类IUserDao

//持久层接口(也可以写成 UserDao 或者 UserMapper)
public interface IUserDao {
	/**
	* 查询所有用户
	* @return
	*/
	List<User> findAll();
}

5.编写持久层接口的映射文件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.itheima.dao.IUserDao">
	<!-- 配置查询所有操作 --> 
	<select id="findAll" resultType="com.itheima.domain.User">
		select * from user
	</select>
</mapper>

注意:

  1. 创建位置必须和持久层接口在相同的包中;
  2. 名称必须以持久层接口名称命名文件名,扩展名是.xml。

6.编写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>
	<!-- 配置 mybatis 的环境 -->
    <environments default="mysql">
		<!-- 配置 mysql 的环境 -->
        <environment id="mysql">
			<!-- 配置事务的类型 -->
            <transactionManager type="JDBC"></transactionManager>
			<!-- 配置连接数据库的信息:用的是数据源(连接池) -->
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/ee50"/>
                <property name="username" value="root"/>
                <property name="password" value="1234"/>
            </dataSource>
        </environment>
    </environments>
    
	<!-- 告知 mybatis 映射配置的位置 -->
    <mappers>
        <mapper resource="com/itheima/dao/IUserDao.xml"/>
    </mappers>
</configuration>

7.编写测试类

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

当满足了如下3点规则,就无须再编写dao的实现类,mybatis自动创建实现类:

  1. mybatis映射配置文件(mapper.xml)位置必须和dao接口的包结构相同;
  2. 映射配置文件的mapper标签namespace属性的取值必须是dao接口的全限定类名;
  3. 映射配置文件的操作配置(select)id属性的取值必须是dao接口的方法名;

基于注解的mybatis使用

1.在持久层接口IUserDao中添加注解

public interface IUserDao {
	/**
	* 查询所有用户
	* @return
	*/
	@Select("select * from user")
	List<User> findAll();
}

2.不再需要持久层接口的映射文件IUserDao.xml,直接删除

3.修改SqlMapConfig.xml,xml路径改为类名

<!-- 告知 mybatis 映射配置的位置 --> 
<mappers>
    <mapper class="com.itheima.dao.IUserDao"/>
</mappers>

自己实现dao的实现类

1.创建持久层接口IUserDao的实现类impl.UserDaoImpl

public class UserDaoImpl implements IUserDao{
    private SqlSessionFactory factory;
    
    public UserDaoImpl(SqlSessionFactory factory){
        this.factory = factory;
    }
    
    public List<User> findAll(){
        //1.使用工厂创建SqlSession对象
        SqlSession session = factory.openSession();
        //2.使用session执行查询所有方法,这里一定要是包含完整包名的接口全名方法,才能找到最终要执行的sql语句
        List<User> users = session.selectList("com.itheima.dao.IUserDao.findAll");
        session.close();
        //3.返回查询结果
        return users;
    }
    
}

2.修改测试类的调用方法

public class MybatisTest {
	public static void main(String[] args)throws Exception {
		//1.读取配置文件
		InputStream in = Resources.getResourceAsStream("SqlMapConfig.xml");
		//2.创建 SqlSessionFactory 的构建者对象
		SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
		//3.使用构建者创建工厂对象 SqlSessionFactory
		SqlSessionFactory factory = builder.build(in);
		//4.使用工厂创建dao对象
		IUserDao userDao = new UserDaoImpl(factory);
		//5.使用dao对象执行查询所有方法
		List<User> users = userDao.findAll();
		for(User user : users) {
			System.out.println(user);
		}
		//6.释放资源
		in.close();
	} 
}

Mybatis基本操作

在上面的快速入门中介绍了最基本的查询操作,接下来介绍mybatis对数据库的增删查改操作

基于代理的增删查改CRUD

1.在持久层接口IUserDao中添加接口方法

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

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

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

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

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

2.在用户的映射配置文件SqlMapConfig.xml中增加配置

<!-- 根据id查询用户 -->
<!-- 内置类型可以用简称int,也可以用全限定名java.lang.Integer,但自定义类型必须用全限定名 -->
<select id="findById" parameterType="int" resultType="com.itheima.domain.User">
    select * from user where id = #{uid}
</select>

<!-- 保存用户 -->
<insert id="saveUser" parameterType="com.itheima.domain.User">
    <!-- 配置插入操作后,获取插入数据的id -->
    <!-- keyProperty实体类属性名 -->
    <!-- keyColumn数据库列名 -->
    <!-- order="AFTER"语句执行完成后再执行 -->
    <selectKey keyProperty="id" keyColumn="id" resultType="int" order="AFTER">
        select last_insert_id();
    </selectKey>
    insert into user(username,address,sex,birthday)values(#{username},#{address},#{sex},#{birthday});
</insert>

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

<!-- 删除用户-->
<delete id="deleteUser" parameterType="java.lang.Integer">
    delete from user where id = #{uid}
</delete>

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

  • resultType 属性:用于指定结果集的类型。
  • parameterType 属性:用于指定传入参数的类型。
  • sql语句中的#{}表示占位符,相当于原来jdbc的?,都是用于执行语句时替换实际的数据,具体的数据由#{}里面的内容决定的,如果数据类型是基本类型,里面可以填写任意名字,比如findById中的uid可以对应接口方法中的实际参数名userId。如果数据类型是复杂类型,语法格式就是使用 #{对象.对象}的方式,比如#{user.username}它会先去找 user 对象,然后在 user 对象中找到 username 属性,并调用getUsername()方法把值取出来,我们在 parameterType 属性上指定了实体类名称,所以可以省略user.,而直接写 username。
  • {}的区别: #{}表示一个占位符号,通过#{}可以实现 preparedStatement 向占位符中设置值,自动进行 java 类型和 jdbc 类型转换,#{}可以有效防止 sql 注入。 #{}可以接收简单类型值或 pojo 属性值。 如果 parameterType 传输单个简单类型值,#{}括号中可以是 value 或其它任意名称。 {}表示拼接 sql 串,通过{}可以将 parameterType 传入的内容拼接在 sql 中且不进行 jdbc 类型转换, {}可以接收简单类型值或 pojo 属性值,如果 parameterType 传输单个简单类型值,{}括号中只能是 value,或者在接口方法中声明参数名:List<User> findByName(@Param(value="name") String username);。

3.编写测试类

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 testFindOne(){
        //5.执行查询一个方法
        User user = userDao.findById(50);
        System.out.println(user);
    }
    
    @Test
    public void testFindByName(){
        //5.执行查询一个方法
        List<User> users = userDao.findByName("%王%");
        //List<User> users = userDao.findByName("王");
        for(User user : users){
            System.out.println(user);
        }
    }
}

4.参数中如果需要传递多个Pojo(Plain Ordinary Java Object)对象,可以用一个pojo类QueryVo包含多个pojo

public class QueryVo {
    private User user;
    public User getUser() {
        return user;
    }
    public void setUser(User user) {
        this.user = user;
    }
}

<!-- 根据用户名称模糊查询,参数变成一个 QueryVo 对象了 -->
<select id="findByVo" resultType="com.itheima.domain.User" parameterType="com.itheima.domain.QueryVo">
    select * from user where username like #{user.username};
</select>

配置文件的主要标签说明

SqlMapConfig.xml文件配置的内容和层级关系:

-properties(属性) —property -settings(全局配置参数) —setting -typeAliases(类型别名) —typeAliase —package -typeHandlers(类型处理器) -objectFactory(对象工厂) -plugins(插件) -environments(环境集合属性对象) —environment(环境子属性对象) —-transactionManager(事务管理) —-dataSource(数据源) -mappers(映射器) —mapper —package

返回值映射resultMap

sql语句中的查询返回列必须和resultType类型中的属性名对应,否则会赋值为null,如果仅大小写不同,仍可以赋值成功,因为mysql 在 windows 系统中不区分大小写! 如果要匹配,可以在sql语句中设置别名强制对应上,此方法要可能要修改大量的sql语句,工作量较大,可以考虑另一种方法,对返回值做映射: 1.在IUserDao.xml中定义resultMap

<!-- 配置 查询结果的列名和实体类的属性名的对应关系 -->
<resultMap id="userMap" type="com.itheima.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>

2.在需要映射的地方将返回类型由resultType改为resultMap

<!-- 根据id查询用户 -->
<select id="findById" parameterType="int" resultMap="userMap">
    select * from user where id = #{uid}
</select>

属性标签配置properties

可将一些配置信息抽离出来保存在properties标签中,实现复用,修改SqlMapConfig.xml

    <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>
...
    <!--配置连接池-->
    <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>

也可将properties抽离出来,放在单独的文件,在 classpath 下定义jdbcConfig.properties 文件

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

SqlMapConfig.xmlproperties标签设置url或者resources属性,用于引用jdbcConfig.properties 文件,两者区别如下:

resource属性:常用的用于指定配置文件的位置,是按照类路径的写法来写,并且必须存在于类路径下。

<properties resource="jdbcConfig.properties"></properties>

url属性:要求按照Url的写法来写地址。

<properties url="file:///D:/xxx/xxx/jdbcConfig.properties"></properties>

类名别名标签typeAliases

<!--使用typeAliases配置别名,它只能配置domain中类的别名 -->
<typeAliases>
    <!--typeAlias用于配置别名。type属性指定的是实体类全限定类名。alias属性指定别名,当指定了别名就再区分大小写 -->
    <!-- 单个别名定义 -->
    <-- <typeAlias type="com.itheima.domain.User" alias="user"></typeAlias> -->
 
    <!-- 用于指定要配置别名的包,当指定之后,该包下的实体类都会注册别名,并且类名就是别名,不再区分大小写-->
    <!-- 批量别名定义,扫描整个包下的类,别名为类名(首字母大写或小写都可以) -->
    <package name="com.itheima.domain"></package>
</typeAliases>

映射器标签mappers

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

配置连接池

在 Mybatis 的SqlMapConfig.xml配置文件中,通过<dataSource type="pooled">来实现 Mybatis 中连接池的配置。 Mybatis 将它自己的数据源分为三类:

  • POOLED:采用传统的javax.sql.DataSource规范中的连接池,mybatis中有针对规范的实现
  • UNPOOLED:采用传统的获取连接的方式,虽然也实现Javax.sql.DataSource接口,但是并没有使用池的思想
  • JNDI:采用服务器提供的JNDI技术实现,来获取DataSource对象,不同的服务器所能拿到DataSource是不一样的

MyBatis内部定义了实现了java.sql.DataSource接口的UnpooledDataSource类和PooledDataSource类分别表示UNPOOLED、POOLED类型的数据源。

Mysql的数据库事务

数据库事务(transaction)是访问并可能操作各种数据项的一个数据库操作序列,这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。

事务的四大特性(ACID)

  • 原子性(Atomicity):事务开始后所有操作,要么全部做完,要么全部不做,不可能停滞在中间环节。事务执行过程中出错,会回滚到事务开始前的状态,所有的操作就像没有发生一样。也就是说事务是一个不可分割的整体,就像化学中学过的原子,是物质构成的基本单位。
  • 一致性(Consistency):事务开始前和结束后,数据库的完整性约束没有被破坏 。比如A向B转账,不可能A扣了钱,B却没收到。
  • 隔离性(Isolation):同一时间,只允许一个事务请求同一数据,不同的事务之间彼此没有任何干扰。比如A正在从一张银行卡中取钱,在A取钱的过程结束前,B不能向这张卡转账。
  • 持久性(Durability):事务完成后,事务对数据库的所有更新将被保存到数据库,不能回滚。

事务并发带来的三个问题

  • 脏读:事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据。
  • 不可重复读:事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果不一致。
  • 幻读:系统管理员A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是系统管理员B就在这个时候插入了一条具体分数的记录,当系统管理员A改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。

小结:不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行(Repeatable read),解决幻读需要锁表。

MySQL定义的四种事务隔离级别

事务隔离级别

脏读

不可重复读

幻读

解释

读未提交(read-uncommitted)

可以读到未提交的事物

不可重复读(read-committed)

只能读提交的事物

可重复读(repeatable-read)

事务提交前后都能读【MySql默认】

串行化(serializable)

serializable时会锁表,是最安全的,也是日常开发基本不会用

Mysql事务设置命令

  • 读未提交 set session transaction isolation level read uncommitted;
  • 不可重复读(读已提交的) set session transaction isolation level read committed;
  • 可重复读 set session transaction isolation level repeatable read;
  • 串行化 set session transaction isolation level serializable;

Mybatis中使用事务

Mybatis是基于jdbc的封装,在 JDBC 中可通过setAutoCommit()将事务提交设置为自动或手动的方式。

setAutoCommit官方定义: 将此连接的自动提交模式设置为给定状态。如果连接处于自动提交模式下,则它的所有SQL语句将被执行并作为单个事务提交。否则,它的SQL语句将聚集到事务中,直到调用commit 方法或 rollback 方法为止。默认情况下,新连接处于自动提交模式。

在mybatis的session = factory.openSession();方法中,默认参数是false,即手动提交事务的方式,在执行完sql语句后,需手动执行session.commitsession.rollback,这也是常用的方式,因为我们可以根据业务情况来决定是否进行提交。 如果设置`session = factory.openSession(true);,则mybatis会设置jdbc为自动提交事务的方法,无须显示执行session.commit

Mybatis动态Sql语句

代码片段

可将重复的sql语句抽离出来,使用时用include引用即可,达到 sql 重用的目的。

<!-- 抽取重复的语句代码片段 --> 
<sql id="defaultSql">
    select * from user
</sql>

<!-- 配置查询所有操作 --> 
<select id="findAll" resultType="user"> 
    <include refid="defaultSql"></include>
</select>

<!-- 根据 id 查询 --> 
<select id="findById" resultType="user" parameterType="int">
    <include refid="defaultSql"></include>
    where id = #{uid}
</select>

动态语句<if>

可以根据条件来判断是否需要拼接sql语句,标签中的 test 属性中写的是对象的属性名,如果是包装类的对象要使用 OGNL 表达式的写法。 为保证拼接顺利,主语句上要加上where 1=1

<select id="findByUser" resultType="user" parameterType="user">
    select * from user where 1=1
    <if test="username!=null and username != '' ">
        and username like #{username}
    </if> 
    <if test="address != null">
        and address like #{address}
    </if>
</select>

动态语句<where>

使用<where>标签可以简化where 1=1的条件封装,同时可以自动处理条件掉表达式开头的and或者or字符,使生成正确的sql语句。

<!-- 根据用户信息查询 --> 
<select id="findByUser" resultType="user" parameterType="user"> 
    <include refid="defaultSql"></include> 
    <where> 
        <if test="username!=null and username != '' ">
            and username like #{username}
        </if> 
        <if test="address != null">
            and address like #{address}
        </if>
    </where>
</select>

动态语句<foreach>

在拼接sql语句时,有时需要传递一个集合,比如这样的sql语句select 字段 from user where id in (?),可以写这样的语句:

<!-- 根据queryvo中的Id集合实现查询用户列表 -->
<select id="findUserInIds" resultMap="userMap" parameterType="queryvo">
    <include refid="defaultUser"></include>
    <where>
        <if test="ids != null and ids.size()>0">
            <foreach collection="ids" open="and id in (" close=")" item="uid" separator=",">
                #{uid}
            </foreach>
        </if>
    </where>
</select>

<foreach>标签用于遍历集合,它的属性含义为:

  • collection:代表要遍历的集合元素,注意编写时不要写#{}
  • open:代表语句的开始部分
  • close:代表结束部分
  • item:代表遍历集合的每个元素,生成的变量名
  • sperator:代表分隔符

测试方法,构造一个ids的集合:

/**
 * 测试foreach标签的使用
 */
@Test
public void testFindInIds(){
    QueryVo vo = new QueryVo();
    List<Integer> list = new ArrayList<Integer>();
    list.add(41);
    list.add(42);
    list.add(46);
    vo.setIds(list);

    List<User> users = userDao.findUserInIds(vo);
    for(User user : users){
        System.out.println(user);
    }
}

多表查询

一对一的association标签

如果2个表关联查询,比如Account表和User表,一个Account只能对应一个User,Account对User是1对1的关系,可以采用继承的方式让Account继承User的全部字段,sql语句返回的数据列分别和各字段绑定。另一个更好的办法是采用组合的方式,为Account定义一个user的属性,使之将和user有关的数据封装起来。 Mybatis中可以使用association标签定义,IAccountDao.xml的定义:

<!-- 定义封装account和user的resultMap -->
<resultMap id="accountUserMap" type="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>
        <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>

Account实体类:

public class Account implements Serializable {
	...

    //从表实体应该包含一个主表实体的对象引用
    private User user;

    public User getUser() {
        return user;
    }

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

一对多的collection标签

一个User可以对应多个Account,User对Account是1对多的关系。 在Mybatis中可以使用collection标签定义,IUserDao.xml的定义:

<!-- 定义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 是用于建立一对多中集合属性的对应关系
         ofType 用于指定集合元素的数据类型 -->
    <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 u.*,a.id as aid ,a.uid,a.money from user u left outer join account a on u.id =a.uid
</select>

User实体类:

public class User implements Serializable {
	...

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

    public void setAccounts(List<Account> accounts) {
        this.accounts = accounts;
    }
}

延迟加载策略

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

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

使用 assocation 实现延迟加载

1.修改IAccountDao.xml,将关联用户设置为延迟加载:

<!-- 定义封装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查询时,所需要的参数名(sql语句返回的列名),必填
    -->
    <association property="user" column="uid" javaType="user" select="com.itheima.dao.IUserDao.findById">
    </association>
</resultMap>

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

2.修改IUserDao.xml,添加查询单个用户的方法:

<!-- 根据id查询用户 -->
<select id="findById" parameterType="int" resultType="user">
    select * from user where id = #{uid}
</select>

3.开启 Mybatis 的延迟加载策略,修改SqlMapConfig.xml

<!--配置参数-->
<settings>
    <!--延迟加载的全局开关,开启Mybatis支持延迟加载,默认值false-->
    <setting name="lazyLoadingEnabled" value="true"/>
    <!--当开启时,任何方法的调用都会加载该对象的所有属性。否则,每个属性会按需加载,默认值false(true in <=3.4.1)-->
    <setting name="aggressiveLazyLoading" value="false"></setting>
</settings>

设置完毕后,当只查询Account而没用到User属性时,不会执行User对象的查询sql语句。

使用 collection 实现延迟加载

1.修改IUserDao.xml,将关联账号设置为延迟加载:

<!-- 定义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集合的映射 -->
    <!-- 用于指定查询account列表的sql语句,所以填写的是该sql映射的id,必填
    column属性指定的内容:用于指定select属性的sql语句的参数来源,上面的参数来自于user的id列,所以就写成id这一个字段名了,必填-->
    <collection property="accounts" ofType="account" select="com.itheima.dao.IAccountDao.findAccountByUid" column="id">
    </collection>
</resultMap>

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

2.修改IAccountDao.xml,添加根据用户id查询账号的方法:

<!-- 根据用户id查询账户列表 -->
<select id="findAccountByUid" resultType="account">
    select * from account where uid = #{uid}
</select>

3.开启mybatis延迟加载全局开关

Mybatis缓存

一级缓存

一级缓存是 SqlSession 级别的缓存,只要 SqlSession 没有 flush 或 close,它就存在。 比如在同一个SqlSession下对用户查询2次User user = userDao.findById(41);,只会执行一次sql语句,2次返回的User对象是同一个对象。 一级缓存是 SqlSession 默认的缓存,当调用 SqlSession 的修改,添加,删除,commit(),close(),clearCache()等方法时,就会清空一级缓存。

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

二级缓存

二级缓存是 mapper 映射级别的缓存,多个 SqlSession 去操作同一个 Mapper 映射的 sql 语句,多个SqlSession可以共用二级缓存,二级缓存是跨 SqlSession 的。

二级缓存的开启与关闭

1.在SqlMapConfig.xml文件开启二级缓存

<settings>
    <!-- 开启二级缓存的支持 -->
    <!-- cacheEnabled 的默认取值就是true,所以这一步其实可以省略不配置 -->
    <setting name="cacheEnabled" value="true"/>
</settings>

2.配置需要二级缓存的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.itheima.dao.IUserDao">
    <!--开启user支持二级缓存-->
    <!--标签表示当前这个 mapper 映射将使用二级缓存,区分的标准就看mapper的namespace值-->
    <cache></cache>
    ...
</mapper>

3.配置 statement 上面的 useCache 属性

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

IUserDao.xml映射文件中的<select>标签中设置useCache="true"代表当前这个 statement 要使用二级缓存,如果不使用二级缓存可以设置为 false。 注意:

  • 在select的statement中useCache默认为true,所以针对每次查询都需要最新的数据sql,要设置成 useCache=false,禁用二级缓存。
  • 二级缓存是将查询对象序列化后缓存起来的,当再次查询时不会执行sql,而是从缓存中反序列化成对象,所以当使用二级缓存时,所缓存的类一定要实现java.io.Serializable接口,这样才可以使用序列化方式来保存对象。

注解开发

使用Mybatis提供的注解方式,就可以不用创建Mapper映射文件了。

mybatis常用注解说明

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

注解实现基本CRUD

1.编写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>
    <!-- 配置别名的注册 -->
    <typeAliases>
        <package name="com.itheima.domain"></package>
    </typeAliases>
    <!-- 配置环境-->
    <environments default="mysql">
        <environment id="mysql">
            <!-- 配置事务的类型是 JDBC -->
            <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>
        <!-- 配置 dao 接口的位置,它有两种方式
			第一种:使用 mapper 标签配置 class 属性
			第二种:使用 package 标签,直接指定 dao 接口所在的包
        -->
        <mapper class="com.itheima.dao.IUserDao"></mapper>
    </mappers>
</configuration>

2.使用注解方式开发持久层接口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="sex",property="userSex"),
			@Result(column="address",property="userAddress"),
			@Result(column="birthday",property="userBirthday")
		})
    List<User> findAll();
    
    /**
     * 根据 id 查询一个用户
     * @param userId
     * @return
     */
    @Select("select * from user  where id=#{id} ")
    @ResultMap("userMap")
    User findById(Integer userId);

    /**
     * 保存操作
     * @param user
     */
    @Insert("insert into user(username,address,sex,birthday)values(#{username},#{address},#{sex},#{birthday})")
    @SelectKey(keyColumn="id",keyProperty="id",resultType=Integer.class,before = 
false, statement = { "select last_insert_id()" })
    int saveUser(User user);

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

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

    /**
     * 查询使用聚合函数
     * @return
     */
    @Select("select count(*) from user ")
    int findTotalUser();

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

Results指定了id后,接下来的ResultMap可以直接引用这个Results,比如查询所有用户和查询单个用户的方法。

注解实现复杂关系映射

注解对应的xml映射字段:

注解

xml

说明

@Results

<resultMap>

可以使用单个@Result 注解,也可以使用@Result 集合<br>@Results({@Result(),@Result()})或@Results(@Result())

@Result

<id>或<result>

属性介绍:id 是否是主键字段column 数据库的列名property 需要装配的属性名one 需要使用的@One注解many 需要使用的@Many注解

@One

<assocation>

用来指定子查询返回单一对象,属性介绍:select 指定用来多表查询的 sqlmapperfetchType 查询方式,懒加载或立即加载,会覆盖全局的配置参数 lazyLoadingEnabled使用格式:<br>@Result(column="",property="",one=@One(select=""))

@Many

<collection>

用来指定子查询返回对象集合,属性介绍:映射文件中的javaType 一般为 ArrayList,注解中可以不定义使用格式:<br>@Result(column="",property="",many=@Many(select=""))

使用@One注解实现一对一关系映射

/**
* 查询所有账户,采用延迟加载的方式查询账户的所属用户
* @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(column="uid",property="user",one=@One(select="com.itheima.dao.IUserDao.findById",fetchType=FetchType.LAZY))
})
List<Account> findAll();

使用@Many注解实现一对多关系映射

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

注解实现二级缓存

1.在SqlMapConfig.xml中开启二级缓存支持

<!-- 配置二级缓存 --> 
<settings>
    <!-- 开启二级缓存的支持 --> 
    <setting name="cacheEnabled" value="true"/>
</settings>

2.在持久层接口中使用注解配置二级缓存

@CacheNamespace(blocking=true)  //mybatis 基于注解方式实现配置二级缓存
public interface IUserDao {
    ...
}

参考资料:

传智播客SSM框架之MyBatis 深入浅出Mybatis系列(五)Mybatis事务篇