Spring Data JPA的一对一、LazyInitializationException异常、一对多、多对多操作

 Spring Data JPA系列

1、SpringBoot集成JPA及基本使用

2、Spring Data JPA Criteria查询、部分字段查询

3、Spring Data JPA数据批量插入、批量更新真的用对了吗

4、Spring Data JPA的一对一、LazyInitializationException异常、一对多、多对多操作

5、Spring Data JPA自定义Id生成策略、复合主键配置、Auditing使用

6、【源码】Spring Data JPA原理解析之Repository的自动注入(一)

7、【源码】Spring Data JPA原理解析之Repository的自动注入(二)

8、【源码】Spring Data JPA原理解析之Repository执行过程及SimpleJpaRepository源码

9、【源码】Spring Data JPA原理解析之Repository自定义方法命名规则执行原理(一)

10、【源码】Spring Data JPA原理解析之Repository自定义方法命名规则执行原理(二)

11、【源码】Spring Data JPA原理解析之Repository自定义方法添加@Query注解的执行原理

12、【源码】SpringBoot事务注册原理

13、【源码】Spring Data JPA原理解析之事务注册原理

14、【源码】Spring Data JPA原理解析之事务执行原理

15、【源码】SpringBoot编程式事务使用及执行原理

16、【源码】Spring事务之传播特性的详解

17、【源码】Spring事务之事务失效及原理

18、【源码】Spring Data JPA原理解析之Hibernate EventListener使用及原理

19、【源码】Spring Data JPA原理解析之Auditing执行原理

20、Spring Data JPA使用及实现原理总结

前言

通过前三篇Spring Data JPA的博文,相信大家对JPA有了一定的了解,然而前面的文章都只介绍单个表,这一篇,将给大家分享一下多表关系的定义及相关操作。在开始介绍之前,先了解一些理论知识。

CascadeType

JPA框架中的cascade属性用于指定实体之间的级联操作。级联操作是指当一个实体的状态发生改变时,关联的其他实体是否同时发生改变。简单理解:cascade用于设置当前实体是否能够操作关联的另一个实体的权限。

cascade的取值为CascadeType枚举类型

1)CascadeType.PERSIST:持久化操作时会级联执行(cascade是用于设置实体的权限,所以对应的持久化操作也是针对实体对象的持久化,对应EntityManager的persist()方法操作,而如果是通过JPQL、HQL或Sql实现的持久化,则不会级联。下同。);

2)CascadeType.MERGE:合并(更新)操作时级联执行;

3)CascadeType.REMOVE:删除操作时级联执行;

4)CascadeType.REFRESH:刷新操作时级联执行;

5)CascadeType.DETACH:级联脱管/游离操作,如果要删除一个实体,但是它有外键无法删除,需要这个级联权限。它会撤销所有相关的外键关联;

6)CascadeType.ALL:上面的5种操作都会级联执行;

当通过注解来映射持久化类时,如果希望使用底层Hibernate的一些级联特性,还可以使用CascadeType类的一些常量,例如:

1)org.hibernate.annotations.CascadeType.LOCK:当通过底层Session的lock()方法把当前游离对象加入到持久化缓存中时,会把所有关联的游离对象也加入到持久化缓存中;

2)org.hibernate.annotations.CascadeType.REPLICATE:当通过底层Session的replicate()方法复制当前对象时,会级联复制所有关联的对象;

3)org.hibernate.annotations.CascadeType.SAVE_UPDATE:当通过底层Session的save()、update()及saveOrUpdate()方法来保存或更新当前对象时,会级联保存所有关联的新建的临时对象,并且级联更新所有关联的游离对象;

注:在实际开发中谨慎使用cascade属性,以免对数据库造成不可预知的影响。

一对一

一对一关系,一个表中的记录与另一个表中的记录之间存在唯一的对应关系。以商品为例,一件商品只有一个详情信息。

2.1 实体类

package com.jingai.jpa.dao.entity;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;

import javax.persistence.*;
import java.util.Date;

@Data
@Entity
@JsonIgnoreProperties(value = { "hibernateLazyInitializer"})
@Table(name = "tb_goods")
public class GoodsEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
    private String subtitle;
    private Long classificationId;
    private Date createTime;

    @Transient
    private String createTimeStr;

    @OneToOne(cascade = {CascadeType.MERGE, CascadeType.REMOVE})
    @JoinColumn(name = "id", referencedColumnName = "id")
    private GoodsDetailEntity detail;

}

@Transient注解:该注解用于标注对应的属性不在数据库表中;

@OneToOne注解:用于实体上的注解,表示一对一关系。此处商品详情的数据变更同商品并存,此处的cascade可以设置CascadeType.MERGE和CascadeType.REMOVE;

@JoinColumn注解:标注实体类与数据库的对应关系。主要可选属性如下:

1)name:定义被标注属性在数据库表中所对应的字段的名称;

2)unique:定义被标注属性在数据库表中的值是否唯一,默认为false;

3)insertable:表示在使用“Insert”脚本插入数据时,是否需要插入被标注属性的值,默认为true;

4)updatable:表示在使用“Update”脚本插入数据时,是否需要更新被标注属性的值,默认为true;

5)referencedColumnName:定义所关联表中的字段名;

6)table:定义包含当前字段的表名;

package com.jingai.jpa.dao.entity;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;

import javax.persistence.*;

@Data
@Entity
@JsonIgnoreProperties(value = { "hibernateLazyInitializer"})
@Table(name = "tb_goods_detail")
public class GoodsDetailEntity {

    @Id
    private Long id;
    private String detailDescribe;
    private String pictures;

}

2.2 Repository类

package com.jingai.jpa.dao;

import com.jingai.jpa.dao.entity.GoodsEntity;
import org.springframework.data.jpa.repository.support.JpaRepositoryImplementation;

public interface GoodsRepository extends JpaRepositoryImplementation<GoodsEntity, Long> {
}
package com.jingai.jpa.dao;

import com.jingai.jpa.dao.entity.GoodsDetailEntity;
import org.springframework.data.jpa.repository.support.JpaRepositoryImplementation;

public interface GoodsDetailRepository extends JpaRepositoryImplementation<GoodsDetailEntity, Long> {
}

2.3 Service类

package com.jingai.jpa.service;

import com.jingai.jpa.dao.GoodsDetailRepository;
import com.jingai.jpa.dao.GoodsRepository;
import com.jingai.jpa.dao.entity.GoodsDetailEntity;
import com.jingai.jpa.dao.entity.GoodsEntity;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.util.Date;
import java.util.Optional;

@Service
public class GoodsService {

    @Resource
    private GoodsRepository goodsRepository;

    @Resource
    private GoodsDetailRepository goodsDetailRepository;

    @Transactional
    public GoodsEntity save(GoodsEntity entity, GoodsDetailEntity detail) {
        entity.setCreateTime(new Date());
        entity = goodsRepository.save(entity);
        detail.setId(entity.getId());
        detail = goodsDetailRepository.save(detail);
        entity.setDetail(detail);
        return entity;
    }

    public GoodsEntity get(long id) {
        // getById()使用懒加载的方式访问数据库,只有在真正访问GoodsEntity的才会真正执行数据库访问
        return goodsRepository.getById(id);
    }

    public GoodsEntity find(long id) {
        // findById()是立即访问数据库查询数据
        Optional<GoodsEntity> entity = goodsRepository.findById(id);
        return entity.isPresent() ? entity.get() : null;
    }

    /**
     * 修改。由于在GoodsEntity中的GoodsDetailEntity的@OneToOne注解配置了CascadeType.MERGE,修改更新会级联执行
     */
    @Transactional
    public GoodsEntity update(GoodsEntity goods, GoodsDetailEntity detail) {
        detail.setId(goods.getId());
        goods.setDetail(detail);
        goods.setCreateTime(new Date());
        goods = goodsRepository.save(goods);
        return goods;
    }

}

说明:此处为了讲解方便,不先定义接口后定义实现类。

2.4 Controller类

package com.jingai.jpa.controller;

import com.jingai.jpa.dao.entity.GoodsDetailEntity;
import com.jingai.jpa.dao.entity.GoodsEntity;
import com.jingai.jpa.service.GoodsService;
import com.jingai.jpa.util.ResponseUtil;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.text.SimpleDateFormat;
import java.util.Map;

@RestController
@RequestMapping("goods")
public class GoodsController {

    @Resource
    private GoodsService goodsService;

    @PostMapping("save")
    public Map<String, Object> save(GoodsEntity goods, GoodsDetailEntity detail) {
        goods = goodsService.save(goods, detail);
        goods.setCreateTimeStr(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(goods.getCreateTime()));
        return ResponseUtil.success(goods);
    }

    @GetMapping("get")
    public Map<String, Object> get(long id) {
        GoodsEntity entity = goodsService.get(id);
        return ResponseUtil.success(entity);
    }

    @GetMapping("find")
    public Map<String, Object> find(long id) {
        GoodsEntity entity = goodsService.find(id);
        return ResponseUtil.success(entity);
    }

    @PostMapping("update")
    public Map<String, Object> update(GoodsEntity goods, GoodsDetailEntity detail) {
        goods = goodsService.update(goods, detail);
        goods.setCreateTimeStr(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(goods.getCreateTime()));
        return ResponseUtil.success(goods);
    }

}

2.5 访问接口,结果如下:

2.6 LazyInitializationException异常

当访问

http://localhost:8080/goods/get?id=14

时,系统会报Method threw ‘org.hibernate.LazyInitializationException‘ exception. Cannot evaluate异常,原因是JpaRepository.getById()方法是懒加载,访问该方法时,返回一个对应实体的引用,而该引用是没有值的,此时创建了一个临时的session,并没有真正访问数据库,并立即关闭了session。在Controller层中访问该引用的信息时才真正执行数据库的访问,此时session已关闭,所以报了上面的异常,提示no session。

解决方法:

方法一:在application.yml中添加spring.jpa.open-in-view=true。这个配置在SpringBoot集成JPA及基本使用-CSDN博客中有讲解,建议关闭。而且该配置只能解决通过controller层访问引起的懒加载问题;

方法二:在application.yml中添加spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true。该方法既能解决通过Controller层访问引起的懒加载,也能解决定时任务等访问引起的懒加载问题;

当然也可以使用CrudRepository.findById()这个接口,立即访问数据库。

一对多

一对多关系,一个表中的一条记录与另一个表中的多条记录之间相对应。如一个会员有多个地址。

3.1 实体类

package com.jingai.jpa.dao.entity;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;

import javax.persistence.*;
import java.util.Date;

@Data
@Entity
@JsonIgnoreProperties(value = { "hibernateLazyInitializer"})
@Table(name = "tb_member")
public class MemberEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;
    private String name;
    private String sex;
    private Date createTime;
}
package com.jingai.jpa.dao.entity;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;

import javax.persistence.*;
import java.util.Date;

@Data
@Entity
@JsonIgnoreProperties(value = { "hibernateLazyInitializer"})
@Table(name = "tb_address")
public class AddressEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    @Column(name = "member_id")
    private int memberId;

    @ManyToOne
    @JoinColumn(name = "member_id", referencedColumnName = "id", updatable = false, insertable = false)
    private MemberEntity member;

    private String province;
    private String city;
    private String address;
    private String phone;
    private Date createTime;
}

@ManyToOne注解:地址与会员是多对一的关系,所以此处添加了@ManyToOne注解,表示多对一,不添加cascade配置,因为会员也可以没有地址,它们之前没有必然关联;

@JoinColumn注解:此处的updateable为false,insertable也为false。即对Address表的修改不会影响到Member表;

3.2 Repository类

package com.jingai.jpa.dao;

import com.jingai.jpa.dao.entity.MemberEntity;
import org.springframework.data.jpa.repository.support.JpaRepositoryImplementation;

public interface MemberRepository extends JpaRepositoryImplementation<MemberEntity, Long> {
}
package com.jingai.jpa.dao;

import com.jingai.jpa.dao.entity.AddressEntity;
import org.springframework.data.jpa.repository.support.JpaRepositoryImplementation;

public interface AddressRepository extends JpaRepositoryImplementation<AddressEntity, Long> {
}

3.3 Service类

package com.jingai.jpa.service;

import com.jingai.jpa.common.form.AddressForm;
import com.jingai.jpa.dao.AddressRepository;
import com.jingai.jpa.dao.entity.*;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import java.util.ArrayList;
import java.util.List;

@Service
public class AddressService {

    @Resource
    private AddressRepository addressRepository;

    public Page<AddressEntity> listByPage(AddressForm form) {
        // 创建一个Specification,实现接口中的toPredicate()方法,该方法返回一个Predicate
        Specification<AddressEntity> specification = new Specification<AddressEntity>() {
            @Override
            public Predicate toPredicate(Root<AddressEntity> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
                List<Predicate> predicates = new ArrayList<>(8);
                // 通过会员名称查询
                if(StringUtils.hasText(form.getName())) {
                    // 先通过AddressEntity_.member定位到MemberEntity,然后再定位到MemberEntity_.name
                    predicates.add(criteriaBuilder.like(root.get(AddressEntity_.member).get(MemberEntity_.name), "%" + form.getName() + "%"));
                }
                if(StringUtils.hasText(form.getPhone())) {
                    predicates.add(criteriaBuilder.like(root.get(AddressEntity_.PHONE), "%" + form.getPhone() + "%"));
                }
                if(form.getStartDate() != null) {
                    predicates.add(criteriaBuilder.greaterThanOrEqualTo(root.get(AddressEntity_.createTime), form.getStartDate()));
                }
                if(form.getEndDate() != null) {
                    predicates.add(criteriaBuilder.lessThanOrEqualTo(root.get(AddressEntity_.createTime), form.getEndDate()));
                }
                return query.where(predicates.toArray(new Predicate[predicates.size()])).getRestriction();
            }
        };
        // 创建排序字段,可设置多个
        Sort sort = Sort.by(Sort.Direction.DESC, ProductEntity_.createTime.getName());
        Pageable pageable = PageRequest.of(form.getPageIndex(), form.getPageSize(), sort);
        // 使用JpaSpecificationExecutor的findAll()方法,只能返回实体类的集合
        return addressRepository.findAll(specification, pageable);
    }

}

此处重点讲解AddressService中使用Criteria查询某个会员名称的地址信息。对Criteria查询不清楚的,可以看

Spring Data JPA Criteria查询、部分字段查询-CSDN博客

示例中的AddressForm在该篇博文也有讲解到。

此处可以通过JPA的元模式,很方便的实现表的关联查询。

3.4 Controller类

@RestController
@RequestMapping("address")
public class AddressController {

    @Resource
    private AddressService addressService;

    @GetMapping("search")
    public Map<String, Object> search(AddressForm form) {
        return ResponseUtil.success(addressService.listByPage(form));
    }

}

访问接口后显示效果如下:

多对多

多对多关系,指两个表中的记录可以相互对应。如一个学生可以选择多门课程,一门课程可以被多个学生选择。针对多对多关系的场景,通常使用中间表进行关联。

@Data
@Entity
@JsonIgnoreProperties(value = { "hibernateLazyInitializer"})
@Table(name = "tb_student")
public class Student {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @ManyToMany
	@JoinTable(name = "student_course", joinColumns = {@JoinColumn(name = "student_id")}, inverseJoinColumns = {@JoinColumn(name = "course_id")})
    private List<Course> courses;

}
@Data
@Entity
@JsonIgnoreProperties(value = { "hibernateLazyInitializer"})
@Table(name = "tb_student")
public class Course {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @ManyToMany(mappedBy = "courses")
    private List<Student> students;

}

@ManyToMany注解:用于标注多对多关系。mappedBy为被注解的多的实体类的属性字段;

@JoinTable注解:name为关联表的名称;joinColumns:关联student表的id;inverseJoinColumns:关联Course表的id;

结尾

限于篇幅,Spring Data JPA的一对一、一对多、多对多操作就分享到这里。

关于本篇内容你有什么自己的想法或独到见解,欢迎在评论区一起交流探讨下吧

  • 22
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值