这几天学习了Mybatis相关内容,在此整理一下这几天的笔记。
他是我们软件开发中的一套解决方案,不同的框架解决的是不同的问题。
使用框架的好处:框架封装了很多的细节,使开发者可以使用极简的方式实现功能。大大提高开发效率。
表现层:用于展示数据的
业务层:处理业务需求
持久层:和数据库交互的
以上这些都不是框架:
jdbc是规范
Spring的JdbcTemplate和Apache的DBUtils都只是工具类
mybatis是一个优秀的基于 java 的持久层框架,它内部封装了 jdbc,使开发者只需要关注 sql语句本身,而不需要花费精力去处理加载驱动、创建连接、创建 statement 等繁杂的过程。
mybatis通过xml 或注解的方式将要执行的各种statement配置起来,并通过java对象和statement 中 sql 的动态参数进行映射生成最终执行的 sql 语句,最后由 mybatis 框架执行 sql 并将结果映射为 java 对象并返回。
采用 ORM 思想解决了实体和数据库映射的问题,对 jdbc进行了封装,屏蔽了 jdbc api 底层访问细节,使我们不用与 jdbc api 打交道,就可以完成对数据库的持久化操作。
简单地说:就是把数据库和实体类及实体类的属性对应起来,让我们可以通过操作实体类实现操作数据库表
mybatis的环境搭建
第一步:创建maven工程并导入坐标
第二步:创建实体类和dao接口
第三步:创建mybatis的主配置文件SqlMapConfig.xml
第四步:创建映射配置文件IUserDao.xml
搭建环境的注意事项
一:创建IUserDao.xml和IUserDao.java是名称是为了和之前的只是保持一致。在mybatis中它把持久层的操作接口名称和映射文件也叫做:Mapper,所以IUserDao和IUserMapper是一样的
二:在idea中创建目录的时候,它和包是不一样的。包在创建时:com.itheima.dao是三级结构;目录在创建时:com.itheima.dao是一级目录
三:mybatis的映射配置文件位置必须和dao接口的包结构相同
四:映射配置文件的mapper标签namespace属性的取值必须是dao接口的全限定类名
五:映射配置文件的操作配置(select),id属性的取值必须是dao接口的方法名
当我们遵从了三四五点之后,我们在开发中就无须在写dao的实现类。
mybatis的入门案例
tips:不要忘记在映射配置文件中告知mybatis要封装到哪个实体类中,配置的方式:指定实体类的全限定类名
把IUserDao.xml移除,在dao接口的方法上使用@select注解,并且指定SQL语句。同时需要在SqlMapConfig.xml中的mapper配置时,使用class属性指定dao接口的全限定类名。
tips:我们在实际开发中都是越简便越好,所以不管使用XML还是注解配置都是采用不写dao实现类的方式。但是mybatis还是支持写dao实现类的。
分析:
mybatis在使用代理dao的方式实现增删改查时做了什么?
自定义mybatis通过入门案例看到类
class Resource class SqlSqssionFactoryBuilder Interface SqlSessionFactory interface SqlSession
实现代码省略...
自定义mybatis开发流程图:
mybatis配置的细节
几个标签的使用
使用要求:
1、持久层接口和持久层接口的映射配置必须在相同的包下
2、持久层映射配置中mapper标签的namespace属性取值必须是持久层接口的全限定类名
3、SQL语句的配置标签 <select>
, <insert>
, <delete>
, <update>
的id属性必须和持久层接口的方法名相同
在持久层接口中添加findById方法
/** * 根据 id 查询 * @param userId * @return */ User findById(Integer userId)
在用户的映射配置文件中配置
<!-- 根据 id 查询 --> <select id="findById" resultType="com.itheima.domain.User" parameterType="int"> select * from user where id = #{uid} </select>
细节:
resultType 属性: 用于指定结果集的类型。 parameterType 属性: 用于指定传入参数的类型。 sql 语句中使用#{}字符: 它代表占位符,相当于原来 jdbc 部分所学的?,都是用于执行语句时替换实际的数据。 具体的数据是由#{}里面的内容决定的。 #{}中内容的写法: 由于数据类型是基本类型,所以此处可以随意写。
在持久层接口中添加新增方法
/** * 保存用户 * @param user * @return 影响数据库记录的行数 */ int saveUser(User user);
在用户的映射配置文件中配置
<!-- 保存用户 --> <!-- 保存用户--> <insert id="saveUser" parameterType="com.itheima.domain.User"> insert into user(username,birthday,sex,address) values(#{username},#{birthday},#{sex},#{address}) </insert>
细节:
patameterType属性: 代表参数的类型,因为我们要传入的是一个类的对象,所以类型就写类的全名称。 SQL语句中使用#{}字符: 它代表占位符,相当于原来jdbc部分所学的?,都是用于执行语句是替换实际的数据。具体的数据是由#{}里面的内容决定的。 #{}中内容的写法: 由于我们保存方法的参数是一个User对象,此处要写User对象中的属性名称,他用的是OGNL表达式。 OGNL表达式: 是Apache提供的一种表达式语言,全称是:Object Graphic Navigation Language 对象导航语言 它是按照一定的语法格式来获取数据的。语法格式就是使用 #{对象.对象}的方式。 #{user.username}它会先去找 user 对象,然后在 user 对象中找到 username 属性,并调用 getUsername()方法把值取出来。但是我们在 parameterType 属性上指定了实体类名称,所以可以省略 user,而直接写 username。
tips:在实现增删改时一定要去控制事务的提交,那么在 mybatis 中如何控制事务提交呢?
可以使用 session.commit() 来实现事务提交。
新增用户ID的返回值
新增用户后,同时还要返回当前新增用户的 id 值,因为 id 是由数据库的自动增长来实现的,所以就相当于我们要在新增后将自动增长 auto_increment 的值返回。
<insert id="saveUser" parameterType="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>
在持久层接口中添加更新方法
/** * 更新用户 * @param user * @return 影响数据库记录的行数 */ int updateUser(User user);
在用户的映射配置文件中配置
<!-- 更新用户 --> <update id="updateUser" parameterType="com.itheima.domain.User"> update user set username=#{username},birthday=#{birthday},sex=#{sex},address=#{address} where id=#{id} <update>
在持久层接口中添加删除方法
/** * 根据id删除用户 * @param userId * @return */ int deleteUser(Integer userId);
在用户的映射配置文件中配置
<!-- 删除用户 --> <delete id="deleteUser" parameterType="java.lang.Integer"> delete from user where id = #{uid} <delete>
在持久层接口中添加模糊查询方法
/** * 根据名称模糊查询 * @param username * @return */ List<User> findByName(String username);
在用户的映射配置文件中配置
<!-- 根据名称模糊查询 --> <select id="findByName" resultType="com.itheima.domain.User" parameterType="String"> select * from user where username like #{username} </select>
我们在配置文件中没有加入%来作为模糊查询的条件,所以在传入字符串实参时,就需要给定模糊查询的标识%。
配置文件中的#{username}也只是一个占位符,所以SQL语句会显示为“?”。
模糊查询的另一种配置方式
<!-- 根据名称模糊查询 --> <select id="findByName" resultType="com.itheima.domain.User" parameterType="String"> select * from user where username like '%${value}%' </select>
我们在上面将原来的#{}占位符改成了 /({value}。如果用模糊查询的这种写法,那么/) {value}的写法就是固定的,不能写成其他名字
#{}
和 ${}
的区别 通过#{}可以实现preparedStatement向占位符中设置值,自动进行java类型和jdbc类型转换,#{}可以有效防止SQL注入。#{}可以接受简单类型值或pojo属性值。如果parameterType传输单个简单类型值,#{}扩号中可以是value或其他名称。
通过 ${}
可以将 parameterType 传入的内容拼接在 sql中且不进行 jdbc 类型转换, ${}
可以接收简单类型值或 pojo 属性值,如果 parameterType 传输单个简单类型值, ${}
括号中只能是 value。
这就说明了源码中指定了读取的key的名字就是“value”,所以我们在绑定参数时就只能叫value。
在持久层接口中添加聚合函数查询方法
/** * 查询总记录条数 * @return */ int findTotal();
在用户的映射配置文件中配置
<!-- 查询总记录条数 --> <select id="findTotal" resultType="int"> select count(*) from user; </select>
1、数据可连接创建、释放频繁造成系统资源浪费从而影响系统性能,如果使用数据库连接池可解决此问题。
解决:在SqlMapConfig.xml中配置数据库连接池,使用连接池管理数据库连接。
2、SQL语句写在代码中造成代码不易维护,实际应用SQL变化的可能较大,SQL变动须要改变java代码。
解决:将SQL语句配置在xxxmapper.xml文件中与java代码分离
3、向SQL语句传参数麻烦,因为SQL语句的where条件不一定,可能多也可能少,占位符需要和参数对应。
解决:mybatis自动将java映射至SQL语句,通过statement中的parameterType定义输入参数的类型
4、对结果集解析麻烦,SQL变化导致解析代码变化,且解析前需要遍历,如果能将数据库记录封装成pojo对象解析比较方便
解决:mybatis自动将SQL执行结果映射至java对象,通过statement中的resultType定义输出结果的类型
我们在上一章节中已经介绍了 SQL 语句传参,使用标签的 parameterType 属性来设定。该属性的取值可以是基本类型,引用类型(例如:String 类型),还可以是实体类类型(POJO 类)。同时也可以使用实体类的包装类,本章节将介绍如何使用实体类的包装类作为参数传递。
基本类型和 String 我们可以直接写类型名称 ,也可以使用 包名.类名 的方式,例如 : java.lang.String
。实体类类型目前我们只能使用全限定类名,究其原因是因为 mybaits 在加载时已经把常用的数据类型注册了别名,从而我们在使用时可以不写包名,而我们的是实体类并没有注册别名,所以必须写全限定类名。接下来将讲解如何注册实体类的别名。
开发中通过 pojo 传递查询条件 ,查询条件是综合的查询条件,不仅包括用户查询条件还包括其它的查询条件(比如将用户购买商品信息也作为查询条件),这时可以使用包装对象传递输入参数。Pojo 类中包含 pojo。
需求:根据用户名查询用户信息,查询条件放到 QueryVo 的 user 属性中。
编写QueryVo
public class QueryVo implements Serializable { private User user; public User getUser() { return user; } public void setUser(User user) { this.user = user; } }
编写持久层接口
public interface IUserDao { /** * 根据 QueryVo 中的条件查询用户 * @param vo * @return */ List<User> findByVo(QueryVo vo); }
持久层接口的映射文件
<!-- 根据用户名称模糊查询,参数变成一个 QueryVo 对象了 --> <select id="findByVo" resultType="com.itheima.domain.User" parameterType="com.itheima.domain.QueryVo"> select * from user where username like #{user.username}; </select>
resultType 属性可以指定结果集的类型,它支持基本类型和实体类类型。
需要注意的是,它和 parameterType 一样,如果注册过类型别名的,可以直接使用别名。没有注册过的必须使用全限定类名。
例如:我们的实体类此时必须是全限定类名。同时,当是实体类名称是,还要求实体类中的属性名称必须和查询语句中的列名保持一致,否则无法实现封装。
resultMap 标签可以建立查询的列名和实体类的属性名称不一致时建立对应关系,从而实现封装。
在 select 标签中使用 resultMap 属性指定引用即可。同时 resultMap 可以实现将查询结果映射为复杂类
型的 pojo,比如在查询结果映射对象中包括 pojo 和 list 实现一对一查询和一对多查询。
<!-- 建立 User 实体和数据库表的对应关系 type 属性:指定实体类的全限定类名 id 属性:给定一个唯一标识,是给查询 select 标签引用用的。 --> <resultMap type="com.itheima.domain.User" id="userMap"> <id column="id" property="userId"/> <result column="username" property="userName"/> <result column="sex" property="userSex"/> <result column="address" property="userAddress"/> <result column="birthday" property="userBirthday"/> </resultMap> id 标签:用于指定主键字段 result 标签:用于指定非主键字段 column 属性:用于指定数据库列名 property 属性:用于指定实体类属性名称
<!-- 配置查询所有操作 --> <select id="findAll" resultMap="userMap"> select * from user </select>
SqlMapConfig.xml 中配置的内容和顺序
-properties(属性) --property -settings(全局配置参数) --setting -typeAliases(类型别名) --typeAliase --package -typeHandlers(类型处理器) -objectFactory(对象工厂) -plugins(插件) -environments(环境集合属性对象) --environment(环境子属性对象) ---transactionManager(事务管理) ---dataSource(数据源) -mappers(映射器) --mapper --package
在使用 properties 标签配置时,我们可以采用两种方式指定属性配置
<properties> <property name="jdbc.driver" value="com.mysql.jdbc.Driver"/> <property name="jdbc.url" value="jdbc:mysql://localhost:3306/eesy"/> <property name="jdbc.username" value="root"/> <property name="jdbc.password" value="root"/> </properties>
在 classpath 下定义 db.properties 文件
jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/eesy jdbc.username=root jdbc.password=root
properties 标签配置
<!-- 配置连接数据库的信息 resource 属性:用于指定 properties 配置文件的位置,要求配置文件必须在类路径下 resource="jdbcConfig.properties" url 属性: URL: Uniform Resource Locator 统一资源定位符 http://localhost:8080/mystroe/CategoryServlet URL 协议 主机 端口 URI URI:Uniform Resource Identifier 统一资源标识符 /mystroe/CategoryServlet 它是可以在 web 应用中唯一定位一个资源的路径 --> <properties url=file:///D:/IdeaProjects/day02_eesy_01mybatisCRUD/src/main/resources/jdbcConfig.properties"> </properties> <!--此时我们的 dataSource 标签就变成了引用上面的配置--> <dataSource type="POOLED"> <property name="driver" value="${jdbc.driver}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </dataSource>
在前面我们讲的 Mybatis 支持的默认别名,我们也可以采用自定义别名方式来开发。
自定义别名:
<!--在 SqlMapConfig.xml 中配置:--> <typeAliases> <!-- 单个别名定义 --> <typeAlias alias="user" type="com.itheima.domain.User"/> <!-- 批量别名定义,扫描整个包下的类,别名为类名(首字母大写或小写都可以) --> <package name="com.itheima.domain"/> <package name="其它包"/> </typeAliases>
< mapper resource=" " />
使用相对于类路径的资源
如:< mapper resource=“com/itheima/dao/IUserDao.xml” />
< mapper class=" " />
使用 mapper 接口类路径
如:< mapper class=“com.itheima.dao.UserDao”/>
注意:此种方法要求 mapper 接口名称和 mapper 映射文件名称相同,且放在同一个目录中。
< package name=""/>
注册指定包下的所有 mapper 接口
如:< package name=“cn.itcast.mybatis.mapper”/>
注意:此种方法要求 mapper 接口名称和 mapper 映射文件名称相同,且放在同一个目录中。
mybatis的深入和多表
mybatis的连接池
mybatis的事务控制及设计的方法
mybatis的多表查询
一对多(多对一)
多对多
Mybatis 中的连接池技术,它采用的是自己的连接池技术。在 Mybatis 的 SqlMapConfig.xml 配置文件中,通过< dataSource type=”pooled”>来实现 Mybatis 中连接池的配置。
Mybatis 将它自己的数据源分为三类:
UNPOOLED | 不使用连接池的数据源 |
POOLED | 使用连接池的数据源 |
JNDI | 使用 JNDI 实现的数据源 |
具体结构如下:
相应地,MyBatis 内部分别定义了实现了 java.sql.DataSource 接口的 UnpooledDataSource,PooledDataSource 类来表示 UNPOOLED、POOLED 类型的数据源。
在这三种数据源中,我们一般采用的是 POOLED 数据源(很多时候我们所说的数据源就是为了更好的管理数据库连接,也就是我们所说的连接池技术)。
我们的数据源配置就是在 SqlMapConfig.xml 文件中,具体配置如下:
<!-- 配置数据源(连接池)信息 --> <dataSource type="POOLED"> <property name="driver" value="${jdbc.driver}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </dataSource>
MyBatis 在初始化时,根据
MyBatis 是通过工厂模式来创建数据源 DataSource 对象的, MyBatis 定义了抽象的工厂口:org.apache.ibatis.datasource.DataSourceFactory,通过其 getDataSource()方法返回数据源DataSource。
下面是 DataSourceFactory 源码,具体如下:
package org.apache.ibatis.datasource; import java.util.Properties; import javax.sql.DataSource; /** @author Clinton Begin */ public interface DataSourceFactory { void setProperties(Properties props); DataSource getDataSource(); }
MyBatis 创建了 DataSource 实例后,会将其放到 Configuration 对象内的 Environment 对象中, 供以后使用。
具体分析过程如下:
1.先进入 XMLConfigBuilder 类中,可以找到如下代码:
2.分析 configuration 对象的 environment 属性,结果如下:
当我们需要创建 SqlSession 对象并需要执行 SQL 语句时,这时候 MyBatis 才会去调用 dataSource 对象来创建java.sql.Connection对象。也就是说,java.sql.Connection对象的创建一直延迟到执行SQL语句的时候。
@Test public void testSql() throws Exception { InputStream in = Resources.getResourceAsStream("SqlMapConfig.xml"); SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in); SqlSession sqlSession = factory.openSession(); List<User> list = sqlSession.selectList("findUserById",41); System.out.println(list.size()); }
只有当第 4句sqlSession.selectList("findUserById"),才会触发MyBatis 在底层执行下面这个方法来创建 java.sql.Connection 对象。
如何证明它的加载过程呢?
我们可以通过断点调试,在 PooledDataSource 中找到如下 popConnection()方法,如下所示:
分析源代码,得出 PooledDataSource 工作原理如下:
下面是连接获取的源代码:
最后我们可以发现,真正连接打开的时间点,只是在我们执行SQL语句时,才会进行。其实这样做我们也可以进一步发现,数据库连接是我们最为宝贵的资源,只有在要用到的时候,才去获取并打开连接,当我们用完了就再立即将数据库连接归还到连接池中。
在 JDBC 中我们可以通过手动方式将事务的提交改为手动方式,通过 setAutoCommit()方法就可以调整。
Mybatis 框架因为是对 JDBC 的封装,所以 Mybatis 框架的事务控制方式,本身也是用 JDBC 的setAutoCommit()方法来设置事务提交方式的。
Mybatis 中事务的提交方式,本质上就是调用 JDBC 的 setAutoCommit()来实现事务控制。
运行下面测试代码:
@Test public void testSaveUser() throws Exception { User user = new User(); user.setUsername("mybatis user09"); //6.执行操作 int res = userDao.saveUser(user); System.out.println(res); System.out.println(user.getId()); } @Before//在测试方法执行之前执行 public void init()throws Exception { //1.读取配置文件 in = Resources.getResourceAsStream("SqlMapConfig.xml"); //2.创建构建者对象 SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder(); //3.创建 SqlSession 工厂对象 factory = builder.build(in); //4.创建 SqlSession 对象 session = factory.openSession(); //5.创建 Dao 的代理对象 userDao = session.getMapper(IUserDao.class); } @After//在测试方法执行完成之后执行 public void destroy() throws Exception{ //7.提交事务 session.commit(); //8.释放资源 session.close(); in.close(); }
观察控制台输出的结果:
这是我们的 Connection 的整个变化过程,通过分析我们能够发现之前的 CUD 操作过程中,我们都要手动进行事务的提交,原因是 setAutoCommit()方法,在执行时它的值被设置为 false 了,所以我们在 CUD 操作中,必须通过 sqlSession.commit()方法来执行提交操作。
通过上面的研究和分析,为什么 CUD 过程中必须使用 sqlSession.commit()提交事务?主要原因就是在连接池中取出的连接,都会将调用 connection.setAutoCommit(false)方法,这样我们就必须使用 sqlSession.commit()方法,相当于使用了 JDBC 中的 connection.commit()方法实现事务提交。
明白这一点后,我们现在一起尝试不进行手动提交,一样实现 CUD 操作。
//4.创建 SqlSession 对象 session = factory.openSession(true);
此时事务就设置为自动提交了,同样可以实现CUD操作时记录的保存。虽然这也是一种方式,但就编程而言,设置为自动提交方式为 false 再根据情况决定是否进行提交,这种方式更常用。因为我们可以根据业务情况来决定提交是否进行提交。
Mybatis 的映射文件中,前面我们的 SQL 都是比较简单的,有些时候业务逻辑复杂时,我们的 SQL 是动态变化的,此时在前面的学习中我们的 SQL 就不能满足要求了。
参考的官方文档描述如下:
<if>
标签 我们根据实体类的不同取值,使用不同的 SQL 语句来进行查询。比如在 id 如果不为空时可以根据 id 查询,如果 username 也不为空时还要加入用户名作为条件。这种情况在我们的多条件组合查询中经常会碰到。
/** * 根据用户信息,查询用户列表 * @param user * @return */ List<User> findByUser(User user);
<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>
标签的 test 属性中写的是对象的属性名,如果是包装类的对象要使用 OGNL 表达式的写法。
另外要注意 where 1=1 的作用~!
<where>
标签 为了简化上面 where 1=1 的条件拼装,我们可以采用
<!-- 根据用户信息查询 --> <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>
标签 传入多个 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)
这样我们在进行范围查询时,就要将一个集合中的值,作为参数动态添加进来。 这样我们将如何进行参数的传递?
public class QueryVo implements Serializable { private List<Integer> ids; public List<Integer> getIds() { return ids; } public void setIds(List<Integer> ids) { this.ids = ids; } }
/** * 根据 id 集合查询用户 * @param vo * @return */ List<User> findInIds(QueryVo vo);
<!-- 查询所有用户在 id 的集合之中 --> <select id="findInIds" resultType="user" parameterType="queryvo"> <!-- select * from user where id in (1,2,3,4,5); --> <include refid="defaultSql"></include> <where> <if test="ids != null and ids.size() > 0"> <foreach collection="ids" open="id in ( " close=")" item="uid" separator=","> #{uid} </foreach> </if> </where> </select>
SQL 语句:
select 字段 from user where id in (?)`
标签用于遍历集合,它的属性:
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>
本次案例主要以最为简单的用户和账户的模型来分析Mybatis多表关系。用户为User 表,账户为Account 表。一个用户(User)可以有多个账户(Account)。具体关系如下:
需求
查询所有账户信息,关联查询下单用户信息。
注意:
因为一个账户信息只能供某个用户使用,所以从查询账户信息出发关联查询用户信息为一对一查询。如果从用户信息出发查询用户下的账户信息则为一对多查询,因为一个用户可以有多个账户。
public class Account implements Serializable { private Integer id; private Integer uid; private Double money; ...//省略getter/setter/tostring }
实现查询账户信息时,也要查询账户所对应的用户信息。
SELECT account.*, user.username, user.address FROM account, user WHERE account.uid = user.id
为了能够封装上面SQL语句查询结果,定义了AccountCustomer类中要包含账户信息同时还要包含用户信息,所以我们要在定义AccountUser类是可以继承User类。
public class AccountUser extends Account implements Serializable { private String username; private String address; ...getter/setter @Override public String toString() { return super.toString() + " AccountUser [username=" + username + ", address=" + address + "]"; } }
public interface IAccountDao { /** * 查询所有账户,同时获取账户的所属用户名称以及它的地址信息 * @return */ List<AccountUser> findAll(); }
<!-- 配置查询所有操作--> <mapper namespace="com.itheima.dao.IAccountDao"> <select id="findAll" resultType="accountuser"> select a.*,u.username,u.address from account a,user u where a.uid =u.id; </select> </mapper>
注意:因为上面查询的结果中包含了账户信息同时还包含了用户信息,所以我们的返回值类型 returnType的值设置为 AccountUser 类型,这样就可以接收账户信息和用户信息了。
小结:定义专门的 pojo 类作为输出类型,其中定义了 sql 查询结果集所有的字段。此方法较为简单,企业中使用普遍。
使用 resultMap,定义专门的 resultMap 用于映射一对一查询结果。通过面向对象的(has a)关系可以得知,我们可以在 Account 类中加入一个 User 类的对象来代表这个账户是哪个用户的。
在 Account 类中加入 User 类的对象作为 Account 类的一个属性。
public class Account implements Serializable { private Integer id; private Integer uid; private Double money; private User user; ...getter/setter/tostring }
public interface IAccountDao { /** * 查询所有账户,同时获取账户的所属用户名称以及它的地址信息 * @return */ List<Account> findAll(); }
注意:第二种方式,将返回值改 为了 Account 类型。
因为 Account 类中包含了一个 User 类的对象,它可以封装账户所对应的用户信息。
<mapper namespace="com.itheima.dao.IAccountDao"> <!-- 建立对应关系 --> <resultMap type="account" id="accountMap"> <id column="aid" property="id"/> <result column="uid" property="uid"/> <result column="money" property="money"/> <!-- 它是用于指定从表方的引用实体属性的 --> <association property="user" javaType="user"> <id column="id" property="id"/> <result column="username" property="username"/> <result column="sex" property="sex"/> <result column="birthday" property="birthday"/> <result column="address" property="address"/> </association> </resultMap> <select id="findAll" resultMap="accountMap"> select u.*,a.id as aid,a.uid,a.money from account a,user u where a.uid =u.id; </select> </mapper>
需求:
查询所有用户信息及用户关联的账户信息。
分析:
用户信息和他的账户信息为一对多关系,并且查询过程中如果用户没有账户信息,此时也要将用户信息查询出来,我们想到了左外连接查询比较合适。
SELECT u.*, acc.id id, acc.uid, acc.money FROM user u LEFT JOIN account acc ON u.id = acc.uid
public class User implements Serializable { private Integer id; private String username; private Date birthday; private String sex; private String address; private List<Account> accounts; //...getter/setter/tostring }
<mapper namespace="com.itheima.dao.IUserDao"> <resultMap type="user" id="userMap"> <id column="id" property="id"/> <result column="username" property="username"/> <result column="address" property="address"/> <result column="sex" property="sex"/> <result column="birthday" property="birthday"/> <!-- collection 是用于建立一对多中集合属性的对应关系 ofType 用于指定集合元素的数据类型--> <collection property="accounts" ofType="account"> <id column="aid" property="id"/> <result column="uid" property="uid"/> <result column="money" property="money"/> </collection> </resultMap> <!-- 配置查询所有操作 --> <select id="findAll" resultMap="userMap"> select u.*,a.id as aid ,a.uid,a.money from user u left outer join account a on u.id =a.uid </select> </mapper>
collection:
部分定义了用户关联的账户信息。表示关联查询结果集
property="accList":
关联查询的结果集存储在 User 对象的上哪个属性。
ofType="account":
指定关联查询的结果集中的对象类型即List中的对象类型。此处可以使用别名,也可以使用全限定名。
多对多关系其实我们看成是双向的一对多关系。
需求:
实现查询所有对象并且加载它所分配的用户信息。
分析:
查询角色我们需要用到Role表,但角色分配的用户的信息我们并不能直接找到用户信息,而是要通过中间表(USER_ROLE 表)才能关联到用户信息。
下面是实现的 SQL 语句:
SELECT r.*,u.id uid, u.username username, u.birthday birthday, u.sex sex, u.address address FROM ROLE r INNER JOIN USER_ROLE ur ON ( r.id = ur.rid) INNER JOIN USER u ON (ur.uid = u.id);
public class Role implements Serializable { private Integer roleId; private String roleName; private String roleDesc; //多对多的关系映射:一个角色可以赋予多个用户 private List<User> users; //...getter/setter/tostring }
public interface IRoleDao { /** * 查询所有角色 * @return */ List<Role> findAll(); }
<mapper namespace="com.itheima.dao.IRoleDao"> <!--定义 role 表的 ResultMap--> <resultMap id="roleMap" type="role"> <id property="roleId" column="rid"></id> <result property="roleName" column="role_name"></result> <result property="roleDesc" column="role_desc"></result> <collection property="users" ofType="user"> <id column="id" property="id"></id> <result column="username" property="username"></result> <result column="address" property="address"></result> <result column="sex" property="sex"></result> <result column="birthday" property="birthday"></result> </collection> </resultMap> <!--查询所有--> <select id="findAll" resultMap="roleMap"> select u.*,r.id as rid,r.role_name,r.role_desc from role r left outer join user_role ur on r.id = ur.rid left outer join user u on u.id = ur.uid </select> </mapper>
从 User 出发,我们也可以发现一个用户可以具有多个角色,这样用户到角色的关系也还是一对多关系。这样我们就可以认为 User 与 Role 的多对多关系,可以被拆解成两个一对多关系来实现。
User到Role的多对多和Role到User的多对多实现差别不大。
在User中添加 List
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
延迟加载:
就是在需要用到数据时才进行加载,不需要用到数据时就不加载数据。延迟加载也称懒加载。
好处:
先从单表查询,需要时再从关联表去关联查询,大大提高数据库性能,因为查询单表要比关联查询多张表速度要快。
坏处:
因为只有当需要用到数据时,才会进行数据库查询,这样在大批量数据查询时,因为查询工作也要消耗时间,所以可能造成用户等待时间变长,造成用户体验下降。
查询账户(Account)信息并且关联查询用户(User)信息。如果先查询账户(Account)信息即可满足要求,当我们需要查询用户(User)信息时再查询用户(User)信息。把对用户(User)信息的按需去查询就是延迟加载。
mybatis第三天实现多表操作时,我们使用了resultMap来实现一对一,一对多,多对多关系的操作。主要是通过 association、collection 实现一对一及一对多映射。association、collection 具备延迟加载功能。
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.itheima.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.itheima.dao.IUserDao.findById"></association> </resultMap> <!-- 查询所有 --> <select id="findAll" resultMap="accountUserMap"> select * from account </select> <!-- 根据用户id查询账户列表 --> <select id="findAccountByUid" resultType="account"> select * from account where uid = #{uid} </select> </mapper>
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.itheima.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.itheima.dao"></package> </mappers> </configuration>
IUserDao.xml
<resultMap type="user" id="userMap"> <id column="id" property="id"></id> <result column="username" property="username"/> <result column="address" property="address"/> <result column="sex" property="sex"/> <result column="birthday" property="birthday"/> <!-- collection是用于建立一对多中集合属性的对应关系 ofType用于指定集合元素的数据类型 select是用于指定查询账户的唯一标识(账户的dao全限定类名加上方法名称) column适用于指定使用哪个字段的值作为条件查询 --> <collection property="accounts" ofType="account" select="com.itheima.dao.IAccountDao.findByUid" column="id"> </collection> </resultMap> <!-- 配置查询所有操作 --> <select id="findAll" resultMap="userMap"> select * from user </select>
IAccountDao.xml
<!-- 根据用户id查询账户信息 --> <select id="findByUid" resultType="account" parameterType="int"> select * from account where uid = #{uid} </select>
什么是缓存
存在于内存中的临时数据。
为什么使用缓存
减少和数据库的交互次数,提高执行效率。
什么样的数据能使用缓存,什么样的数据不能使用
适用于缓存:
不适用于缓存:
Mybatis中的缓存分为一级缓存、二级缓存
它指的是Mybatis中SqlSession对象的缓存。
当我们执行查询之后,查询的结果会同时存入到SqlSession为我们提供一块区域中。
该区域的结构是一个Map。当我们再次查询同样的数据,Mybatis会先去SqlSession中查询是否有,有的话直接拿出来用。
当SqlSession对象消失时,Mybatis的一级缓存也就消失了。
分析:
一级缓存是 SqlSession 范围的缓存,当调用 SqlSession 的修改,添加,删除,commit(),close()等方法时,就会清空一级缓存。
第一次发起查询用户 id 为 1 的用户信息,先去找缓存中是否有 id 为 1 的用户信息,如果没有,从数据库查询用户信息。
得到用户信息,将用户信息存储到一级缓存中。
如果 sqlSession 去执行 commit 操作(执行插入、更新、删除),清空 SqlSession 中的一级缓存,这样做的目的为了让缓存中存储的是最新的信息,避免脏读。
第二次发起查询用户 id 为 1 的用户信息,先去找缓存中是否有 id 为 1 的用户信息,缓存中有,直接从缓存中获取用户信息。
二级缓存:
它指的是Mybatis中SqlSessionFactory对象的缓存。
由同一个SqlSessionFactory对象创建的SqlSession共享其缓存。
首先开启 mybatis 的二级缓存。
sqlSession1 去查询用户信息,查询到用户信息会将查询数据存储到二级缓存中。
如果 SqlSession3 去执行相同 mapper 映射下 sql,执行 commit 提交,将会清空该 mapper 映射下的二级缓存区域的数据。
sqlSession2 去查询与 sqlSession1 相同的用户信息,首先会去缓存中找是否存在数据,如果存在直接从缓存中取出数据。
二级缓存的使用步骤:
第一步:让Mybatis框架支持二级缓存(在SqlMapConfig.xml中配置)
第二步:让当前的映射文件支持二级缓存(在IUserDao.xml中配置/通过注解配置)
第三步:让当前的操作支持二级缓存(在select标签中配置)
<settings> <!-- 开启二级缓存的支持 --> <setting name="cacheEnabled" value="true"/> </settings>
因为cacheEnabled的取值默认就为true,所以这一步可以省略不配置,为true代表开启二级缓存;为false代表不开启二级缓存。
<cache>标签表示当前这个mapper映射将使用二级映射,区分的标准就看mapper的namespace值 <?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.com.IUserDao"> <!-- 开启二级缓存的支持 --> <cache></cache> </mapper>
<!-- 根据id查询 --> <select id="findById" resultType="user" parameterType="int" useCache="true"> select * from user where id = #{uid} </select>
将UserDao.xml映射文件中的 <select>
标签中设置useCache="true"代表这个statement要使用二级缓存,如果不使用二级缓存可以设置为false。
当我们在使用二级缓存时,所缓存的类一定要实现 java.io.Serializable 接口,这种就可以使用序列化方式来保存对象。
这几年来注解开发越来越流行,Mybatis 也可以使用注解开发方式,这样我们就可以减少编写 Mapper 映射文件了。本次我们先围绕一些基本的 CRUD 来学习,再学习复杂映射关系及延迟加载。
@Insert
:实现新增
@Update
:实现更新
@Delete
:实现删除
@Select
:实现查询
@Result
:实现结果集封装
@Results
:可以与@Result 一起使用,封装多个结果集
@ResultMap
:实现引用@Results 定义的封装
@One
:实现一对一结果集封装
@Many
:实现一对多结果集封装
@SelectProvider
: 实现动态 SQL 映射
@CacheNamespace
:实现注解二级缓存的使用
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"> <!-- 配置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> <!-- 配置映射信息 --> <mappers> <!-- 配置dao接口的位置,它有两种方式 第一种:使用mapper标签配置class属性 第二种:使用package标签,直接指定dao接口所在的包 --> <package name="com.itheima.dao"></package> <!-- 或 <mapper class="com.itheima.dao.IUserDao"></mapper> --> </mappers> </configuration>
IUserDao.java
package com.itheima.dao; import com.itheima.domain.User; import org.apache.ibatis.annotations.Delete; import org.apache.ibatis.annotations.Insert; import org.apache.ibatis.annotations.Select; import org.apache.ibatis.annotations.Update; import java.util.List; /** * 在mybatis中针对,CRUD一共有四个注解 * @Select @Insert @Update @Delete */ public interface IUserDao { /** * 查询所有用户 * @return */ @Select("select * from user") List<User> findAll(); /** * 保存用户 * @param user */ @Insert("insert into user(username,address,sex,birthday)values(#{username},#{address},#{sex},#{birthday})") void saveUser(User user); /** * 更新用户 * @param user */ @Update("update user set username=#{username},sex=#{sex},birthday=#{birthday},address=#{address} where id=#{id}") void updateUser(User user); /** * 删除用户 * @param userId */ @Delete("delete from user where id=#{id} ") void deleteUser(Integer userId); /** * 根据id查询用户 * @param userId * @return */ @Select("select * from user where id=#{id} ") User findById(Integer userId); /** * 根据用户名称模糊查询 * @param username * @return */ //@Select("select * from user where username like '%${value}%' ") @Select("select * from user where username like #{username} ") List<User> findUserByName(String username); /** * 查询总用户数量 * @return */ @Select("select count(*) from user ") int findTotalUser(); }
实现复杂关系映射之前我们可以在映射文件中通过配置
<resultMap>
该注解中可以使用单个@Result 注解,也可以使用@Result 集合
@Results({@Result(),@Result()})或@Results(@Result())
@Resutl 注解
<id>
标签和 <result>
标签 @Result 中 属性介绍:
@One 注解属性介绍:
使用格式:
@Result(column=" ",property="",one=@One(select=""))
注意:聚集元素用来处理“一对多”的关系。需要指定映射的 Java 实体类的属性,属性的 javaType(一般为 ArrayList)但是注解中可以不定义;
使用格式:
@Result(property="",column="",many=@Many(select=""))
加载账户信息时并且加载该账户的用户信息,根据情况可实现延迟加载。(注解方式实现)
IUserdao
@CacheNamespace(blocking = true) 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); }
@Many:
相当于 <collection>
的配置
select 属性:代表将要执行的 sql 语句
fetchType 属性:代表加载方式,一般如果要延迟加载都设置为 LAZY 的值
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.itheima.dao.IUserDao.findById",fetchType= FetchType.EAGER)) }) List<Account> findAll(); /** * 根据用户id查询账户信息 * @param userId * @return */ @Select("select * from account where uid = #{userId}") List<Account> findAccountByUid(Integer userId); }
<!-- 配置二级缓存 --> <settings> <!-- 开启二级缓存的支持 --> <setting name="cacheEnabled" value="true"/> </settings>
@CacheNamespace(blocking=true) //mybatis基于注解方式实现配置二级缓存 public interface IUserDao()