JPA使用总结

1. 前言

本周写了一个自定义的认证、鉴权功能,发现以前SpringBoot的使用记录对于查找问题并不友好,这里对这几篇文章重新进行整理。分成以下几大部分:

  1. 存储
  2. 传输
  3. 其他机制
  4. 缓存组件

JPA使用的版本是spring-boot-starter-data-jpa-2.6.4,数据库使用的是MySQL

2. 配置

  1. 如何控制JPA自定义创建表?

      sping:
          datasource:
            driver-class-name: com.mysql.cj.jdbc.Driver
            url: jdbc:mysql://localhost:3306/auth?useUnicode=true&characterEncoding=utf8&useSSL=false&allowMultiQueries=true
            username: root
            password: xxxxx
          jpa:
            hibernate:
              ddl-auto: update
            show-sql: true
            properties:
              hibernate:
                dialect: org.hibernate.dialect.MySQL57Dialect
                format_sql: true
    

    配置:jpa.hibernate.ddl-auto: update它的意思是没有就创,有变化就自动修改,没变化就不执行。

    PS:这种方式比较适合测试、demon等场景,不适合在产品环境中使用,这时候可以将该配置设为none

3. 主键

3.1自增主键的设置

在当前的环境下,直接使用:

@Id
@GeneratedValue
private Long id;

会出现所有表的主键公用一个Hibernate序列的情况,各个表主键比较混乱,不利于维护,这时候可以通过指明strategy的方式来确定自增的方式。

对于Mysql而言,适合使用GenerationType.IDENTITY

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

可选值包括:

  • IDENTITY:采用数据库ID自增长的方式来自增主键字段,Oracle 不支持这种方式
  • AUTO: JPA自动选择合适的策略,是默认选项;
  • SEQUENCE:通过序列产生主键,通过@SequenceGenerator 注解指定序列名,MySql不支持这种方式
  • TABLE:通过表产生主键,框架借由表模拟序列产生主键,使用该策略可以使应用更易于数据库移植。

参考:参考1 参考2

3.2 联合主键

主要有两种方式:IdClass方式与EmbeddedId方式,它俩都需要创建一个联合主键类。

  • idClass方式

    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    @EqualsAndHashCode
    public class StudentUPK implements Serializable {
        private Integer stuNo;
        private String stuName;
    }
    
    @Entity
    @IdClass(StudentUPK.class)
    @Table(name = "student")
    public class Student {
        @Id
        @Column(name = "stu_no", nullable = false, length = 11)
        private Integer stuNo;
    
        @Id
        @Column(name = "stu_name", nullable = false, length = 128)
        private String stuName;
    
        @Column(name = "stu_age", nullable = false, length = 3)
        private Integer stuAge;
    }
    

    这种方式直接在Student上增加@IdClass即可,类内部依然使用@Id,对于StudentUPK也没有要求

  • EmbeddedId方式

    @Builder
    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    @Embeddable
    @EqualsAndHashCode
    public class StudentUPK implements Serializable {
        @Column(name = "stu_no", nullable = false, length = 11)
        private Integer stuNo;
    
        @Column(name = "stu_name", nullable = false, length = 128)
        private String stuName;
    }
    
    @Entity
    @Table(name = "student")
    public class Student {
        @EmbeddedId
        private StudentUPK studentUPK;
    
        @Column(name = "stu_age", nullable = false, length = 3)
        private Integer stuAge;
    }
    

    这种方式在StudentUPK增加了@Embeddable,且增加@Colomn等装饰,在Student上使用@EmbeddedId

    在实践中,我使用的是前一种方式

    参考

在我的实践中,是用于自己创建中间表,这个中间表中有其他字段的情况。这里有这样一种实践,如下:

student与course是多对多的关系,这个关系还有一个rateing评分的属性

@Embeddable
class CourseRatingKey implements Serializable {

    @Column(name = "student_id")
    Long studentId;

    @Column(name = "course_id")
    Long courseId;
    // standard constructors, getters, and setters
    // hashcode and equals implementation
}

@Entity
class CourseRating {

    @EmbeddedId
    CourseRatingKey id;

    @ManyToOne
    @MapsId("studentId")
    @JoinColumn(name = "student_id")
    Student student;

    @ManyToOne
    @MapsId("courseId")
    @JoinColumn(name = "course_id")
    Course course;

    int rating;
    
    // standard constructors, getters, and setters
}

class Student {
    // ...
    @OneToMany(mappedBy = "student")
    Set<CourseRating> ratings;

    // ...
}

class Course {
    // ...
    @OneToMany(mappedBy = "course")
    Set<CourseRating> ratings;
    // ...
}

参考

4. 关联关系

4.1 OneToMany

  1. 注解介绍

    • OneToMany

      配置在一对多关系中的"一"的一侧,属性包括cascade、fetch、mappedBy、orphanRemoval

    • ManyToOne

      配置在一对多关系中”多“的一侧,属性包括cascade、fetch

    • joinColumn

      用于对外键列进行详细的定义,属性包括name(外键列名)、referencedColumnName(被指向表的列名)

    这里需要注解的cascade、fetch、mappedBy、orphanRemoval的属性进行介绍

    • cascade级联

      ALL:所有操作都从父entity传播到子entity

    ​ PERSIST:将持久化(save)操作从父entity传播到子entity

    ​ Merge:将合并(修改)操作从父entity传播到子entity

    ​ REMOVE:将删除操作从父entity传播到子entity

    ​ DETACH:将实体从持久化上下文中删除,父entity与子entity的操作也就独立了

    ​ 注意,这些与get无关,都是CUD操作时候的。

    • fetch

      与get相关的是fetch属性

      LAZY、EAGER。LAZY = fetch when needed EAGER = fetch immediately

    • mappedBy

      单向关系使用,在one的一侧使用,指明many一侧关系的外键。如果不用,在one的一侧会用中间表创建关系。

    • orphanRemoval

    为true时,之间的关系断开,会执行删除操作。这个与cascase的remove有些类似,但只用cascade=CascadeType.REMOVE时,关系的断开,不会执行删除操作。

    参考

  2. 示例

    @Entity
    public class Person {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private int id;
        private String name;
        @OneToMany(mappedBy = "person", cascade = CascadeType.ALL)
        @JsonManagedReference
        private List<Address> addresses;
    }
    @Entity
    public class Address {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private int id;
        private String street;
        private int houseNumber;
        private String city;
        private int zipCode;
        @ManyToOne(fetch = FetchType.LAZY)
    	@JoinColumn(name = "person_id", referencedColumnName = "id")
        @JsonBackReference
        private Person person;
    }
    

    这里出现了@JsonManagedReference与@JsonBackReference,它们用于解决关系中无限递归查询的问题,这两个标注通常配对使用,用在父子关系中。

    @JsonBackReference标注的属性在序列化时,会被忽略,@JsonManagedReference标注的属性则会被序列化;

    在反序列化时,如果没有@JsonManagedReference,则不会自动注入@JsonBackReference标注的属性,如果有@JsonManagedReference,则会自动注入自动注入@JsonBackReference标注的属性。

    参考

  3. N+1查询问题

    这个问题指的是一对多级联查询时,对多的一方执行n次SQL查询的情况,如:

    select owner0_.id as id1_1_, owner0_.created_at as created_2_1_, owner0_.updated_at as updated_3_1_, owner0_.name as name4_1_, owner0_.version as version5_1_ from owner owner0_
    
    select cars0_.owner_id as owner_id6_0_0_, cars0_.id as id1_0_0_, cars0_.id as id1_0_1_, cars0_.created_at as created_2_0_1_, cars0_.updated_at as updated_3_0_1_, cars0_.license_no as license_4_0_1_, cars0_.owner_id as owner_id6_0_1_, cars0_.version as version5_0_1_ from car cars0_ where cars0_.owner_id=? [1]
    
    select cars0_.owner_id as owner_id6_0_0_, cars0_.id as id1_0_0_, cars0_.id as id1_0_1_, cars0_.created_at as created_2_0_1_, cars0_.updated_at as updated_3_0_1_, cars0_.license_no as license_4_0_1_, cars0_.owner_id as owner_id6_0_1_, cars0_.version as version5_0_1_ from car cars0_ where cars0_.owner_id=? [2]
    

    这种方式可以在多的一次增加@BatchSize来处理,或者在配置上

    hibernate.batch_fetch_style=PADDED
    hibernate.default_batch_fetch_size=25
    

    参考1 参考2

    ps: 该问题需要再去验证

4.2 ManyToMany

有了oneToMany的介绍,ManyToMany的介绍要简单很多

  1. 注解介绍

    与ManyToMany有关的注解有2个:@ManyToMany、与@JoinTable

    • ManyToMany

      用于建立多对多关系,其主要的属性包括:cascade、fetch、mappedBy,这些注解与OneToMany中的含义相同,这里不多解释了

    • JoinTable

      对中间表进行控制,其主要属性包括:name(中间表名)、joinColumns(本表的对应的外键名)、inverseJoinColumns(另外一种关联表对应的外键名)

    使用上注意:

    • 可以创建单边的多对多关系,只在一个entity上使用ManyToMany与JoinTable进行注解
    • 如果只用@ManyToMany进行注解,会建立两个中间表a_b与b_a,通过mappedBy属性可以只创建一个。
  2. 示例

    @Data
    @AllArgsConstructor
    @Entity
    @EqualsAndHashCode(exclude = {"userList"})
    public class Org implements Serializable {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
    
        @Column(nullable = false)
        private String name;
    
        // ... other properties
    
        @ManyToMany(
                cascade = CascadeType.DETACH,
                fetch = FetchType.LAZY
        )
        @JoinTable(
                name = "org_user",
                joinColumns = @JoinColumn(name = "org_id"),
                inverseJoinColumns = @JoinColumn(name = "user_id")
        )
        private List<User> userList;
    
        @Override
        public String toString(){
            return JsonUtil.bean2JsonString(this);
        }
    }
    
    @Data
    @AllArgsConstructor
    @Entity
    @EqualsAndHashCode(exclude = {"orgList"})
    public class User implements Serializable {
    
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
    
        @Column(nullable = false,unique = true)
        private String username;
    
        // ... other properties
    
        @ManyToMany(
                cascade = CascadeType.DETACH,
                fetch = FetchType.LAZY,
                mappedBy = "userList"
        )
        @JsonIgnore
        private List<Org> orgList;
    }
    
  3. 无限递归查询问题

    无限递归查询问题指的是:由于创建了双向的ManyToMany关系,在查询一方时关联查询另外一方而造成了递归的死循环情况。在上边的OneToMany中使用@JsonManagedReference与@JsonBackReference来解决,还可以通过JsonIgnore来解决。

    这种无限递归查询的问题往往在2个情况下发生:

    1. get到数据后,返回时数据时,将entity序列化为json时发生

      这种情况通过fetch = FecthType.LAZY 与 @JsonIgnore方式来解决

    2. 通过print打印时发生

      这种通过fetch = FecthType.LAZY 与 重载toString()来解决

    参考1参考2

  4. 多对多关系的删除实践

    一般采用ORM的实践的方式是将相关数据取出,然后删除掉要断开关系的entity,然后通过save的方式来删除,例如:这里

    这种需要先select、再delete、再save逗了一个大圈仅仅完成删除一对关系的操作,性能太低,不如使用原生更高效。

    原生的写法见下文

    ps:测试原生删除与context之间的影响

5. JPA查询

JPA最舒服的是单表的查询,可以直接使用 JpaRepository提供的方法来进行查询

JpaRepository的继承关系如下图所示:

在实践过程中,单表查询有2种比较好的方式:

  1. 通过继承JpaRepository的interface,在其中指定相应接口,JPA会帮我们实现相应的接口,如

    public interface RoleDao extends JpaRepository<Role, Long> {
        Role getByName(String name);
    }
    

    这种方式也称为JPA Named Queries方式

    除此之外,对于id进行进行查询,直接使用即可

  2. 通过Example的方式来进行查询,如

    User user = new User();
    user.setUsername("admin");
    Example<User> example = Example.of(user);
    List<User> list = userRepository.findAll(example);
    
    User user = new User();
    user.setUsername("y");
    user.setAddress("sh");
    user.setPassword("admin");
    ExampleMatcher matcher = ExampleMatcher.matching()
        .withMatcher("username", ExampleMatcher.GenericPropertyMatchers.startsWith())//模糊查询匹配开头,即{username}%
        .withMatcher("address" ,ExampleMatcher.GenericPropertyMatchers.contains())//全部模糊查询,即%{address}%
        .withIgnorePaths("password");//忽略字段,即不管password是什么值都不加入查询条件
    Example<User> example = Example.of(user ,matcher);
    List<User> list = userRepository.findAll(example);
    

    参考

链表等查询,我比较喜欢原生SQL的方式,下边看看原生SQL

6. 原生SQL语句

6.1 查询

与查询相关的主要是@Query(将nativeQuery设置为true)与@Param(传递参数),如下:

public interface OrgDao extends JpaRepository<Org, Long>{

    @Query( nativeQuery = true,
            value = "select o.id, o.name from org o "
                    +"inner join user_role ur on o.id = ur.org_id "
                    + "where ur.user_id =:userId")
    List<Map<String, Object>> getByUserId(@Param("userId") Long userId);
    ...
}

这是一个链表查询,通过userId去join获取所在组织的信息,这里注意返回值,是result set的形式,还需要进一步将返回结果转换成想要的VO。

这里实现了一个converter:

public class NativeQueryConverter {
    private final static ObjectMapper objectMapper = new ObjectMapper();
    public NativeQueryConverter(){
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    }

    public static <T> List<T> convert(List<Map<String, Object>> dataList, Class<T> objClass) {
        List<T> res = new ArrayList<>();
        for (Map<String, Object> item: dataList) {
            try {
                String json = objectMapper.writeValueAsString(item);
                T bean = objectMapper.readValue(json, objClass);
                res.add(bean);
            } catch (JsonProcessingException e){
                e.printStackTrace();
            }
        }
        return res;
    }
}

这样返回的结果就可以:

  public List<OrgVo> getOrgList(Long userId){
        List<Map<String, Object>> resMap = orgDao.getByUserId(userId);
        return NativeQueryConverter.convert(resMap, OrgVo.class);
    }

通过这种方式,间接绕过了JPQL的语句,喜欢JPQL的也可以直接使用JPQL。

6.2 删除

除了@Query、@Param,还需要@Modifying与@Transactional

@Transactional
@Modifying
    @Query(nativeQuery = true,
        value = "delete from org_user "
                + "where org_id=:orgId and user_id=:userId")
    void removeUser(@Param("orgId") Long orgId, @Param("userId") Long userId);

对于@Transactional,如果外层已经在事务中,这里也可以不同

参考:

JPQL与原生

原生传参参考

Modifying参考

7. 尾声

目前先整理这些,有时间再慢慢添加

# spring 

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×