1. 前言
本周写了一个自定义的认证、鉴权功能,发现以前SpringBoot的使用记录对于查找问题并不友好,这里对这几篇文章重新进行整理。分成以下几大部分:
- 存储
- 传输
- 其他机制
- 缓存组件
JPA使用的版本是spring-boot-starter-data-jpa-2.6.4,数据库使用的是MySQL
2. 配置
-
如何控制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
-
注解介绍
-
OneToMany
配置在一对多关系中的"一"的一侧,属性包括cascade、fetch、mappedBy、orphanRemoval
-
ManyToOne
配置在一对多关系中”多“的一侧,属性包括cascade、fetch
-
joinColumn
用于对外键列进行详细的定义,属性包括name(外键列名)、referencedColumnName(被指向表的列名)
这里需要注解的cascade、fetch、mappedBy、orphanRemoval的属性进行介绍
PERSIST:将持久化(save)操作从父entity传播到子entity
Merge:将合并(修改)操作从父entity传播到子entity
REMOVE:将删除操作从父entity传播到子entity
DETACH:将实体从持久化上下文中删除,父entity与子entity的操作也就独立了
注意,这些与get无关,都是CUD操作时候的。
为true时,之间的关系断开,会执行删除操作。这个与cascase的remove有些类似,但只用cascade=CascadeType.REMOVE时,关系的断开,不会执行删除操作。
参考
-
示例
@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标注的属性。
参考
-
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的介绍要简单很多
-
注解介绍
与ManyToMany有关的注解有2个:@ManyToMany、与@JoinTable
使用上注意:
- 可以创建单边的多对多关系,只在一个entity上使用ManyToMany与JoinTable进行注解
- 如果只用@ManyToMany进行注解,会建立两个中间表a_b与b_a,通过mappedBy属性可以只创建一个。
-
示例
@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;
}
-
无限递归查询问题
无限递归查询问题指的是:由于创建了双向的ManyToMany关系,在查询一方时关联查询另外一方而造成了递归的死循环情况。在上边的OneToMany中使用@JsonManagedReference与@JsonBackReference来解决,还可以通过JsonIgnore来解决。
这种无限递归查询的问题往往在2个情况下发生:
-
get到数据后,返回时数据时,将entity序列化为json时发生
这种情况通过fetch = FecthType.LAZY 与 @JsonIgnore方式来解决
-
通过print打印时发生
这种通过fetch = FecthType.LAZY 与 重载toString()来解决
参考1、参考2
-
多对多关系的删除实践
一般采用ORM的实践的方式是将相关数据取出,然后删除掉要断开关系的entity,然后通过save的方式来删除,例如:这里
这种需要先select、再delete、再save逗了一个大圈仅仅完成删除一对关系的操作,性能太低,不如使用原生更高效。
原生的写法见下文
ps:测试原生删除与context之间的影响
5. JPA查询
JPA最舒服的是单表的查询,可以直接使用 JpaRepository
提供的方法来进行查询
JpaRepository的继承关系如下图所示:
在实践过程中,单表查询有2种比较好的方式:
-
通过继承JpaRepository的interface,在其中指定相应接口,JPA会帮我们实现相应的接口,如
public interface RoleDao extends JpaRepository<Role, Long> {
Role getByName(String name);
}
这种方式也称为JPA Named Queries
方式
除此之外,对于id进行进行查询,直接使用即可
-
通过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. 尾声
目前先整理这些,有时间再慢慢添加