玩转MyBatis多表关系#
xml版: 一对一级联#
实验场景: 比如说学生和学号是百分百的一对一的关系, 但是我直接用一个不太恰当的例子, 就是强制规定 用户和账户的是一对一的关系,即一个用户只能存在一个账户(这个例子很牵强...)
这时,我们想实现这样的级联效果: 在查询账户的同时,级联查询出这个账户所属的用户信息
第一点: 就是数据库中的表怎么设计,在账户表中添加一列当成外键,关联用户表中的用户id
第二点: 我们想在查询账户的同时级联查询出用户的信息,说白了就是让Mybatis帮我们将属性封装进我们使用resultType
标签指定的返回值类型的对象中,于是我们就得像下面这样写 账户类,在账户类中添加用户信息引用
public class Account { private Integer id; private Integer uid; private Integer money; private User user; }
第三点: sql语句怎么写?
关注点就是下面的resultMap
标签中的<association>
标签, 通过这个标签告诉Mybatis如何将查询出来的结果封装进Account
中的User
字段
可以看到我在association
标签中将user所有的属性全都配置进去了,其实这是没必要的,因为我的sql语句并没有返回全部的结果
association
中存在一个 column
属性,这个属性存放就是在account
表中的外键的列名 , javaType
表示是告诉MyBatis,这个封装类的类型
<resultMap id="accountUserMap" type="com.changwu.pojo.Account"> <id property="id" column="id"/> <result property="uid" column="uid"/> <result property="money" column="money"/> <association property="user" column="uid" `javaType`="com.changwu.pojo.User"> <id property="id" column="id"/> <result property="userName" column="userName"/> <result property="birthday" column="birthday"/> <result property="sex" column="sex"/> <result property="address" column="address"/> </association> </resultMap> <!--查询所有的账户,同时包含用户名和地址信息--> <select id="findAllAccount" resultMap="accountUserMap" > select a.*,u.username,u.address from account a,user u where a.uid=u.id </select>
注解版: 一对一级联#
配置查询账户时,级联查询出账户所属的用户,如果说,账户实体和数据库中表的字段命名不同,需要用到下面的@Result()
注解进行纠正,当然虽然我下面写了四个@Result
,除了第一个配置id,中间两个的property
和column
值是一样的,所以根本没有写的必要
有必要的是一对一的关系需要使用@Result()
配置,同样column
为Account表中的关联user表中的外键列名,强制不能写错,具体是一对一,但是一对多的关系通过one 和 manay控制, 通过fetchType控制是及早求值还是懒加载
@Select("select * from account") @Results(id = "account",value = { //先描述自己的信息,然后描述一对一的信息 @Result(id = true, property = "id",column = "id"), @Result(property = "uid",column = "uid"), @Result(property = "money",column = "money"), @Result(property = "user",column = "uid", one=@One(select = "com.changwu.dao.IUserDao.findById",fetchType = FetchType.EAGER)) }) List<Account> findAll();
XML版: 一对多级联#
实验场景: 查询用户的信息的同时级联查询出用户所有的账户的信息
在java中上面的描述转换成代码的意思是, User类中要添加一个集合,存放查询出来的账户的信息, 我们进一步通过配置告诉Mybatis将查询出的属性封装进这个list中
public class User { private Integer id; private String userName; private Date birthday; private String sex; private String address; // 在主表中唯一从表的集合 private List<Account> accounts;
像下面这样配置
注意点就是在一对多的配置中我们使用collection
标签,接着使用属性ofType
标识 一的一方维护的集合中元素的类型
像这种property
类型的属性全都是java类中的属性名,写错了MyBatis会报错
column
属性: 按理说是数据库中列名,如果不一样的话,不至于报错,但是数据一定封装不上,但是有时候 也可能是在sql语句中为原列名取的别名的名称
<!-- todo 一对多的配置 --> <resultMap id="findAllUserAndUserAccount" type="com.changwu.pojo.User"> <id property="id" column="id"/> <result property="userName" column="userName"/> <result property="birthday" column="birthday"/> <result property="sex" column="sex"/> <result property="address" column="address"/> <!--一对多的配置--> <!--ofType是一的一方维护的集合中元素的类型--> <collection property="accounts" ofType="com.changwu.pojo.Account" > <id property="id" column="aid"/> <result property="uid" column="uid"/> <result property="money" column="money"/> </collection> </resultMap> <select id="findAllUserAndUserAccount" resultMap="findAllUserAndUserAccount"> select * from user u left outer join account a on u.id=a.uid </select>
注解版: 一对多级联#
和1对1的配置很像
@Select("select * from user") @Results(id="UserAccount",value = { @Result(id = true ,property = "id",column = "id"), @Result(property = "accounts",column = "id", many = @Many(select = "com.changwu.dao.IAccountDao.findAccountByUid",fetchType = FetchType.LAZY)) }) List<User> findAll();
多对多级联#
实验场景: 典型的用户和角色之间的关系
多对多的配置其实和一对多一样, 比如想在查询出用户信息时级联查询出这个用户拥有的角色的信息
于是第一步:
我们在User类中添加属性private List<Role> roles;
第二步: 写xml中的sql mapper配置
下面这个column
属性配置的rid
其实就是在使用我们sql中为数据库中的某列取的别名
如果查询的结果中出现了两个相同的列名,但是值不同,代表的意义也不同,最好就给其中一个取别名
<!--todo 多对多 role user--> <resultMap id="roleUserMap" type="com.changwu.pojo.Role"> <!--todo 这里的colum就是不原生的数据库的列名,而是取的别名--> <id property="id" column="rid"/> <result property="roleName" column="role_name"/> <result property="roleDesc" column="role_desc"/> <collection property="users" ofType="com.changwu.pojo.User"> <id property="id" column="id"/> <result property="userName" column="userName"/> <result property="birthday" column="birthday"/> <result property="sex" column="sex"/> <result property="address" column="address"/> </collection> </resultMap> <select id="findAll" resultMap="roleUserMap"> select u.*,r.ID as rid,r.ROLE_NAME,r.ROLE_DESC from role r left join user_role ur on r.ID=ur.RID left join user u on ur.UID = u.id </select>
MyBatis的延迟加载#
association
一对一的延迟加载#
即用户和账户的关系是一对一的关系,我们希望这样,当用户仅仅查询账户信息时,Mybatis仅仅执行查询账户信息的语句,但是当用户使用这个账户关联的对象时,再让MyBatis将账户对象中的用户对象的引用时,触发懒加载,让mybatis再去查询数据库
像下面这样配置xml文件, 它其实是对原生的一对一的级联查询的升级,将association
标签内部的通过result
的属性描述全部去掉了,因为目标是懒加载,加上这些描述也用不到了
取而代之的是一个新星select
, 它指向了IUserDao
中的根据id查询用户的方法findUserById
还有一个注意点就是,association
中的column
属性不能去掉,而且必须写成数据库中Account表中存放关联User的外键的那个列名,通过它指定当触发延迟加载时,使用哪个字段给findById()
方法使用
<!--todo 延迟加载--> <resultMap id="findAllAccountLazy" type="com.changwu.pojo.Account"> <id property="id" column="id"/> <result property="uid" column="uid"/> <result property="money" column="money"/> <!-- select指定的内容, 可以查询出单个用户的唯一方法标识 --> <!-- 这里的column属性,指定的是select中指定的fingById 所需要的id值--> <association property="user" column="uid" javaType="com.changwu.pojo.User" select="com.changwu.dao.IUserDao.findById"> </association> </resultMap> <select id="findAll" resultType="int" resultMap="findAllAccountLazy"> select * from account </select>
下面是User中findById的配置, sql中的#{}
中的内容是可以随便写的
<!--todo 疑问下面这id可不可以乱写 --> <select id="findById" parameterType="int" resultMap="UserMap"> select * from user where id = #{123id} </select>
实验成功的结果就是,当我们使用Account的fingAll方法时,如果不继续getUser()
,结果控制台打印单条sql,一旦使用getUser()
,控制台会继续打印多条新的sql
collection
实现一对多的延迟加载#
一个用户存在多个账户,我们希望如果仅仅是查询用户信息则延迟加载用户账户的信息,使用用户信息时,才再次执行新的sql加载用户的信息
实现的思路和上面的相似, 注意collection
标签中的column
的值,已经select
标签中findAccountByUid
的实现
<!-- todo 一对多的配置 延迟加载 --> <resultMap id="findAllUserAndUserAccount" type="com.changwu.pojo.User"> <id property="id" column="id"/> <result property="userName" column="userName"/> <result property="birthday" column="birthday"/> <result property="sex" column="sex"/> <result property="address" column="address"/> <!--一对多的配置--> <!--ofType是一的一方维护的集合中元素的类型--> <collection property="accounts" ofType="com.changwu.pojo.Account" column="id" select="com.changwu.dao.IAccountDao.findAccountByUid"> </collection> </resultMap> <select id="findAllUserAndUserAccount" resultMap="findAllUserAndUserAccount"> select * from user </select>
缓存#
什么是缓存?#
缓存是指将查询的数据暂存在内存中,当下次再次查询时,直接从内存中获取,实现减少和数据库交互的次数,提高执行效率
适用于缓存的数据: 经常被查询, 不经常被修改, 而且对此类数据的一致性没有很严格的要求, 与之相反的数据比如,银行的汇率,商品的库存中数据一致性要求极其严格的数据就不适合使用缓存机制
Mybatis中的一级缓存#
一级缓存是存在于MyBatis的SqlSession中的数据,当用户执行一次查询,查询的结果就会被缓存在SqlSession中一份,当用户再次查询时,先检查sqlSession中是否存在相应的数据,如果存在的话不再重新查询数据库,而是取出缓存中的数据给用户
所以,当sqlSession对象消失时,一级缓存就不复存在
一级缓存是默认存在的
像下面这个测试,全程使用同一个sqlSession,那么获取出来的user也会是同一个, 控制台仅仅输入一条sql,打印结果也为true
@Test public void testFirstCache(){ IUserDao userDao = this.sqlSession.getMapper(IUserDao.class); User user1 = userDao.findUserByIdFirstCache(42); User user2 = userDao.findUserByIdFirstCache(42); System.out.println(user1==user2); }
但是像下面这样,一级缓存将会消失
public void testFirstCache(){ IUserDao userDao = this.sqlSession.getMapper(IUserDao.class); User user1 = userDao.findUserByIdFirstCache(42); this.sqlSession.close(); this.sqlSession = this.factory.openSession(); userDao = this.sqlSession.getMapper(IUserDao.class); User user2 = userDao.findUserByIdFirstCache(42); System.out.println(user1==user2); }
同样适用sqlSession的clearCache()
也可以实现缓存的清空
为了安全,MyBatis的一级缓存在sqlSession出现 修改,添加,删除,commit(),close()等方法时,就会被清空
MyBatis中的二级缓存#
二级缓存指的是MyBatis中的SqlSessionFactory对象的缓存,由同一个SqlSessionFactory对象创建的SqlSession会共享这块二级缓存
使用: 在MyBatis主配置文件中开始支持缓存的配置,默认是开启的状态
<setting name="cacheEnabled" value="true"></setting>
在从配置文件中开启缓存的配置
<!--为当前的mapper开启二级缓存的支持--> <cache/>
第三步: 在select
标签中添加userCache
属性
<!--测试一级缓存--> <select id="findUserByIdFirstCache" parameterType="int" resultMap="UserMap" useCache="true"> select * from user where id = #{id} </select>
测试:
按照上面的配置后,编写下面的测试方法,测试二级缓存的存在就得关闭一级缓存,在下面的测试用例中同时开启两个sqlSession,第一个sqlSession查询完结果后随即关闭,接着开启第二个sqlSession,获取mapper继续查询,但是整个流程查询的sql仅仅会执行一次,原因就是存在二级缓存, 为什么最后的输出结果user!=user2呢? 因为属于SqlSessionFactory的二级缓存,存放的并不是对象,而是键值对形式存在的数据,使用这部分缓存时,MyBatis会自动为我们创新的对象,然后将这部分数据封装进去,返回这个新对象
@Test public void testFirstCache(){ SqlSession sqlSession1 = this.factory.openSession(); IUserDao userDao = sqlSession1.getMapper(IUserDao.class); User user1 = userDao.findUserByIdFirstCache(42); System.out.println(user1); sqlSession1.close(); SqlSession sqlSession2 = this.factory.openSession(); userDao = sqlSession2.getMapper(IUserDao.class); User user2 = userDao.findUserByIdFirstCache(42); System.out.println(user2); sqlSession2.close(); System.out.println(user1==user2); }
注解版开启二级缓存#
在我们的dao层上添加如下注解即可
@CacheNamespace(blocking = true)