从零一起学Spring Boot之LayIM项目长成记(三) 数据库的简单设计和JPA的简单使用。
前言
今天是第三篇了,上一篇简单模拟了数据,实现了LayIM页面的数据加载。那么今天呢就要用数据库的数据了。闲言少叙,书归正传,让我们开始吧。
数据库
之前有好多小伙伴问我数据库是怎么设计的。我个人用关系型数据库比较多,一般就是根据业务来分析,一对一的关系,一对多的关系,多对多的关系等,那么对于LayIM就根据这几个关系出发。而且先根据业务来设计。它初始化的数据我们都见过了,数据中分别包含以下四个部分。
- 个人用户信息
- 好友分组信息
- 群组信息
大部分业务都是围绕着用户转的,那么我们一条一条的分析。首先,个人用户信息不必说,这里我们就根据LayIM所需的用户字段设计就好,另外加上createAt字段或者其他字段都行。很简单,用户表SQL如下:
CREATE TABLE `user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `avatar` varchar(255) DEFAULT NULL, `sign` varchar(255) DEFAULT NULL, `user_name` varchar(255) DEFAULT NULL, `create_at` bigint(20) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=984389 DEFAULT CHARSET=utf8;
好友分组信息,一个用户可以拥有多个分组,而每个分组下又可以有多个好友。 所以,维护好友关系需要两张表,分组表和用户(好友)分组关系表
CREATE TABLE `friend_group` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `create_at` bigint(20) DEFAULT NULL, `name` varchar(255) DEFAULT NULL, `uid` bigint(20) DEFAULT NULL, PRIMARY KEY (`id`), KEY `FKawe6k1o9mujam11lxiwcwbqpu` (`uid`), CONSTRAINT `FKawe6k1o9mujam11lxiwcwbqpu` FOREIGN KEY (`uid`) REFERENCES `user` (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=770369 DEFAULT CHARSET=utf8;
群组信息,一个用户可以有多个群,一个群内有多个用户,他们是多对多的关系。所以维护这个关系需要两张表。群组表和群员关系表。
CREATE TABLE `big_group` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `create_at` bigint(20) DEFAULT NULL, `avatar` varchar(255) DEFAULT NULL, `create_uid` bigint(20) DEFAULT NULL, `description` varchar(255) DEFAULT NULL, `group_name` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`), KEY `FKdw1jlswinwe6gas1tqk1trgia` (`create_uid`), CONSTRAINT `FKdw1jlswinwe6gas1tqk1trgia` FOREIGN KEY (`create_uid`) REFERENCES `user` (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
剩下的两个表没有写,分别是 (分组,用户)关系表,(群组,用户)关系表。 没错,他们都是多对多的关系。你可以在张三的好友列表里,也可以在李四的好友列表。同理,你可以在多个群里。
JPA-表的创建
上面简单介绍了表的结构,虽然写出了SQL语句,但是呢在项目里我们并不会手动创建表。因为JPA会帮我们实现自动创建。如下图,我使用的是update,(即实体类变化等会重新修改表结构)
下面就用户表写一个对应的类。(例)
//加上@Entity注解 @Entity public class UserInfo { //主键注解 @Id @GeneratedValue @Column(nullable = false) private long id; //列 可以自定义列名,长度等,或者是否为空 @Column(name = "user_name",length = 20) private String name; }
运行程序,查看一下表,已经成功创建,没有问题。
但是只是简单的表还不够,因为我们知道,表之间是有关系的,于是JPA中的关系注解就派上用场了。
JPA-关系的映射
之所以在上文中提到了一对多,多对多等词,其实就是为了在这里具体解释。JPA提供了关系注解。@OneToMany,@OneToOne,@ManyToOne,@ManyToMany等。下面我会根据LayIM所需要的关系一一讲解。
首先,一个用户对应多个好友分组。而一个分组只属于某个(当前)用户。这里可以理解为用户与好友分组的关系是一对多,即 @OneToMany。代码中是这样体现的。
//一对多,一个用户有多个好友分组 @OneToMany(mappedBy = "user") private List<FriendGroup> friendGroups;
在FriendGroup类中
@ManyToOne @JoinColumn(name = "uid",nullable = false) private User user;
其中呢,name ="uid" 代表,FriendGroup表中的uid字段对应的是User表中的主键(id)字段。也就是说,当我们查询一个User实体时,friendGroups字段会带上该用户下的好友分组。当然其中还涉及到了懒加载机制。(具体我也没研究,大体就是说,当调用 getFriendGroups()方法的时候才会根据关系从数据库查询相应的结果,这个后边会有体现)
其次,LayIM加载中会把当前用户的群组信息直接加载出来。这里我们也可以直接用 @OneToMany实现,原理同上。(刚开始我做错了,我只是列出了该用户创建的群,没有考虑到用户加入的群)
第三,@OneToOne 。这个注解呢在获取好友分组下的好友列表中用到了。比如,一个好友分组对应多个好友关系。那么每个好友关系从当前登录者的角度来说就是一对一的。这句话要怎么理解呢?就是说从程序上来讲,我们知道每个分组ID对应的多个UserId,因为表中存的是Id,但是我们取数据的时候需要把该用户的其他信息拿出来,那么使用@OneToOne注解,就会帮我们自动关联查询。代码如下:
@OneToOne @JoinColumn(name="friend_uid") private User friend;
代码中的friend_uid字段即用户id。(因为User类中的主键已经确定,所以不用再User类中增加 @OneToOne注解)
代码实现
(2017-11-7 日晚:此段代码已重构,为了写出我当时的思路,下文内容不会删掉,重构后代码讲解:http://www.cnblogs.com/panzi/p/7793854.html)
说了这么多,想必大家都有点晕了,下面就是代码实现了。在这里呢,我将拿好友分组举例,这个最简单。写一个单元测试,如下:
public User getUser(Long userId){ return userRepository.findOne(userId); }
@Test public void testGetUserFriendGroups() { User user = userService.getUser(100000L); System.out.println("用户好友分组的个数为:"+user.getFriendGroups().size()); }
运行结果:
小 tips
当你做单元测试的时候,会遇到懒加载没有Session的问题,如果遇到了,在字段上面添加 FetchType.EAGER即可
@OneToMany(mappedBy = "user",fetch = FetchType.EAGER)
但是程序运行可以不用加这个,可以在配置文件中增加open-in-view :true 解决
JPA默认方法
在讲解具体实现之前呢,还要介绍一下JpaRepository这个接口,这个玩意挺好玩的,除了默认的方法,还可以自定义比如 findBy,getBy,findBy..OrderBy..等等方法。这里只介绍当前用到的,至于没有用到的,有兴趣的同学可以自行研究。代码很简单,新建一个UserRepository继承自JpaRepository即可
public interface UserRepository extends JpaRepository<User,Long> { }
这样写了之后,UserRepository默认会有一些方法,增删改查或者分页。这里呢,我们没有用到分页,只是简单的查询。没错就是 findOne方法
LayIM init接口实现
好了,讲了这么多,可能大家都云里雾里的,没有关系,下面就进入LayIM init接口实现部分。当然作为初学者来说,我就使用了笨方法,为什么说笨方法呢,方法思路如下:
方法笨就笨在里面的for循环。(应该是有优化的,后期在看)
for(FriendGroup friendGroup : friendGroups) { List<RelationShip> relationShips = friendGroup.getRelationShips(); List<UserViewModel> userViewModels = new ArrayList<UserViewModel>(); //遍历 relationShips 获取userViewModels的集合 for(RelationShip relationShip : relationShips){ UserViewModel userViewModel = LayimMapper.INSTANCE.mapUser(relationShip.getFriend()); userViewModels.add(userViewModel); } //获取主键 Long friendGroupId = friendGroup.getId(); for (FriendGroupViewModel viewModel : friends) { if (viewModel.getId().equals(friendGroupId)) { viewModel.setList(userViewModels); } } }
代码当中我使用了 LayimMapper,他依赖于一个实体转换工具:MapStruct。有兴趣大家自行去官网看看。不过我个人觉得有点类似代码生成器,因为在编译过后,target里面会多出一个类来。
其实还是挺方便的,毕竟自己写也是这么写,这样还大大节约了时间。
好了,本篇内容啰嗦了一大堆,我也不知道讲了些啥,总之就是我在学习之中的一些问题和开发思路。知识点很多,所以在我这里介绍的都是皮毛,要深追究下去,每个知识点都可以写上几篇。最后看一下运行效果。接口同上一篇的一样。
对应数据库:
后台执行的语句:
总结
本篇到此为止就结束啦,Jpa部分讲解的比较少,决定下一篇重点学习记录一下。这个基础接口实现了。LayIM还有一个getMembers接口,你是不是也会了呢?在我学习的过程中,包括关系注解,MapStruct应用和其他实践上自己给自己挖了很多坑,也出现过很多莫名奇妙的问题,有些东西自己动手了才知道。愉快的周末结束啦。我们下一篇见。
下篇预告:从零一起学Spring Boot之LayIM项目长成记(四) Spring Boot JPA 深入了解