一对多
最近在写一个博客项目,数据库表的建立要考虑多表的问题,记录下学习所得!
CRUD create read update delete
实体类的设计:
Article: articleId title content createDate author(User类型 多对一关系维护端) category(Category类型 多对一关系维护端)
Category: categoryId name displayName articles(Set<Article>类型 一对多)
Comments: commentId comment article(Article类型-评论对应的文章 多对一关系维护端)
User: uid username password sex birthday phone email address discription(个人介绍) roles(Set<Role> 多对多)
Role: rid name discription users(Set<User> 多对多)
希望达到的目的,加载文章的时候可以加载出全部的评论(评论可有可无,但评论不能单独存在,必须有对应的文章),可以根据分类直接查询对应的所有文章,文章必须分类,但用户可以增加新的分类,在删除用户的时候,角色表不能动,暂时想到这么多。。。
准备测试多对多关系时,又有了新的想法,我希望可以单独添加角色,但是添加用户时必须要有相应的角色,默认的角色为游客,一个用户可以有一到多个角色,一个角色可以没有用户,而不同的角色又有不同的权限,权限包括添加、删除、修改,游客只能浏览,普通用户可以增删改自己的博客,管理员具备管理普通用户的权限,超级管理员具备管理管理员的权限。。。
实体类:
Article.java
@Entity //指定一个实体类
@Table(name = "t_article") //指定表名
public class Article {
@Id //指定主键
@GenericGenerator(name = "generator",strategy = "native") //声明主键生成策略
@generatedValue(generator = "generator") //设定主键生成策略
@Column(name = "article_id") //类中的属性和表中的列名的对应关系,默认为表字段名与实体类属性名一致
private Long articleId;
private String title;
@Column(columnDefinition = "text")
private String content;
@Column(columnDefinition = "text")
private String summary;
@Manytoone //指定多对一的关系
@JoinColumn(name="author_id") //设置外键
private User author; //一篇文章对应于一个作者,所以这里不用集合
@Column(name = "create_date")
private String createDate; //创建日期
@ManyToOne
@JoinColumn(name = "category_id") //不管是一对多还是多对一,都让多的一方维护关系
private Category category; //表示多篇博客属于同一个分类
@OneToMany(cascade=CascadeType.ALL,mAPPedBy = "article")
@orderby(value = "commentId asc") //按id升序排列
private Set<Comments> comments; //一篇文章对应多个评论,加载文章的时候能加载出评论
//这里省略getter和setter方法
}
Category.java
@Entity
@Table(name = "t_category")
public class Category {
@Id
@GenericGenerator(name = "generator",strategy = "native")
@GeneratedValue(generator = "generator")
@Column(name="category_id")
private Long categoryId;
private String name;
@Column(name = "display_name")
private String displayName;
@OneToMany(cascade=CascadeType.ALL,mappedBy = "category")
@OrderBy(value = "articleId asc ")
private Set<Article> articleSet = new HashSet<>();
//省略getter和setter
}
Comments.java
@Entity
@Table(name = "t_comments")
public class Comments {
@Id
@GenericGenerator(name = "generator", strategy = "native")
@GeneratedValue(generator = "generator")
@Column(name = "comment_id")
private Long commentId;
@ManyToOne(cascade = CascadeType.ALL,optional = false) //option=false表示文章不能不存在
@JoinColumn(name="article_id")
private Article article; //评论对应的文章
@Column(columnDefinition = "text")
private String comment; //评论内容
//省略getter和setter
}
User.java
@Entity
@Table(name = "t_user")
public class User {
@Id
@GenericGenerator(name = "generator", strategy = "native")
@GeneratedValue(generator = "generator")
private Long uid;
@Column(name = "username",nullable = false)
private String username;
@Column(name = "password",nullable = false)
private String password;
@Column(name = "email", nullable = false)
private String email;
private String sex;
private String birthday;
private String address;
private String phone;
@Column(columnDefinition = "text")
private String description; //用户的个人简介
@ManyToMany
//两者的关系表,由两者的主键ID组成,joinColumns指定主表的外键,inverseJoinColumns指定匹配表的外键
@jointable(name = "tb_user_role", joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id"))
private Set<Role> roles = new HashSet<>(); //用户拥有的不同角色
//省略getter和setter
}
Role.java
@Entity
@Table(name = "t_role")
public class Role {
@Id
@GenericGenerator(name = "generator",strategy = "native")
@GeneratedValue(generator = "generator")
private Long rid;
private String name;
private String discription; //角色描述
@ManyToMany(mappedBy="roles")
private Set<User> users = new HashSet<>();
//省略getter和setter
}
建立测试类,hibernate自动为我们建立了表,查看生成的数据库表(以及自动生成的sql语句):
我们再来具体看看我们的实体类,这里有很多的细节可以分析:
配置持久化类:
配置关联关系
单向关联就是只在一端配置注解,而双向关联是指在两端都配置注解
单向多对一:article->user 由多端维护关系
双向多对一:
article->category
article->comments
解释下:参考这里
@OneToMany的属性:
1>targetEntity 定义关系类的类型,默认是该成员属性对应的类类型,所以通常不需要提供定义。
2>mappedBy 定义类之间的双向关系。如果类之间是单向关系,不需要提供定义,如果类和类之间形成双向关系,我们就需要使用这个属性进行定义,否则可能引起数据一致性的问题。
3>cascade 该属性定义类和类之间的级联关系。定义的级联关系将被容器视为对当前类对象及其关联类对象采取相同的操作,而且这种关系是递归调用的。举个例子:Article 和Comments有级联关系,那么删除Article时将同时删除它所对应的所有Comments对象。而如果Comments还和其他的对象之间有级联关系,那么这样的操作会一直递归执行下去。cascade的值只能从CascadeType.persist(级联新建)、CascadeType.REMOVE(级联删除)、CascadeType.REFRESH(级联刷新)、CascadeType.MERGE(级联更新)中选择一个或多个。还有一个选择是使用CascadeType.ALL,表示选择全部四项。
4>fatch 可选择项包括:FetchType.EAGER和FetchType.LAZY。前者表示关系类(本例是Comments 类)在主类(本例是Artice类)加载的时候同时加载,后者表示关系类在被访问时才加载。默认值是FetchType.LAZY。
@ManyToOne注释有四个属性:targetEntity、cascade、fetch 和optional,前三个属性的具体含义和@OneToMany的同名属性相同,但@ManyToOne的fetch 属性默认值是FetchType.EAGER。
optional属性是定义该关联类是否必须存在,默认值是true,当值为false 时,关联类双方都必须存在,如果关系维护端(Comments)不存在,查询的结果为null。值为true 时, 关系维护端可以不存在(Comments可以为空),查询的结果仍然会返回关系被维护端(Article),在关系被维护端中(一的一端)指向关系维护端(多的一端)的属性为null。属性实际上指定关联类与被关联类的join 查询关系,如optional=false 时join 查询关系为inner join, optional=true 时join 查询关系为left join。
@JoinColumn 指定外键名称,在维护关系的一方加入
问题:1、为什么不管是多对一还是一对多,都在多的一方维护外键?
引用这里的例子
一个组中有多个用户,一个用户只能属于一组。用户和组之间就是一个多对一的关系的。如下图
这个关系我们要怎样维护呢?我们想象一下,假如在一的一端维护关系,即在group一端加一个字段userId来标识学生。那设计出来的表格存储数据是这个样子的。
不解释,直接看在多的一端维护关系
不用说,大家就知道在多的一端维护数据冗余要少的多。怎么来解释这个问题呢?大家想一下是多的记少的容易记,还是少的记多的容易记呢?举个例子员工和老板。你说是老板记员工比较容易还是员工记老板比较容易呢?很明显记少的比较容易啊,能维护二者的关系也能减少工作量。hibernate当然也是这么做的。
一对多的关联最后生成的表格与多对一是一样的。但是他们到底有什么区别呢?多对一的维护关系是多指向一的关系,有了此关系,在加载多的时候可以将一加载上来。即我们查询用户的时候,组也被查询出来了。而一对多的关系,是指在加载一的时候可以将多加载进来。即查询组的时候,用户也被查出来了。他们适用于不同的需求。
不管是一对多还是多对一,都是在多的一端维护关系。从程序的执行状况来解释一下这样做的原因。若在一的一端维护关系,多的一端User并不知道Group的存在。所以在保存User的时候,关系字段groupId是为null的。如果将该关系字段设置为非空,则将无法保存数据。另外因为User不维护关系,Group维护关系,则在对Group进行操作时,Group就会发出多余的update语句,去维持Group与User的关系,这样加载Group的时候才会把该Group对应的学生加载进来。可见一对多单向关联映射是存在很大的问题的。那怎么解决这些问题呢?那就是使用一对多双向关联。
2、为什么Hibernate的OneToMany用Set集合而不用List?
首先,你要清楚List和Set的区别:List是有序和可重复;Set是无序和不可重复的。思考一个问题:将一个对象放在一个List中,在将这个对象的属性改变,再放入这个List中,这个List中的对象属性是不是一样的呢?
我们新建一个Employee类:
@component
public class Employee {
private String name;
private integer age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "Employee{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
测试:
@Autowired
private Employee employee;
@Test
public void test2() {
List<Employee> employees = new ArrayList<>();
Set<Employee> employeeSet = new HashSet<>();
employee.setName("小明");
employee.setAge(18);
employees.add(employee); //添加进List
employeeSet.add(employee); //添加进Set
employee.setName("小红");
employee.setAge(17);
employees.add(employee); //修改后添加进List
employeeSet.add(employee); //修改后添加进Set
System.out.println("我在List中");
for(Employee e : employees){
System.out.println(e);
}
System.out.println("我在Set中");
for(Employee e : employeeSet){
System.out.println(e);
}
}
最后的输出结果是:
原因是改变前后的employee对象始终指向同一个内存区域!
在一对多关联中,想象一下这种情况:你要更新从表记录,从List中得到从表的一个对象引用,然后你对这样对象修改后又放回List,你的List中就包括两个从表对象的引用。你再保存这两个引用,你觉得会保存两次还是一次?当然是两次!而如果是Set,你得到从表对象的引用之后修改从表对象的内容,再往Set里面放,此时Set里面因为已经有了这个从表对象的引用,就不会再给里面加。这样,你的Set里面还是只有一个从表对象的引用。所以你保存的话,这个从表对象只保存一次。
既然Set是无序的,那么我们怎么保持顺序呢,这里就用到了@OrderBy注解,默认是asc升序,也可以修改为desc降序,支持多个条件进行排序,用‘,’隔开。
最后我们再来看看多对多:user->role
@ManyToMany
//两者的关系表,由两者的主键ID组成,joinColumns指定主表的外键,inverseJoinColumns指定匹配表的外键
@JoinTable(name = "tb_user_role", joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id"))
private Set<Role> roles = new HashSet<>(); //用户拥有的不同角色
@ManyToMany(mappedBy="roles")
private Set<User> users = new HashSet<>();
@ManyToMany注释表示此类是多对多关系的一端,@mapperBy表示此类放弃维护关联关系,而@JoinColumn注解的都是在“主控方”,(这里表明user是双向关系的维护端)注意:mappedBy 属性的值为此关系的另一端的属性名。@JoinTable 描述了多对多关系的数据表关系。name 属性指定中间表名称,joinColumns 定义中间表与主表的外键关系 ,代码中,中间表的user_id列是User表的主键列对应的外键列,inverseJoinColumns 属性定义了中间表与另外一端(Role)的外键关系。
值得注意的是,以上定义中删除角色(实际上此时是无法单独删除角色的,见后面的测试)是无法级联删除中间表的。如果两个实体类都配置了joinColumns和inverseJoinColumns属性,并且位置互相调换,就可以使用双向维护。比如你此时删除角色时,角色会删除角色与用户的关系,也会删除角色中与用户的关系表。
@ManyToMany
@JoinTable(name = "tb_user_role", joinColumns = @JoinColumn(name = "role_id"),
inverseJoinColumns = @JoinColumn(name = "user_id"))
private Set<User> users = new HashSet<>();
好啦,让我们来测试下!
双向一对多添加操作给文章(主动方)设置分类
/**
* @Description: 测试一对多添加操作
* @Param: []
* @return: void
* @Date: 2018/8/25
*/
@Test
public void test(){
session session = null;
transaction tx = null;
try{
session = sessionfactory.openSession(); //需要手动关闭session,实际项目中使用getCurrentSession
tx = session.beginTransaction(); //开启事务
//CRUD操作
Article article = new Article();
article.setTitle("mapreduce功能实现");
article.setSummary("Hadoop的版本0.20.0包含有一个新的java MapReduce API...");
article.setContent("Hadoop的版本0.20.0包含有一个新的java MapReduce API,有时也称为\"上下文对象\"(context object),旨在使API在今后更容易扩展。新的API 在类型上不兼容先前的API,所以,需要重写以前的应用程序才能使新的API发挥作用。");
article.setCreateDate("2017年07月25日 10:36:32");
//设置分类
Category category = new Category();
category.setCategoryId(2L); //与id有关,与session无关,托管态
article.setCategory(category);
session.save(article);
tx.commit();
}catch (HibernateException e){
if(tx != null)
tx.rollback();
}finally {
session.close();
}
}
一对多添加 给分类(主动方)添加文章
@Test
public void test2(){
Session session = null;
Transaction tx = null;
try{
session = sessionFactory.openSession(); //需要手动关闭session,实际项目中使用getCurrentSession
tx = session.beginTransaction(); //开启事务
//CRUD操作
Category category = new Category();
category.setName("other");
category.setDisplayName("其他");
//添加文章
Article article1 = new Article();
article1.setTitle("何凯文考研英语每日一句整理三");
article1.setSummary("If I had to single out a particular bias as the most pervasive and damaging...");
article1.setContent("If I had to single out a particular bias as the most pervasive and damaging, it would probably be confirmation bias.");
article1.setCreateDate("2018年08月10日 16:26:32");
Article article2 = new Article();
article2.setTitle("何凯文考研英语每日一句整理二");
article2.setSummary("For all our efforts to flatten, pile and stuff box...");
article2.setContent("For all our efforts to flatten, pile and stuff boxes into recycling bins, consumers aren’t that good at recycling cardboard.");
article2.setCreateDate("2018年08月22日 12:30:44");
//设置对应关系
article1.setCategory(category);
article2.setCategory(category);
category.getArticleSet().add(article1);
category.getArticleSet().add(article2);
session.save(category);
tx.commit();
}catch (HibernateException e){
if(tx != null)
tx.rollback();
}finally {
session.close();
}
}
一对多查询操作
/**
* @Description: 一对多查询操作
* @Param: []
* @return: void
* @Date: 2018/8/25
*/
@Test
public void test3(){
Session session = null;
Transaction tx = null;
try{
session = sessionFactory.openSession(); //需要手动关闭session,实际项目中使用getCurrentSession
tx = session.beginTransaction(); //开启事务
//CRUD操作
Category category = session.get(Category.class,1L); //根据id获取分类
System.out.println("分类名称:"+category.getDisplayName());
for(Article a : category.getArticleSet()){
System.out.println("文章标题:"+a.getTitle());
}
System.out.println("===================================");
Article article = session.get(Article.class,2L);
System.out.println("文章分类:"+article.getCategory().getDisplayName());
tx.commit();
}catch (HibernateException e){
if(tx != null)
tx.rollback();
}finally {
session.close();
}
}
如果我把分类删了,对应的文章会删除吗?测试级联删除
/**
* @Description: 一对多删除,删除分类,分类下的文章是否会删除?会!!!
* @Param: []
* @return: void
* @Date: 2018/8/25
*/
@Test
public void test4(){
Session session = null;
Transaction tx = null;
try{
session = sessionFactory.openSession(); //需要手动关闭session,实际项目中使用getCurrentSession
tx = session.beginTransaction(); //开启事务
//CRUD操作
Category category = session.get(Category.class,10L);
session.delete(category);
tx.commit();
}catch (HibernateException e){
if(tx != null)
tx.rollback();
}finally {
session.close();
}
}
给文章添加评论,这里值得注意的是,由于之前我设置了option=false;所以文章为空时,是无法添加评论的,给文章添加评论,必须设置对应的文章,(这点与category为null时,可以添加文章是不同的,默认的option为true)那么要不要设置article.getComments().add()操作呢,答案是不必这么做,因为评论(多的一端)是维护方,article是根据articleId在comments中查询对象的。
/**
* @Description: 测试给文章添加评论
* @Param: []
* @return: void
* @Date: 2018/8/25
*/
@Test
public void test5(){
Session session = null;
Transaction tx = null;
try{
session = sessionFactory.openSession(); //需要手动关闭session,实际项目中使用getCurrentSession
tx = session.beginTransaction(); //开启事务
//CRUD操作
Article article = session.get(Article.class,12L);
//设置评论
Comments comment1 = new Comments();
comment1.setComment("凯文大帝");
Comments comment2 = new Comments();
comment2.setComment("相信凯文!");
// //给文章添加评论,这一步是多余的,因为article会根据id,直接从comment中读取对象
// article.getComments().add(comment1);
// article.getComments().add(comment2);
//必须给评论添加对应的文章,评论是维护关系的一方
comment1.setArticle(article);
comment2.setArticle(article);
session.save(comment1);
session.save(comment2);
tx.commit();
}catch (HibernateException e){
if(tx != null)
tx.rollback();
}finally {
session.close();
}
}
多对多 添加
由于设置的维护方是user,所以session只需管理user就行了
我添加了级联配置,这样当role为瞬时态时,也可以成功插入,否则的话,如果role是瞬时态,就无法插入成功,只有当role为持久态时,才可以成功插入
@Cascade(org.hibernate.annotations.CascadeType.SAVE_UPDATE)//设置级联保存和更新
//以下属性为非空字段,必须设置,否则无法保存记录
User user = new User();
user.setUsername("Daniel");
user.setPassword("123456");
user.setEmail("[email protected]");
//设置瞬时态对象,要想插入成功必须在user中设置级联
Role role = new Role();
role.setName("admin");
role.setDescription("管理员");
user.getRoles().add(role);//添加角色
session.save(user);
看看生成的sql语句
可知,会添加用户,角色,以及在中间表建立联系
如果想给新的用户添加已有的角色呢?角色表已经有了角色,用户表还没有用户,中间表关系尚未建立
//以下属性为非空字段,必须设置,否则无法保存记录
User user = new User();
user.setUsername("Yummy");
user.setPassword("666666");
user.setEmail("[email protected]");
Role role = session.get(Role.class,4L); //持久态对象
user.getRoles().add(role);//添加角色
session.save(user);
看看hibernate为我们做了啥
可以单独添加角色吗?
Role role = new Role();
role.setName("user");
role.setDescription("普通用户");
session.save(role);
可以看出,这是可以的,原因也很简单,role不是维护关系的一方
那么不设置role,可以单独添加用户吗?测试下
User user = new User();
user.setUsername("Yummy");
user.setPassword("666666");
user.setEmail("[email protected]");
session.save(user);
然而,结果表明是还是可以单独添加用户的。
最后,可以根据role,添加用户吗?
我在role中添加了级联配置:
@Cascade(org.hibernate.annotations.CascadeType.SAVE_UPDATE)//设置级联保存和更新
User user = new User();
user.setUsername("Yummy");
user.setPassword("666666");
user.setEmail("[email protected]");
Role role = new Role();
role.setName("user");
role.setDescription("普通用户");
role.getUsers().add(user); //给角色添加用户
session.save(role); //保存角色
也就是说,hibernate为我们添加了用户和角色(因为我添加了级联配置,所以插入role的同时,也插入了user,如果没有设置级联,只会添加role),但是它没有去维护中间表,没有给用户和角色建立关联。
再来测试下删除:
//测试删除user
User user = session.get(User.class,3L);
session.delete(user);
先从中间表中删除关系,再来删除用户,OK
//测试删除role
Role role = session.get(Role.class,6L);
session.delete(role);
从结果可以看出,出错啦,role无法维护中间表,所以删除错误,也无法删除角色
现在我们希望role和user两边都可以维护关系,可以将role改为如下配置:
@ManyToMany
@Cascade(org.hibernate.annotations.CascadeType.SAVE_UPDATE)
@JoinTable(name = "t_user_role",joinColumns = @JoinColumn(name = "role_id"),
inverseJoinColumns = @JoinColumn(name = "user_id"))
private Set<User> users = new HashSet<>();
再来测试删除role
删除成功!
多对多查询
//根据角色查拥有该角色的用户
Role role = session.get(Role.class,11L);
for(User u : role.getUsers()){
System.out.println(u.getUsername());
}
//根据用户查询所拥有的角色
User user = session.get(User.class,9L);
for(Role r : user.getRoles()){
System.out.println(r.getDescription());
}
我们来测试下给文章设置作者
Article article = session.get(Article.class,2L);
User author = session.get(User.class,1L);
article.setAuthor(author);
推荐一个关于hibernate写的不错的博客这里
把多对多分成两个一对多
Role.java
@OneToMany(targetEntity = Rolepermission.class,cascade = CascadeType.ALL,mappedBy = "role") //注意这里映射的是role
private Set<RolePermission> rolePermissions = new HashSet<>();
Permission.java
@Entity
@Table(name = "t_permission")
public class Permission {
@Id
@GenericGenerator(name = "generator",strategy = "native")
@GeneratedValue(generator = "generator")
@Column(name = "permission_id")
private Integer pid;
private String name;
private String description; //权限描述
@OneToMany(targetEntity = RolePermission.class,cascade = CascadeType.ALL,mappedBy = "permission") //targetEntity默认是该属性对应的类型,可不写,注意这里映射的是permission
private Set<RolePermission> rolePermissions = new HashSet<>();
//省略getter和setter
}
RolePermission.java
@Entity
@Table(name = "t_role_permission")
public class RolePermission {
@Id
@GenericGenerator(name="generator",strategy = "native")
@GeneratedValue(generator = "generator")
private Long id;
@ManyToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "role_id")
private Role role;
@ManyToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "permission_id")
private Permission permission;
//省略getter和setter
}
相关阅读
1. 什么是hqlHQL是Hibernate Query Language的缩写2. hql和sql区别/异同 HQL SQL 类名/属性 表名/列名 区分大小写,关键
在关系型数据库中,通过外键将表跟表之间联系在了一起。一对多关系一个班级有很多学生,外键维护在学生的一方,也就是多的一方。(在做页
Hibernate Criteria多表查询Hibernate有多种查询方式,如果想要多表查询,有3种方法1.HQL查询(写连接语句)2.Native SQL(手写原生SQL语句
通俗解释Hibernate中SessionFactory和Session是否是线
******************************************************************* SessionFactory接口负责Hibernate的初始化和建立Sessio
前阵有个应用程序项目需要连接数据库mysql,想着单纯写sql语句做CRUD太麻烦了,就想起了ORM技术,简单点说就是把数据库的表映射成对象,