icon: edit
date: 2022-01-02
category:
- CategoryA
tag: - tag A
- tag B
star: true
Spring Boot JPA 2.7.2
项目介绍
依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>2.7.2</version>
</dependency>
子依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
<version>2.7.2</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>jakarta.transaction</groupId>
<artifactId>jakarta.transaction-api</artifactId>
<version>1.3.3</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>jakarta.persistence</groupId>
<artifactId>jakarta.persistence-api</artifactId>
<version>2.2.3</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>5.6.10.Final</version>
<scope>compile</scope>
<exclusions>
.....
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
<version>2.7.2</version>
<scope>compile</scope>
</dependency>
可以看到啊:引入了JPA就等于引入了JDBC, 还有实现了JPA的ORM框架:Hibernate
:::tip Jakarta
Eclipse基金会社区已将Java EE改名为Jakarta EE。这个名称来自Apache的一个早期开源项目:Jakarta Project。
:::
接下来我们着重讲解Hibernate。
Hibernate
介绍
Hibernate包含六个模块:
- Hibernate ORM 数据库与后端模型的映射
- Hibernate Search 数据模型的全文索引
- Hibernate Validator 基于注解的验证器
- Hibernate Reactive 反应式的数据模型映射
- Hibernate Tools 为IDE提供的Hibernate工具支持
- Others 其他Hibernate的杂项支持
JPA Starter引入了hibernate-core,也就是ORM映射包,我们着重去看这一块吧。
简例
@Entity(name = "Product")
public class Product {
@Id
@Basic
private Integer id;
@Basic
private String sku;
@Basic
private String name;
@Basic
@Column( name = "NOTES" )
private String description;
}
::: tip 注解
对于这样一个实体:
-
@Id:用来标识主码字段。
-
@Basic :一般可以省略,因为你既然声明了该属性,就表示数据库存在该字段。
-
@Column name表示将该属性映射为name字段。
-
@Entity name表示该实体映射到目标数据表。
:::
- Java primitive types (
boolean
,int
, etc) - wrappers for the primitive types (
java.lang.Boolean
,java.lang.Integer
, etc) java.lang.String
java.math.BigInteger
java.math.BigDecimal
java.util.Date
java.util.Calendar
java.sql.Date
java.sql.Time
java.sql.Timestamp
byte[]
orByte[]
char[]
orCharacter[]
enums
::: tip 提示
如果考虑到提供者的可移植性,您应该只使用这些基本类型。
:::
枚举映射
基本映射
public enum PhoneType {
LAND_LINE,
MOBILE;
}
@Entity(name = "Phone")
public static class Phone {
@Id
private Long id;
@Column(name = "phone_number")
private String number;
@Enumerated(EnumType.ORDINAL)
@Column(name = "phone_type")
private PhoneType type;
}
::: tip Enumerated
将注解标识在声明的枚举属性上,并配置EnumType,即可完成枚举映射,该方式提供两种映射方式:
- EnumType.ORDINAL 默认按照枚举里声明的顺序映射,比如LAND_LINE就在数据存的0,MOBILE存的1
- EnumType.STRING 按照该枚举的name属性映射
:::
自定义映射
public enum Gender {
MALE( 'M' ),
FEMALE( 'F' );
private final char code;
Gender(char code) {
this.code = code;
}
public static Gender fromCode(char code) {
if ( code == 'M' || code == 'm' ) {
return MALE;
}
if ( code == 'F' || code == 'f' ) {
return FEMALE;
}
throw new UnsupportedOperationException(
"The code " + code + " is not supported!"
);
}
public char getCode() {
return code;
}
}
@Entity(name = "Person")
public static class Person {
@Id
private Long id;
private String name;
@Convert( converter = GenderConverter.class )
public Gender gender;
//Getters and setters are omitted for brevity
}
@Converter
public static class GenderConverter implements AttributeConverter<Gender, Character> {
public Character convertToDatabaseColumn( Gender value ) {
if ( value == null ) {
return null;
}
return value.getCode();
}
public Gender convertToEntityAttribute( Character value ) {
if ( value == null ) {
return null;
}
return Gender.fromCode( value );
}
}
::: tip AttributeConverter
实现该接口,然后在需要映射的字段上加上 @Convert 注解并配置转换器,改接口泛型1为枚举类,泛型2位目标映射类型,有两个方法需要实现:
- convertToDatabaseColumn 从枚举类映射到目标类型的转换
- convertToEntityAttribute 从数据库映射到枚举类的转换
:::
::: warning 注意
@Convert 和 @Enumerated不能混用,JPA显式地禁止使用带有 @Enumerated 属性的AttributeConverter!
:::
使用AttributeConverter实体映射
AttributeConverter不仅可以用映射枚举,亦可以映射java对象,
public class Money {
private long cents;
public Money(long cents) {
this.cents = cents;
}
public long getCents() {
return cents;
}
public void setCents(long cents) {
this.cents = cents;
}
}
public class Account {
private Long id;
private String owner;
@Convert( converter = MoneyConverter.class )
private Money balance;
//Getters and setters are omitted for brevity
}
public class MoneyConverter implements AttributeConverter<Money, Long> {
@Override
public Long convertToDatabaseColumn(Money attribute) {
return attribute == null ? null : attribute.getCents();
}
@Override
public Money convertToEntityAttribute(Long dbData) {
return dbData == null ? null : new Money( dbData );
}
}
时间日期映射
-
DATE =>
java.sql.Date
-
TIME =>
java.sql.Time
-
TIMESTAMP =>
java.sql.Timestamp
::: warning 注意
Hibernate建议使用 java.util
或者java.time
的时间日期映射,避免java.sql
映射,如果要使用 java.util
或者java.time
的时间日期映射,请使用==@Temporal==注解声明。
:::
@Entity(name = "DateEvent")
public static class DateEvent {
@Id
@GeneratedValue
private Long id;
@Column(name = "`timestamp1`")
@Temporal(TemporalType.DATE)
private java.util.Date timestamp1;
@Column(name = "`timestamp2`")
@Temporal(TemporalType.TIME)
private java.util.Date timestamp2;
@Column(name = "`timestamp3`")
@Temporal(TemporalType.TIMESTAMP)
private java.util.Date timestamp3;
//Getters and setters are omitted for brevity
}
public enum TemporalType {
/** Map as <code>java.sql.Date</code> */
DATE,
/** Map as <code>java.sql.Time</code> */
TIME,
/** Map as <code>java.sql.Timestamp</code> */
TIMESTAMP
}
SQL引用标识符
::: tip ``
如果字段的名字和数据库中的某些关键字或保留字冲突,可以使用引用标识符来区别:``
:::
@Entity(name = "Product")
public static class Product {
@Id
private Long id;
@Column(name = "`name`")
private String name;
@Column(name = "`number`")
private String number;
//Getters and setters are omitted for brevity
}
@Entity(name = "Product")
public static class Product {
@Id
private Long id;
@Column(name = "\"name\"")
private String name;
@Column(name = "\"number\"")
private String number;
//Getters and setters are omitted for brevity
}
生成的属性 @Generated
生成的属性是由数据库生成其值的属性。通常,Hibernate应用程序需要刷新包含数据库正在为其生成值的任何属性的对象。但是,将属性标记为生成的,可以让应用程序将此责任委托给Hibernate。当Hibernate为定义了生成属性的实体发出SQL INSERT或UPDATE时,它立即发出一个选择来检索生成的值。
标记为已生成的属性必须是不可插入和不可更新的。只有@Version和@Basic类型可以被标记为已生成。
NEVER
(默认) 给定的属性值不会在数据库中生成。INSERT
给定的属性值在插入时生成,但在后续更新时不会重新生成。ALWAYS
该属性值在插入和更新时生成。
@Entity(name = "Person")
public static class Person {
@Id
private Long id;
private String firstName;
private String lastName;
private String middleName1;
private String middleName2;
private String middleName3;
private String middleName4;
private String middleName5;
@Generated( value = GenerationTime.ALWAYS )
@Column(columnDefinition =
"AS CONCAT(" +
" COALESCE(firstName, ''), " +
" COALESCE(' ' + middleName1, ''), " +
" COALESCE(' ' + middleName2, ''), " +
" COALESCE(' ' + middleName3, ''), " +
" COALESCE(' ' + middleName4, ''), " +
" COALESCE(' ' + middleName5, ''), " +
" COALESCE(' ' + lastName, '') " +
")")
private String fullName;
}
::: warning 注意
这个属性必须是数据库不存在的属性。
:::
自动更新修改和插入时间
CreationTimestamp
@CreationTimestamp注释指示Hibernate在持久化实体时,用JVM的当前时间戳值设置已注释实体属性。
@UpdateTimestamp注释指示Hibernate在持久化或更新实体时,用JVM的当前时间戳值设置已注释实体属性。
他们支持如下类型:
java.util.Date
java.util.Calendar
java.sql.Date
java.sql.Time
java.sql.Timestamp
@Entity(name = "Event")
public static class Event {
@Id
@GeneratedValue
private Long id;
@Column(name = "`timestamp`")
@CreationTimestamp
private Date timestamp;
//Constructors, getters, and setters are omitted for brevity
}
Event dateEvent = new Event( );
entityManager.persist( dateEvent );
INSERT INTO Event ("timestamp", id)
VALUES (?, ?)
-- binding parameter [1] as [TIMESTAMP] - [Tue Nov 15 16:24:20 EET 2016]
-- binding parameter [2] as [BIGINT] - [1]
列转换器:读写表达式
Hibernate允许您定制ColumnTransformer用于读写映射到@Basic类型的列的值的SQL。例如,如果您的数据库提供了一组数据加密函数,那么您可以像下面的示例那样为各个列调用它们。
@Entity(name = "Employee")
public static class Employee {
@Id
private Long id;
@NaturalId
private String username;
@Column(name = "pswd")
@ColumnTransformer(
read = "decrypt( 'AES', '00', pswd )",
write = "encrypt('AES', '00', ?)"
)
private String password;
private int accessLevel;
@ManyToOne(fetch = FetchType.LAZY)
private Department department;
@ManyToMany(mappedBy = "employees")
private List<Project> projects = new ArrayList<>();
//Getters and setters omitted for brevity
}
如果一个属性使用多个列,则必须使用forColumn属性来指定@ColumnTransformer读写表达式的目标列。
@Entity(name = "Savings")
public static class Savings {
@Id
private Long id;
@Type(type = "org.hibernate.userguide.mapping.basic.MonetaryAmountUserType")
@Columns(columns = {
@Column(name = "money"),
@Column(name = "currency")
})
@ColumnTransformer(
forColumn = "money",
read = "money / 100",
write = "? * 100"
)
private MonetaryAmount wallet;
//Getters and setters omitted for brevity
}
::: warning 注意
如果指定了write表达式,则必须包含一个’?的占位符。
:::
Formula
在数据库的计算属性,该属性是只读的,而且不存在数据库中。
@Entity(name = "Account")
public static class Account {
@Id
private Long id;
private Double credit;
private Double rate;
@Formula(value = "credit * rate")
private Double interest;
//Getters and setters omitted for brevity
}
可嵌入的类型
出版商拥有一个地理位置:
@Embeddable
public static class Publisher {
private String name;
private Location location;
public Publisher(String name, Location location) {
this.name = name;
this.location = location;
}
private Publisher() {}
//Getters and setters are omitted for brevity
}
@Embeddable
public static class Location {
private String country;
private String city;
public Location(String country, String city) {
this.country = country;
this.city = city;
}
private Location() {}
//Getters and setters are omitted for brevity
}
可嵌入类型是值类型的另一种形式,它的生命周期绑定到父实体类型,因此从父实体类型继承属性访问(关于属性访问的详细信息,请参阅访问策略)。
可嵌入类型可以由基本值和关联组成,但需要注意的是,当用作集合元素时,它们不能定义集合本身。
@Entity(name = "Book")
public static class Book {
@Id
@GeneratedValue
private Long id;
private String title;
private String author;
private Publisher publisher;
//Getters and setters are omitted for brevity
}
@Embeddable
public static class Publisher {
@Column(name = "publisher_name")
private String name;
@Column(name = "publisher_country")
private String country;
//Getters and setters, equals and hashCode methods omitted for brevity
}
create table Book (
id bigint not null,
author varchar(255),
publisher_country varchar(255),
publisher_name varchar(255),
title varchar(255),
primary key (id)
)
::: tip 提示
JPA定义了两个用于处理可嵌入类型的术语:@Embeddable和@Embedded。
@Embeddable
用于描述映射类型本身(例如Publisher)@Embedded
用于引用给定的可嵌入类型(例如book.publisher)。
:::
上面代码等价于下面的代码:
@Entity(name = "Book")
public static class Book {
@Id
@GeneratedValue
private Long id;
private String title;
private String author;
@Column(name = "publisher_name")
private String publisherName;
@Column(name = "publisher_country")
private String publisherCountry;
//Getters and setters are omitted for brevity
}
多个可嵌入的类型
@Embeddable
public static class Publisher {
private Long id;
private String name;
//Getters and setters, equals and hashCode methods omitted for brevity
}
create table Country (
id bigint not null,
name varchar(255),
primary key (id)
)
对多个相同类型的可嵌入的类如此声明即可。
@Entity(name = "Book")
@AttributeOverrides({
@AttributeOverride(
name = "ebookPublisher.name",
column = @Column(name = "ebook_publisher_name")
),
@AttributeOverride(
name = "paperBackPublisher.name",
column = @Column(name = "paper_back_publisher_name")
),
@AttributeOverride(
name = "ebookPublisher.country",
column = @Column(name = "ebook_publisher_country_id")
),
@AttributeOverride(
name = "paperBackPublisher.country",
column = @Column(name = "paper_back_publisher_country_id")
)
})
public static class Book {
@Id
@GeneratedValue
private Long id;
private String title;
private String author;
private Publisher ebookPublisher;
private Publisher paperBackPublisher;
//Getters and setters are omitted for brevity
}
create table Book (
id bigint not null,
author varchar(255),
ebook_publisher_name varchar(255),
paper_back_publisher_name varchar(255),
title varchar(255),
ebook_publisher_country_id bigint,
paper_back_publisher_country_id bigint,
primary key (id)
)
POJO模型
JPA 2.1规范的实体类定义了实体类的需求。希望在JPA提供者之间保持可移植性的应用程序应该遵守以下要求:
- 实体类必须用javax.persistence.Entity注释(或在XML映射中这样表示)。
- 实体类必须有一个公共或受保护的无参数构造函数。它还可以定义其他构造函数。
- 实体类必须是顶级类。
- 枚举或接口不能被指定为实体。
- 实体类不能是final类。实体类的任何方法或持久实例变量都不能是final的。
- 如果要将实体实例作为分离对象远程使用,则实体类必须实现Serializable接口。
- 抽象类和具体类都可以是实体。实体可以扩展非实体类和实体类,而非实体类可以扩展实体类。
- 实体的持久状态由实例变量表示,它可能对应于javabean样式的属性。实例变量必须只能由实体实例本身从实体的方法内部直接访问。客户端只能通过实体的访问器方法(getter/setter方法)或其他业务方法来访问实体的状态。
然而,Hibernate的要求并不那么严格。与上述清单的区别包括:
- 实体类必须具有无参数构造函数,该构造函数可以是公共的、受保护的或包可见的。它还可以定义其他构造函数。
- 实体类不必是顶级类。
- 从技术上讲,Hibernate可以持久化最终类或具有最终持久状态访问器(getter/setter)方法的类。但是,这通常不是一个好主意,因为这样做会使Hibernate无法生成用于延迟加载实体的代理。
- Hibernate并不限制应用程序开发人员公开实例变量并从实体类本身之外引用它们。然而,这种范式的有效性充其量是有争议的。
详情请参考:
https://docs.jboss.org/hibernate/orm/5.6/userguide/html_single/Hibernate_User_Guide.html#entity-pojo
实体&数据表
@Entity
@Table(name = "`user`", catalog = "jpa")
class UserEntity {
@Id
@Column(name = "id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long = 0
@Basic
@Column(name = "username")
var username: String? = null
}
@Entity
@Entity注释只定义了name属性,不设置默认使用雷鸣,用于给出在JPQL查询中使用的特定实体名称。
@Query("select id from UserEntity")
fun searchAll()
在query
的UserEntity
便是。
@Table
name
表示数据表名。catalog
指定给定表所在的编目(大多指数据库名)。schema
只有在底层数据库支持schema的数据库有效,例如在postgresql
中表示模式名。
实现equals()和hashCode()
实际上只有一种绝对情况:作为标识符的类必须基于id值实现equals/hashCode。通常,这适用于用作复合标识符的用户定义类。除了这个非常具体的用例和我们将在下面讨论的其他几个用例之外,您可能想要考虑不完全实现equals/hashCode。
有什么好大惊小怪的?通常,大多数Java对象根据对象的标识提供内置的equals()和hashCode(),因此每个新对象将不同于所有其他对象。这通常是您在普通Java编程中所需要的。然而,从概念上讲,当您开始考虑一个类的多个实例表示相同数据的可能性时,这就开始失效了。
事实上,在处理来自数据库的数据时正是如此。每次从数据库加载特定的Person时,我们自然会获得一个惟一的实例。然而,Hibernate努力确保在给定的Session中不会发生这种情况。事实上,Hibernate保证在特定会话范围内持久标识(数据库行)和Java标识等价。因此,如果我们多次请求Hibernate会话加载特定的Person,我们实际上会返回相同的实例:
Book book1 = entityManager.find( Book.class, 1L );
Book book2 = entityManager.find( Book.class, 1L );
assertTrue( book1 == book2 ); //true
下面的book1
仍然等于book2
,所以断言为true:
Library library = entityManager.find( Library.class, 1L );
Book book1 = entityManager.find( Book.class, 1L );
Book book2 = entityManager.find( Book.class, 1L );
library.getBooks().add( book1 );
library.getBooks().add( book2 );
assertEquals( 1, library.getBooks().size() ); //true
对于下面不同session
查询出的,就不会相等:
Book book1 = doInJPA( this::entityManagerFactory, entityManager -> {
return entityManager.find( Book.class, 1L );
} );
Book book2 = doInJPA( this::entityManagerFactory, entityManager -> {
return entityManager.find( Book.class, 1L );
} );
assertFalse( book1 == book2 ); //true
doInJPA( this::entityManagerFactory, entityManager -> {
Set<Book> books = new HashSet<>();
books.add( book1 );
books.add( book2 );
//此处的结果取决于`equals/hashCode`
//因为book1和book2他们不再是同一对象,就需要依据Set规范判断
assertEquals( 2, books.size() );
} );
在您将处理Session之外的实体的情况下(无论它们是短暂的还是分离的),特别是在您将在Java集合中使用它们的情况下,您应该考虑实现equals/hashCode。
这里通过判断该类的所有属性是否相等来决定两个对象是否相等:
@Entity(name = "Library")
public static class Library {
@Id
private Long id;
private String name;
@OneToMany(cascade = CascadeType.ALL)
@JoinColumn(name = "book_id")
private Set<Book> books = new HashSet<>();
//Getters and setters are omitted for brevity
}
@Entity(name = "Book")
public static class Book {
@Id
@GeneratedValue
private Long id;
private String title;
private String author;
//Getters and setters are omitted for brevity
@Override
public boolean equals(Object o) {
if ( this == o ) {
return true;
}
if ( o == null || getClass() != o.getClass() ) {
return false;
}
Book book = (Book) o;
return Objects.equals( id, book.id );
}
@Override
public int hashCode() {
return Objects.hash( id );
}
}
::: warning 建议
无论何时,在一个类被包含在集合中都要去实现hashCode和equals方法,除非你知道你自己想要的是什么。
:::
基于属性的访问
有时候你想指定访问的方式,比如下方你想访问id
使用getter,而version
则直接访问
@Entity(name = "Book")
public static class Book {
private Long id;
private String title;
private String author;
@Access( AccessType.FIELD )
@Version
private int version;
@Id
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
}
主码
定义
标识符对实体的主键建模。它们用于惟一地标识每个特定的实体,就是我们常说的主键。
::: warning 警告
每个实体都必须定义一个标识符。对于实体继承层次结构,标识符必须仅在作为层次结构根的实体上定义。
:::
标识符可以是简单的(单个值)或复合的(多个值)。
根据JPA,只有以下类型应该用作标识符属性类型:
- 任何Java原语类型
- 任何原始包装类型
java.lang.String
java.util.Date
(TemporalType#DATE)java.sql.Date
java.math.BigDecimal
java.math.BigInteger
一个简单的例子(Long):
@Entity(name = "Book")
public static class Book {
@Id
private Long id;
private String title;
private String author;
//Getters and setters are omitted for brevity
}
生成的标识符
可以生成简单标识符的值。为了表示生成了一个标识符属性,它使用javax.persistence.GeneratedValue
进行注释
对生成的标识符值的期望是,在进行保存/持久化时,Hibernate将生成该值。
简单来说就是自动生成主键。
@Entity(name = "Book")
public static class Book {
@Id
@GeneratedValue
private Long id;
private String title;
private String author;
//Getters and setters are omitted for brevity
}
GeneratedValue
的GenerationType
的属性有以下值:
public enum GenerationType {
/**
* 表示持久性提供程序必须赋值
* 使用底层实体的主键
* 数据库表确保唯一性。
*/
TABLE,
/**
* 表示持久性提供程序必须赋值
* 使用数据库序列的实体的主键。 (auto_increment)
*/
SEQUENCE,
/**
* 表示持久性提供程序必须赋值
* 使用数据库标识列的实体的主键。
*/
IDENTITY,
/**
* 表示持久性提供程序应该选择一个
* 针对特定数据库的适当策略。的
* <code>AUTO</code>生成策略可能期望一个数据库
* 资源不存在,或者尝试创建一个。一个供应商
* 可以提供如何创建此类资源的文档
* 在不支持模式生成的情况下
* 或不能在运行时创建架构资源。
* 使用数据库标识列的实体的主键。
*/
AUTO
}
详情请参考:https://docs.jboss.org/hibernate/orm/5.6/userguide/html_single/Hibernate_User_Guide.html#identifiers-generators
一对一
单向一对一
@OneToOne
关联可以是单向的,也可以是双向的。单向关联遵循关系数据库外键语义,客户端拥有关系。双向关联还具有mappedBy
@OneToOne
父端。
@Entity(name = "Phone")
public static class Phone {
@Id
@GeneratedValue
private Long id;
@Column(name = "`number`")
private String number;
@OneToOne
@JoinColumn(name = "details_id")
private PhoneDetails details;
//Getters and setters are omitted for brevity
}
@Entity(name = "PhoneDetails")
public static class PhoneDetails {
@Id
@GeneratedValue
private Long id;
private String provider;
private String technology;
//Getters and setters are omitted for brevity
}
CREATE TABLE Phone (
id BIGINT NOT NULL ,
number VARCHAR(255) ,
details_id BIGINT ,
PRIMARY KEY ( id )
)
CREATE TABLE PhoneDetails (
id BIGINT NOT NULL ,
provider VARCHAR(255) ,
technology VARCHAR(255) ,
PRIMARY KEY ( id )
)
ALTER TABLE Phone
ADD CONSTRAINT FKnoj7cj83ppfqbnvqqa5kolub7
FOREIGN KEY (details_id) REFERENCES PhoneDetails
双向一对一
@Entity(name = "Phone")
public static class Phone {
@Id
@GeneratedValue
private Long id;
@Column(name = "`number`")
private String number;
@OneToOne(mappedBy = "phone",
cascade = CascadeType.ALL,
orphanRemoval = true,
fetch = FetchType.LAZY
)
private PhoneDetails details;
//Getters and setters are omitted for brevity
public void addDetails(PhoneDetails details) {
details.setPhone( this );
this.details = details;
}
public void removeDetails() {
if ( details != null ) {
details.setPhone( null );
this.details = null;
}
}
}
@Entity(name = "PhoneDetails")
public static class PhoneDetails {
@Id
@GeneratedValue
private Long id;
private String provider;
private String technology;
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "phone_id")
private Phone phone;
//Getters and setters are omitted for brevity
}
CREATE TABLE Phone (
id BIGINT NOT NULL ,
number VARCHAR(255) ,
PRIMARY KEY ( id )
)
CREATE TABLE PhoneDetails (
id BIGINT NOT NULL ,
provider VARCHAR(255) ,
technology VARCHAR(255) ,
phone_id BIGINT ,
PRIMARY KEY ( id )
)
ALTER TABLE PhoneDetails
ADD CONSTRAINT FKeotuev8ja8v0sdh29dynqj05p
FOREIGN KEY (phone_id) REFERENCES Phone
::: warning 唯一约束
当使用双向@OneToOne
关联时,Hibernate在获取子端时强制唯一约束。如果有多于一个子节点与同一个父节点关联,Hibernate将抛出org.hibernate.exception.ConstraintViolationException
。继续前面的示例,在添加另一个PhoneDetails
时,Hibernate在重新加载Phone对象时验证惟一约束。
:::
一对多
@OneToMany
关联将一个父实体与一个或多个子实体连接起来。如果@OneToMany
在子端没有镜像@ManyToOne
关联,那么@OneToMany
关联是单向的。如果在子端有一个@ManyToOne
关联,那么@OneToMany
关联是双向的,应用开发者可以从两端导航这个关系。
单向一对多
该方式会创建一个中间表且效率不高,忽略。。。
双向一对多
双向的@OneToMany
关联也需要在子端有@ManyToOne
关联。尽管域模型公开了导航该关联的两个方面,但在幕后,关系数据库只有一个用于该关系的外键。
每个双向关联必须只有一个拥有侧(子侧),另一个被称为逆侧(或mappedBy
):
@Entity(name = "Person")
public static class Person {
@Id
@GeneratedValue
private Long id;
@OneToMany(mappedBy = "person", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Phone> phones = new ArrayList<>();
//Getters and setters are omitted for brevity
public void addPhone(Phone phone) {
phones.add( phone );
phone.setPerson( this );
}
public void removePhone(Phone phone) {
phones.remove( phone );
phone.setPerson( null );
}
}
@Entity(name = "Phone")
public static class Phone {
@Id
@GeneratedValue
private Long id;
@NaturalId
@Column(name = "`number`", unique = true)
private String number;
@ManyToOne
private Person person;
//Getters and setters are omitted for brevity
@Override
public boolean equals(Object o) {
if ( this == o ) {
return true;
}
if ( o == null || getClass() != o.getClass() ) {
return false;
}
Phone phone = (Phone) o;
return Objects.equals( number, phone.number );
}
@Override
public int hashCode() {
return Objects.hash( number );
}
}
CREATE TABLE Person (
id BIGINT NOT NULL ,
PRIMARY KEY ( id )
)
CREATE TABLE Phone (
id BIGINT NOT NULL ,
number VARCHAR(255) ,
person_id BIGINT , #我们可以看到Phone没有在@ManyToOne下声明JoinColumn,则外键生成规则:属性名+下划线+一的一方的主键列名
PRIMARY KEY ( id )
)
ALTER TABLE Phone
ADD CONSTRAINT UK_l329ab0g4c1t78onljnxmbnp6
UNIQUE (number)
ALTER TABLE Phone
ADD CONSTRAINT FKmw13yfsjypiiq0i1osdkaeqpg
FOREIGN KEY (person_id) REFERENCES Person
::: warning orphanRemoval属性
注解OneToMany
下有一个orphanRemoval
属性,默认为false
,他表示:
true
当删除多的一方时,多的一方会执行delete
语句false
当删除多的一方时,多的一方的外键字段会设置成null
:::
::: warning 双向关联删除
每当形成双向关联时,应用程序开发人员必须确保双方一直处于同步状态。
你可以看到addPhone()
和removePhone()
是在添加或删除子元素时同步两端的实用工具方法。
新增插入和删除都是互相同步的。
:::
多对一
@ManyToOne
是最常见的关联,在关系数据库中也有直接等价的(例如外键),因此它在子实体和父实体之间建立了关系。
@Entity(name = "Person")
public static class Person {
@Id
@GeneratedValue
private Long id;
//Getters and setters are omitted for brevity
}
@Entity(name = "Phone")
public static class Phone {
@Id
@GeneratedValue
private Long id;
@Column(name = "`number`")
private String number;
@ManyToOne
@JoinColumn(name = "person_id", foreignKey = @ForeignKey(name = "PERSON_ID_FK"))
private Person person;
//Getters and setters are omitted for brevity
}
CREATE TABLE Person (
id BIGINT NOT NULL ,
PRIMARY KEY ( id )
)
CREATE TABLE Phone (
id BIGINT NOT NULL ,
number VARCHAR(255) ,
person_id BIGINT ,
PRIMARY KEY ( id )
)
ALTER TABLE Phone
ADD CONSTRAINT PERSON_ID_FK
FOREIGN KEY (person_id) REFERENCES Person
上述的Phone
声明了ManyToOne
伴随JoinColumn
来关联Person
:
name
表示外键字段foreignKey
表示外键名称
例子
Person person = new Person();
entityManager.persist( person );
Phone phone = new Phone( "123-456-7890" );
phone.setPerson( person );
entityManager.persist( phone );
entityManager.flush();
phone.setPerson( null );
INSERT INTO Person ( id )
VALUES ( 1 )
INSERT INTO Phone ( number, person_id, id ) VALUES ( '123-456-7890', 1, 2 );
UPDATE
Phone
SET
number = '123-456-7890',
person_id = NULL
WHERE
id = 2;
逻辑外键
当处理不是由物理外键强制的关联时,一个非空的外键值可能指向关联实体表上不存在的值。
::: warning 警告
不建议在数据库级别强制使用物理外键。
:::
Hibernate使用@NotFound
注释为这类模型提供支持,该注释接受一个NotFoundAction
值,该值指示当遇到这种坏掉的外键时,Hibernate应该如何操作:
EXCEPTION
(默认)Hibernate将抛出一个异常(FetchNotFoundException
)IGNORE
该关联将被视为null
@NotFound
(IGNORE)和@NotFound
(EXCEPTION)都使Hibernate假定没有物理外键。
::: tip 提示
如果应用程序本身管理引用完整性,并且能够保证没有损坏的外键,那么可以使用jakarta.persistence.ForeignKey
(NO_CONSTRAINT)来代替。这将迫使Hibernate不导出物理外键,但在避免@NotFound
的缺点方面仍然表现得像导出外键一样。
:::
::: warning 警告
@NotFound
还会影响HQL
和Criteria
中如何将关联视为“隐式连接”。当存在物理外键时,Hibernate可以放心地假设外键的键列中的值将与目标列中的值匹配,因为数据库会确保是这样的情况。但是,@NotFound
强制Hibernate在不需要的情况下执行隐式连接的物理连接。
:::
级联操作
::: tip 一对多集合
在一对多中的集合类型,hibernate默认支持了:
java.util.Collection
,
java.util.List
,
java.util.Set
,
java.util.Map
,
java.util.SortedSet
,
java.util.SortedMap
且!!!一定要声明为接口类型!!!
当然也可以支持更多,那就需要自己去实现 :org.hibernate.usertype.UserCollectionType
:::
单向级联操作
@Entity(name = "Person")
public static class Person {
@Id
private Long id;
@OneToMany(cascade = CascadeType.ALL)
private List<Phone> phones = new ArrayList<>();
//Getters and setters are omitted for brevity
}
@Entity(name = "Phone")
public static class Phone {
@Id
private Long id;
private String type;
@Column(name = "`number`")
private String number;
//Getters and setters are omitted for brevity
}
CREATE TABLE Person (
id BIGINT NOT NULL ,
PRIMARY KEY ( id )
)
CREATE TABLE Person_Phone (
Person_id BIGINT NOT NULL ,
phones_id BIGINT NOT NULL
)
CREATE TABLE Phone (
id BIGINT NOT NULL ,
number VARCHAR(255) ,
type VARCHAR(255) ,
PRIMARY KEY ( id )
)
ALTER TABLE Person_Phone
ADD CONSTRAINT UK_9uhc5itwc9h5gcng944pcaslf
UNIQUE (phones_id)
ALTER TABLE Person_Phone
ADD CONSTRAINT FKr38us2n8g5p9rj0b494sd3391
FOREIGN KEY (phones_id) REFERENCES Phone
ALTER TABLE Person_Phone
ADD CONSTRAINT FK2ex4e4p7w1cj310kg2woisjl2
FOREIGN KEY (Person_id) REFERENCES Person
::: tip CascadeType.ALL
级联机制允许您将实体状态转换从父实体传播到其子实体。
通过使用CascadeType标记父端。ALL属性,单向关联生命周期变得非常类似于值类型集合的生命周期。
:::
就像这样:
Person person = new Person( 1L );
person.getPhones().add( new Phone( 1L, "landline", "028-234-9876" ) );
person.getPhones().add( new Phone( 2L, "mobile", "072-122-9876" ) );
entityManager.persist( person );
INSERT INTO Person ( id )
VALUES ( 1 )
INSERT INTO Phone ( number, type, id )
VALUES ( '028-234-9876', 'landline', 1 )
INSERT INTO Phone ( number, type, id )
VALUES ( '072-122-9876', 'mobile', 2 )
INSERT INTO Person_Phone ( Person_id, phones_id )
VALUES ( 1, 1 )
INSERT INTO Person_Phone ( Person_id, phones_id )
VALUES ( 1, 2 )
在上面的例子中,一旦父实体被持久化,子实体也将被持久化。
双向级联操作
@Entity(name = "Person")
public static class Person {
@Id
private Long id;
@OneToMany(mappedBy = "person", cascade = CascadeType.ALL)
private List<Phone> phones = new ArrayList<>();
//Getters and setters are omitted for brevity
public void addPhone(Phone phone) {
phones.add( phone );
phone.setPerson( this );
}
public void removePhone(Phone phone) {
phones.remove( phone );
phone.setPerson( null );
}
}
@Entity(name = "Phone")
public static class Phone {
@Id
private Long id;
private String type;
@Column(name = "`number`", unique = true)
@NaturalId
private String number;
@ManyToOne
private Person person;
//Getters and setters are omitted for brevity
@Override
public boolean equals(Object o) {
if ( this == o ) {
return true;
}
if ( o == null || getClass() != o.getClass() ) {
return false;
}
Phone phone = (Phone) o;
return Objects.equals( number, phone.number );
}
@Override
public int hashCode() {
return Objects.hash( number );
}
}
CREATE TABLE Person (
id BIGINT NOT NULL, PRIMARY KEY (id)
)
CREATE TABLE Phone (
id BIGINT NOT NULL,
number VARCHAR(255),
type VARCHAR(255),
person_id BIGINT,
PRIMARY KEY (id)
)
ALTER TABLE Phone
ADD CONSTRAINT UK_l329ab0g4c1t78onljnxmbnp6
UNIQUE (number)
ALTER TABLE Phone
ADD CONSTRAINT FKmw13yfsjypiiq0i1osdkaeqpg
FOREIGN KEy (person_id) REFERENCES Person
person.addPhone( new Phone( 1L, "landline", "028-234-9876" ) );
person.addPhone( new Phone( 2L, "mobile", "072-122-9876" ) );
entityManager.flush();
person.removePhone( person.getPhones().get( 0 ) );
INSERT INTO Phone (number, person_id, type, id)
VALUES ( '028-234-9876', 1, 'landline', 1 )
INSERT INTO Phone (number, person_id, type, id)
VALUES ( '072-122-9876', 1, 'mobile', 2 )
UPDATE Phone
SET person_id = NULL, type = 'landline' where id = 1
这里还演示了删除操作,但只是更新了Phone
的外键为null
,如果我们要执行删除操作,那就把OneToMany
的orphanRemoval
属性声明为true
。
@OneToMany(mappedBy = "person", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Phone> phones = new ArrayList<>();
DELETE FROM Phone WHERE id = 1
排序
尽管它们在Java端使用List接口,但包不保留元素顺序。要保持集合元素的顺序,有两种可能:
@OrderBy
集合在使用子实体属性检索时排序@OrderColumn
集合在集合链接表中使用专用的排序列
OrderBy
排序
@Entity(name = "Person")
public static class Person {
@Id
private Long id;
@OneToMany(cascade = CascadeType.ALL)
@OrderBy("number")
private List<Phone> phones = new ArrayList<>();
//Getters and setters are omitted for brevity
}
@Entity(name = "Phone")
public static class Phone {
@Id
private Long id;
private String type;
@Column(name = "`number`")
private String number;
//Getters and setters are omitted for brevity
}
SELECT
phones0_.Person_id AS Person_i1_1_0_,
phones0_.phones_id AS phones_i2_1_0_,
unidirecti1_.id AS id1_2_1_,
unidirecti1_."number" AS number2_2_1_,
unidirecti1_.type AS type3_2_1_
FROM
Person_Phone phones0_
INNER JOIN
Phone unidirecti1_ ON phones0_.phones_id=unidirecti1_.id
WHERE
phones0_.Person_id = 1
ORDER BY
unidirecti1_."number"
::: tip 排序规则
@OrderBy
注释可以接受多个实体属性,并且每个属性也可以接受一个排序方向(例如@OrderBy(“name ASC,type DESC”)
)。
如果没有指定属性(例如@OrderBy
),则使用子实体表的主键进行排序。
:::
OrderColumn
排序
@OneToMany(cascade = CascadeType.ALL)
@OrderColumn(name = "order_id")
private List<Phone> phones = new ArrayList<>();
CREATE TABLE Person_Phone (
Person_id BIGINT NOT NULL ,
phones_id BIGINT NOT NULL ,
order_id INTEGER NOT NULL ,
PRIMARY KEY ( Person_id, order_id )
)
select
phones0_.Person_id as Person_i1_1_0_,
phones0_.phones_id as phones_i2_1_0_,
phones0_.order_id as order_id3_0_,
unidirecti1_.id as id1_2_1_,
unidirecti1_.number as number2_2_1_,
unidirecti1_.type as type3_2_1_
from
Person_Phone phones0_
inner join
Phone unidirecti1_
on phones0_.phones_id=unidirecti1_.id
where
phones0_.Person_id = 1
有了order_id列,Hibernate就可以在从数据库中获取列表之后在内存中对其进行排序。
自定义有序列表序数
您可以使用@ListIndexBase
注释来定制底层有序列表的序号。
@OneToMany(mappedBy = "person", cascade = CascadeType.ALL)
@OrderColumn(name = "order_id")
@ListIndexBase(100)
private List<Phone> phones = new ArrayList<>();
当插入两条Phone记录时,Hibernate这次将从100开始创建List索引。
Person person = new Person( 1L );
entityManager.persist( person );
person.addPhone( new Phone( 1L, "landline", "028-234-9876" ) );
person.addPhone( new Phone( 2L, "mobile", "072-122-9876" ) );
INSERT INTO Phone("number", person_id, type, id)
VALUES ('028-234-9876', 1, 'landline', 1)
INSERT INTO Phone("number", person_id, type, id)
VALUES ('072-122-9876', 1, 'mobile', 2)
UPDATE Phone
SET order_id = 100
WHERE id = 1
UPDATE Phone
SET order_id = 101
WHERE id = 2
自定义排序
@Entity(name = "Person")
public static class Person {
@Id
private Long id;
private String name;
@OneToMany(
mappedBy = "person",
cascade = CascadeType.ALL
)
@org.hibernate.annotations.OrderBy(
clause = "CHAR_LENGTH(name) DESC"
)
private List<Article> articles = new ArrayList<>();
//Getters and setters are omitted for brevity
}
@Entity(name = "Article")
public static class Article {
@Id
@GeneratedValue
private Long id;
private String name;
private String content;
@ManyToOne(fetch = FetchType.LAZY)
private Person person;
//Getters and setters are omitted for brevity
}
erson person = entityManager.find( Person.class, 1L );
assertEquals(
"High-Performance Hibernate",
person.getArticles().get( 0 ).getName()
);
select
a.person_id as person_i4_0_0_,
a.id as id1_0_0_,
a.content as content2_0_1_,
a.name as name3_0_1_,
a.person_id as person_i4_0_1_
from
Article a
where
a.person_id = ?
order by
CHAR_LENGTH(a.name) desc
::: warning 注意
这里使用的是org.hibernate.annotations.OrderBy
,而不是javax.persistence.OrderBy
。
:::
SortedSet
对于已排序的集合,实体映射必须使用SortedSet
接口。根据SortedSet
契约,所有元素都必须实现Comparable接口,因此必须提供排序逻辑。
::: warning 注意
依赖于子元素Comparable
实现逻辑给出的自然排序顺序的SortedSet
必须使用@SortNatural
Hibernate注释。
:::
@Entity(name = "Person")
public static class Person {
@Id
private Long id;
@OneToMany(cascade = CascadeType.ALL)
@SortNatural
private SortedSet<Phone> phones = new TreeSet<>();
//Getters and setters are omitted for brevity
}
@Entity(name = "Phone")
public static class Phone implements Comparable<Phone> {
@Id
private Long id;
private String type;
@NaturalId
@Column(name = "`number`")
private String number;
//Getters and setters are omitted for brevity
@Override
public int compareTo(Phone o) {
return number.compareTo( o.getNumber() );
}
@Override
public boolean equals(Object o) {
if ( this == o ) {
return true;
}
if ( o == null || getClass() != o.getClass() ) {
return false;
}
Phone phone = (Phone) o;
return Objects.equals( number, phone.number );
}
@Override
public int hashCode() {
return Objects.hash( number );
}
}
MappedSuperclass
当使用MappedSuperclass
时,继承只在域模型中可见,并且每个数据库表都包含基类和子类属性。
@MappedSuperclass
public static class Account {
@Id
private Long id;
private String owner;
private BigDecimal balance;
private BigDecimal interestRate;
//Getters and setters are omitted for brevity
}
@Entity(name = "DebitAccount")
public static class DebitAccount extends Account {
private BigDecimal overdraftFee;
//Getters and setters are omitted for brevity
}
@Entity(name = "CreditAccount")
public static class CreditAccount extends Account {
private BigDecimal creditLimit;
//Getters and setters are omitted for brevity
}
CREATE TABLE DebitAccount (
id BIGINT NOT NULL ,
balance NUMERIC(19, 2) ,
interestRate NUMERIC(19, 2) ,
owner VARCHAR(255) ,
overdraftFee NUMERIC(19, 2) ,
PRIMARY KEY ( id )
)
CREATE TABLE CreditAccount (
id BIGINT NOT NULL ,
balance NUMERIC(19, 2) ,
interestRate NUMERIC(19, 2) ,
owner VARCHAR(255) ,
creditLimit NUMERIC(19, 2) ,
PRIMARY KEY ( id )
)
不可变性
如果特定的实体是不可变的,最好使用@Immutable注释对其进行标记
@Entity(name = "Event")
@Immutable
public static class Event {
@Id
private Long id;
private Date createdOn;
private String message;
//Getters and setters are omitted for brevity
}
在内部,Hibernate将执行一些优化,例如:
- 减少内存占用,因为不需要为脏检查机制保游离状态
- 加速持久化上下文刷新阶段,因为不可变实体可以跳过脏检查过程
doInJPA( this::entityManagerFactory, entityManager -> {
Event event = new Event();
event.setId( 1L );
event.setCreatedOn( new Date( ) );
event.setMessage( "Hibernate User Guide rocks!" );
entityManager.persist( event );
} );
doInJPA( this::entityManagerFactory, entityManager -> {
Event event = entityManager.find( Event.class, 1L );
log.info( "Change event message" );
event.setMessage( "Hibernate User Guide" );
} );
doInJPA( this::entityManagerFactory, entityManager -> {
Event event = entityManager.find( Event.class, 1L );
assertEquals("Hibernate User Guide rocks!", event.getMessage());
} );
SELECT e.id AS id1_0_0_,
e.createdOn AS createdO2_0_0_,
e.message AS message3_0_0_
FROM event e
WHERE e.id = 1
-- Change event message
SELECT e.id AS id1_0_0_,
e.createdOn AS createdO2_0_0_,
e.message AS message3_0_0_
FROM event e
WHERE e.id = 1
当加载实体并试图更改其状态时,Hibernate将跳过任何修改,因此不会执行SQL UPDATE语句。
杂项
声明数据库默认值
@Entity(name = "Person")
@DynamicInsert
public static class Person {
@Id
private Long id;
@ColumnDefault("'N/A'")
private String name;
@ColumnDefault("-1")
private Long clientId;
//Getter and setters omitted for brevity
}
CREATE TABLE Person (
id BIGINT NOT NULL,
clientId BIGINT DEFAULT -1,
name VARCHAR(255) DEFAULT 'N/A',
PRIMARY KEY (id)
)
索引
自动模式生成工具使用@Index注释创建数据库索引。
@Entity
@Table(
name = "author",
indexes = @Index(
name = "idx_author_first_last_name",
columnList = "first_name, last_name",
unique = false
)
)
public static class Author {
@Id
@GeneratedValue
private Long id;
@Column(name = "first_name")
private String firstName;
@Column(name = "last_name")
private String lastName;
//Getter and setters omitted for brevity
}
create table author (
id bigint not null,
first_name varchar(255),
last_name varchar(255),
primary key (id)
)
create index idx_author_first_last_name
on author (first_name, last_name)
锁
Hibernate提供了在应用程序中实现这两种锁定的机制,您的锁定策略可以是乐观的,也可以是悲观的:
Optimistic
乐观锁定假设多个事务可以在不相互影响的情况下完成,因此事务可以在不锁定它们所影响的数据资源的情况下继续进行。在提交之前,每个事务都要验证是否没有其他事务修改了其数据。如果检查显示有冲突的修改,则提交事务回滚。Pessimistic
悲观锁定假设并发事务将相互冲突,并要求在读取资源后锁定资源,只有在应用程序使用完数据后才解锁资源。
Hibernate提供了两种不同的机制来存储版本信息,专用的版本号
或时间戳
。
::: tip 游离态
对于分离实例,版本或时间戳属性永远不能为空。不管您指定的其他未保存值策略是什么,Hibernate都将null版本或时间戳检测为瞬态的任何实例。声明一个可为空的版本或时间戳属性是避免Hibernate中传递性重连接问题的一种简单方法,在使用分配的标识符或组合键时尤其有用。
:::
乐观锁映射
JPA基于版本(顺序数字)或时间戳策略定义了对乐观锁定的支持。要启用这种风格的乐观锁定,只需将javax.persistence.Version
添加到定义乐观锁定值的persistent属性中。根据JPA,这些属性的有效类型限制为:
int
orInteger
short
orShort
long
orLong
java.sql.Timestamp
乐观锁定的版本号机制是通过@Version
注释提供的。
@Entity(name = "Person")
public static class Person {
@Id
@GeneratedValue
private Long id;
@Column(name = "`name`")
private String name;
@Version
private long version;
//Getters and setters are omitted for brevity
}
@Entity(name = "Person")
public static class Person {
@Id
@GeneratedValue
private Long id;
@Column(name = "`name`")
private String name;
@Version
@Source(value = SourceType.DB) //你还可以指定生成方式
private Timestamp version;
//Getters and setters are omitted for brevity
}
@Entity(name = "Person")
public static class Person {
@Id
@GeneratedValue
private Long id;
@Column(name = "`name`")
@OptimisticLock( excluded = true )
private String name;
@Version
private Instant version;
//Getters and setters are omitted for brevity
}
::: warning 时间戳
与版本号相比,时间戳是一种不太可靠的乐观锁定方式,但应用程序也可以将其用于其他目的。如果您在日期或日历属性类型上使用@Version注释,则会自动使用时间戳。
:::
::: tip 排除更新
默认情况下,每个实体属性修改都会触发版本递增。如果有一个实体属性不应该提高实体版本,那么您需要使用Hibernate @OptimisticLock注释它,如下面的示例所示。
:::
乐观锁定类型
虽然默认的@Version属性乐观锁定机制在许多情况下已经足够,但有时需要依赖实际的数据库行列值来防止丢失更新。
这个想法是,您可以让Hibernate使用实体的所有属性或仅更改的属性来执行“版本检查”。这是通过使用@OptimisticLocking
注释实现的,该注释定义了类型org.hibernate.annotations.OptimisticLockType
的单个属性。
Hibernate支持4种OptimisticLockTypes
:
-
NONE
即使存在
@Version
注释,乐观锁定也会被禁用 -
VERSION
(默认)如上所述,基于
@Version
执行乐观锁定(就是标识了Version
注解的字段) -
ALL
作为UPDATE/DELETE SQL语句扩展的WHERE子句限制的一部分,基于
所有字段
执行乐观锁定 -
DIRTY
作为UPDATE/DELETE SQL语句扩展的WHERE子句限制的一部分,基于
dirty
字段执行乐观锁定
@Entity(name = "Person")
@OptimisticLocking(type = OptimisticLockType.ALL)
@DynamicUpdate
public static class Person {
@Id
private Long id;
@Column(name = "`name`")
private String name;
private String country;
private String city;
@Column(name = "created_on")
private Timestamp createdOn;
//Getters and setters are omitted for brevity
}
Person person = entityManager.find( Person.class, 1L );
person.setCity( "Washington D.C." );
UPDATE
Person
SET
city=?
WHERE
id=?
AND city=?
AND country=?
AND created_on=?
AND "name"=?
-- binding parameter [1] as [VARCHAR] - [Washington D.C.]
-- binding parameter [2] as [BIGINT] - [1]
-- binding parameter [3] as [VARCHAR] - [New York]
-- binding parameter [4] as [VARCHAR] - [US]
-- binding parameter [5] as [TIMESTAMP] - [2016-11-16 16:05:12.876]
-- binding parameter [6] as [VARCHAR] - [John Doe]
在WHERE子句中使用了相关数据库行的所有列。如果在行加载后有任何列发生了更改,则不会有任何匹配,并且将抛出StaleStateException
或OptimisticLockException
。
@Entity(name = "Person")
@OptimisticLocking(type = OptimisticLockType.DIRTY)
@DynamicUpdate
@SelectBeforeUpdate
public static class Person {
@Id
private Long id;
@Column(name = "`name`")
private String name;
private String country;
private String city;
@Column(name = "created_on")
private Timestamp createdOn;
//Getters and setters are omitted for brevity
}
Person person = entityManager.find( Person.class, 1L );
person.setCity( "Washington D.C." );
UPDATE
Person
SET
city=?
WHERE
id=?
and city=?
-- binding parameter [1] as [VARCHAR] - [Washington D.C.]
-- binding parameter [2] as [BIGINT] - [1]
-- binding parameter [3] as [VARCHAR] - [New York]
这一次,在WHERE子句中只使用了更改的数据库列。
::: warning 警告
当使用OptimisticLockType.ALL
或OptimisticLockType.ALL
,您还应该使用@DynamicUpdate
,因为UPDATE语句必须考虑所有实体属性的值。
:::
悲观锁映射
::: tip 提示
Hibernate始终使用数据库的锁定机制,从不锁定内存中的对象。
:::
Long before JPA 1.0, Hibernate already defined various explicit locking strategies through its LockMode
enumeration. JPA comes with its own LockModeType
enumeration which defines similar strategies as the Hibernate-native LockMode
.
LockModeType | LockMode | Description |
---|---|---|
NONE | NONE | 没有锁在事务结束时,所有对象都切换到这种锁模式。通过调用update() 或saveOrUpdate() 与会话关联的对象也以这种锁模式开始。 |
READ and OPTIMISTIC | READ | 实体版本在当前运行的事务接近结束时被检查。 |
WRITE and OPTIMISTIC_FORCE_INCREMENT | WRITE | 即使实体没有改变,实体版本也会自动增加。 |
PESSIMISTIC_FORCE_INCREMENT | PESSIMISTIC_FORCE_INCREMENT | 该实体被悲观地锁定,并且它的版本会自动增加,即使该实体没有改变。 |
PESSIMISTIC_READ | PESSIMISTIC_READ | 如果数据库支持共享锁特性,则使用共享锁对实体进行悲观锁定。否则,将使用显式锁。 |
PESSIMISTIC_WRITE | PESSIMISTIC_WRITE , UPGRADE | 实体使用显式锁被锁定。 |
PESSIMISTIC_WRITE with a javax.persistence.lock.timeout setting of 0 | UPGRADE_NOWAIT | 如果行已经被锁定,锁获取请求会很快失败。 |
PESSIMISTIC_WRITE with a javax.persistence.lock.timeout setting of -2 | UPGRADE_SKIPLOCKED | 锁获取请求跳过已经锁定的行。它在Oracle和PostgreSQL 9.5中使用SELECT…FOR UPDATE SKIP LOCKED ,或者在SQL Server中使用SELECT…with (rowlock, updlock, readpast) 。 |
HQL & JPQL
缓存
拦截器和事件
CriteriaQuery
Spring boot JPA
Repository
Spring Data 存储库抽象中的中央接口是Repository
. 它需要域类来管理以及域类的 ID 类型作为类型参数。此接口主要用作标记接口,以捕获要使用的类型并帮助您发现扩展此接口的接口。该CrudRepository
接口为被管理的实体类提供了复杂的 CRUD 功能。
public interface CrudRepository<T, ID> extends Repository<T, ID> {
<S extends T> S save(S entity);
Optional<T> findById(ID primaryKey);
Iterable<T> findAll();
long count();
void delete(T entity);
boolean existsById(ID primaryKey);
// … more functionality omitted.
}
::: tip 温馨提示
JPA还提供了特定于持久性技术的抽象,例如JpaRepository
或MongoRepository
。一般我们直接继承JpaRepository
即可。
:::
在 之上CrudRepository
,还有一个PagingAndSortingRepository
抽象,它添加了额外的方法来简化对实体的分页访问:
public interface PagingAndSortingRepository<T, ID> extends CrudRepository<T, ID> {
Iterable<T> findAll(Sort sort);
Page<T> findAll(Pageable pageable);
}
比如:
PagingAndSortingRepository<User, Long> repository = // … get access to a bean
Page<User> users = repository.findAll(PageRequest.of(1, 20));
方法查询
关键词 | 样本 | JPQL 片段 |
---|---|---|
Distinct | findDistinctByLastnameAndFirstname | select distinct … where x.lastname = ?1 and x.firstname = ?2 |
And | findByLastnameAndFirstname | … where x.lastname = ?1 and x.firstname = ?2 |
Or | findByLastnameOrFirstname | … where x.lastname = ?1 or x.firstname = ?2 |
Is ,Equals | findByFirstname , findByFirstnameIs ,findByFirstnameEquals | … where x.firstname = ?1 |
Between | findByStartDateBetween | … where x.startDate between ?1 and ?2 |
LessThan | findByAgeLessThan | … where x.age < ?1 |
LessThanEqual | findByAgeLessThanEqual | … where x.age <= ?1 |
GreaterThan | findByAgeGreaterThan | … where x.age > ?1 |
GreaterThanEqual | findByAgeGreaterThanEqual | … where x.age >= ?1 |
After | findByStartDateAfter | … where x.startDate > ?1 |
Before | findByStartDateBefore | … where x.startDate < ?1 |
IsNull ,Null | findByAge(Is)Null | … where x.age is null |
IsNotNull ,NotNull | findByAge(Is)NotNull | … where x.age not null |
Like | findByFirstnameLike | … where x.firstname like ?1 |
NotLike | findByFirstnameNotLike | … where x.firstname not like ?1 |
StartingWith | findByFirstnameStartingWith | … where x.firstname like ?1 (与 append 绑定的参数% ) |
EndingWith | findByFirstnameEndingWith | … where x.firstname like ?1 (以 prepended 绑定的参数% ) |
Containing | findByFirstnameContaining | … where x.firstname like ?1 (参数绑定在 中% ) |
OrderBy | findByAgeOrderByLastnameDesc | … where x.age = ?1 order by x.lastname desc |
Not | findByLastnameNot | … where x.lastname <> ?1 |
In | findByAgeIn(Collection<Age> ages) | … where x.age in ?1 |
NotIn | findByAgeNotIn(Collection<Age> ages) | … where x.age not in ?1 |
True | findByActiveTrue() | … where x.active = true |
False | findByActiveFalse() | … where x.active = false |
IgnoreCase | findByFirstnameIgnoreCase | … where UPPER(x.firstname) = UPPER(?1) |
特殊参数处理
要处理查询中的参数,请定义前面示例中已经看到的方法参数。除此之外,该基础架构还可以识别某些特定类型,例如Pageable
and Sort
,以便动态地将分页和排序应用于您的查询。以下示例演示了这些功能:
Page<User> findByLastname(String lastname, Pageable pageable);
Slice<User> findByLastname(String lastname, Pageable pageable);
List<User> findByLastname(String lastname, Sort sort);
List<User> findByLastname(String lastname, Pageable pageable);
::: tip 温馨提示
API 接受Sort
和Pageable
期望将非null
值传递给方法。如果您不想应用任何排序或分页,请使用Sort.unsorted()
and Pageable.unpaged()
。
:::
Sort sort = Sort.by("firstname").ascending()
.and(Sort.by("lastname").descending());
TypedSort<Person> person = Sort.sort(Person.class);
Sort sort = person.by(Person::getFirstname).ascending()
.and(person.by(Person::getLastname).descending());
QSort sort = QSort.by(QPerson.firstname.asc())
.and(QSort.by(QPerson.lastname.desc()));
限制查询结果
first
您可以使用or关键字来限制查询方法的结果top
,您可以互换使用它们。您可以将可选数值附加到top
或first
指定要返回的最大结果大小。如果省略该数字,则假定结果大小为 1。以下示例显示了如何限制查询大小:
User findFirstByOrderByLastnameAsc();
User findTopByOrderByAgeDesc();
Page<User> queryFirst10ByLastname(String lastname, Pageable pageable);
Slice<User> findTop3ByLastname(String lastname, Pageable pageable);
List<User> findFirst10ByLastname(String lastname, Sort sort);
List<User> findTop10ByLastname(String lastname, Pageable pageable);
限制表达式还支持Distinct
支持不同查询的数据存储的关键字。Optional
此外,对于将结果集限制为一个实例的查询,支持使用关键字将结果包装到其中。
返回集合或迭代的存储库方法
返回多个结果的查询方法可以使用标准的 Java Iterable
、List
和Set
. 除此之外,我们还支持返回 Spring Data 的Streamable
自定义扩展Iterable
,以及Vavr提供的集合类型。请参阅解释所有可能的查询方法返回类型的附录。
您可以Streamable
用作任何集合类型的替代品Iterable
或任何集合类型。它提供了方便的方法来访问非并行Stream
(缺少Iterable
)以及直接….filter(…)
和….map(…)
覆盖元素并将其连接Streamable
到其他元素的能力:
interface PersonRepository extends Repository<Person, Long> {
Streamable<Person> findByFirstnameContaining(String firstname);
Streamable<Person> findByLastnameContaining(String lastname);
}
Streamable<Person> result = repository.findByFirstnameContaining("av")
.and(repository.findByLastnameContaining("ea"));
可空性注释
您可以使用Spring Framework 的可空性注释来表达存储库方法的可空性约束。它们在运行时提供了一种工具友好的方法和选择加入null
检查,如下所示:
@NonNullApi
:在包级别上用于声明参数和返回值的默认行为分别是既不接受也不产生null
值。@NonNull
: 用于不能使用的参数或返回值null
(在适用的情况下不需要用于参数和返回值@NonNullApi
)。@Nullable
: 用在参数或返回值上即可null
。
Spring 注释使用JSR 305注释(一种休眠但广泛使用的 JSR)进行元注释。JSR 305 元注释让工具供应商(例如IDEA、Eclipse和Kotlin)以通用方式提供空安全支持,而无需对 Spring 注释进行硬编码支持。
一旦非空默认设置到位,存储库查询方法调用将在运行时验证可空性约束。如果查询结果违反了定义的约束,则会引发异常。当方法将返回null
但被声明为不可为空(默认情况下,在存储库所在的包上定义注释)时,就会发生这种情况。如果您想再次选择可空结果,请有选择地使用@Nullable
单个方法。使用本节开头提到的结果包装类型继续按预期工作:空结果被转换为表示缺席的值。
package com.acme; //1
import org.springframework.lang.Nullable;
interface UserRepository extends Repository<User, Long> {
User getByEmailAddress(EmailAddress emailAddress); //2
@Nullable
User findByEmailAddress(@Nullable EmailAddress emailAdress); //3
Optional<User> findOptionalByEmailAddress(EmailAddress emailAddress); //4
}
1 | 存储库位于我们为其定义了非空行为的包(或子包)中。 |
---|---|
2 | EmptyResultDataAccessException 当查询不产生结果时抛出一个。IllegalArgumentException 当emailAddress 交给方法时抛出一个null 。 |
3 | null 当查询没有产生结果时返回。也接受null 作为 的值emailAddress 。 |
4 | Optional.empty() 当查询没有产生结果时返回。IllegalArgumentException 当emailAddress 交给方法时抛出一个null 。 |
基于 Kotlin 的存储库中的可空性
interface UserRepository : Repository<User, String> {
fun findByUsername(username: String): User
fun findByFirstname(firstname: String?): User?
}
该方法将参数和结果都定义为不可为空(Kotlin 默认)。Kotlin 编译器拒绝传递给方法的方法调用null 。如果查询产生空结果,EmptyResultDataAccessException 则抛出 an。 | 1 |
---|---|
此方法接受null 参数firstname 并null 在查询未产生结果时返回。 | 2 |
异步查询结果
Future<User> findByFirstname(String firstname); //1
@Async
CompletableFuture<User> findOneByFirstname(String firstname); //2
@Async
ListenableFuture<User> findOneByLastname(String lastname); //3
1 | 用作java.util.concurrent.Future 返回类型。 |
---|---|
2 | 使用 Java 8java.util.concurrent.CompletableFuture 作为返回类型。 |
3 | 使用 aorg.springframework.util.concurrent.ListenableFuture 作为返回类型。 |
使用@Query
使用命名查询来声明实体查询是一种有效的方法,并且适用于少量查询。由于查询本身与运行它们的 Java 方法相关联,因此您实际上可以使用 Spring Data JPA@Query
注释直接绑定它们,而不是将它们注释到域类。这将域类从持久性特定信息中解放出来,并将查询定位到存储库接口。
注释到查询方法的查询优先于使用定义的查询@NamedQuery
或在 中声明的命名查询orm.xml
。
以下示例显示了使用@Query
注释创建的查询:
public interface UserRepository extends JpaRepository<User, Long> {
@Query("select u from User u where u.emailAddress = ?1")
User findByEmailAddress(String emailAddress);
@Query("select u from User u where u.firstname like %?1")
List<User> findByFirstnameEndsWith(String firstname);
@Query(value = "SELECT * FROM USERS WHERE EMAIL_ADDRESS = ?1", nativeQuery = true)
User findByEmailAddress(String emailAddress);
@Query(value = "SELECT * FROM USERS WHERE LASTNAME = ?1",
countQuery = "SELECT count(*) FROM USERS WHERE LASTNAME = ?1",
nativeQuery = true)
Page<User> findByLastname(String lastname, Pageable pageable);
}
排序
public interface UserRepository extends JpaRepository<User, Long> {
@Query("select u from User u where u.lastname like ?1%")
List<User> findByAndSort(String lastname, Sort sort);
@Query("select u.id, LENGTH(u.firstname) as fn_len from User u where u.lastname like ?1%")
List<Object[]> findByAsArrayAndSort(String lastname, Sort sort);
}
repo.findByAndSort("lannister", Sort.by("firstname")); //1
repo.findByAndSort("stark", Sort.by("LENGTH(firstname)")); //2
repo.findByAndSort("targaryen", JpaSort.unsafe("LENGTH(firstname)")); //3
repo.findByAsArrayAndSort("bolton", Sort.by("fn_len")); //4
1 | Sort 指向域模型中属性的有效表达式。 |
---|---|
2 | Sort 包含函数调用无效。抛出异常。 |
3 | 有效Sort 包含显式unsafe Order 。 |
4 | Sort 指向别名函数的有效表达式。 |
传参
默认情况下,Spring Data JPA 使用基于位置的参数绑定,如前面所有示例中所述。这使得查询方法在重构参数位置时有点容易出错。为了解决这个问题,可以使用@Param
注解给方法参数一个具体的名称,并在查询中绑定名称,如下例所示:
public interface UserRepository extends JpaRepository<User, Long> {
@Query("select u from User u where u.firstname = :firstname or u.lastname = :lastname")
User findByLastnameOrFirstname(@Param("lastname") String lastname,
@Param("firstname") String firstname); //命名参数可以无序
}
SpEL 表达式
@Query("select u from User u where u.firstname = ?1 and u.firstname=?#{[0]} and u.emailAddress = ?#{principal.emailAddress}")
List<User> findByFirstnameAndCurrentUserWithCustomQuery(String firstname);
@Query("select u from User u where u.lastname like %:#{[0]}% and u.lastname like %:lastname%")
List<User> findByLastnameWithSpelExpression(@Param("lastname") String lastname);
@Query("select u from User u where u.firstname like %?#{escape([0])}% escape ?#{escapeCharacter()}")
List<User> findContainingEscaped(String namePart);
更新语句
前面的所有部分都描述了如何声明查询以访问给定的实体或实体集合。您可以使用“ Spring Data Repositories 的自定义实现”中描述的自定义方法工具添加自定义修改行为。由于这种方法对于全面的自定义功能是可行的,因此您可以通过使用 注释查询方法来修改只需要参数绑定的查询@Modifying
,如下例所示:
@Modifying
@Query("update User u set u.firstname = ?1 where u.lastname = ?2")
int setFixedFirstnameFor(String firstname, String lastname);
这样做会触发注释到方法的查询作为更新查询而不是选择查询。由于在EntityManager
执行修改查询后可能包含过时的实体,我们不会自动清除它(有关详细信息,请参阅JavaDoc )EntityManager.clear()
,因为这有效地删除了所有在EntityManager
. 如果您希望EntityManager
自动清除 ,可以将@Modifying
注解的clearAutomatically
属性设置为true
。
@Modifying
注释仅与注释组合相关@Query
。派生查询方法或自定义方法不需要此注解。
删除语句
@Modifying
@Query("delete from User u where u.role.id = ?1")
void deleteInBulkByRoleId(long roleId);
存储过程
/;
DROP procedure IF EXISTS plus1inout
/;
CREATE procedure plus1inout (IN arg int, OUT res int)
BEGIN ATOMIC
set res = arg + 1;
END
/;
NamedStoredProcedureQuery
可以使用实体类型上的注释来配置存储过程的元数据。
@Entity
@NamedStoredProcedureQuery(name = "User.plus1", procedureName = "plus1inout", parameters = {
@StoredProcedureParameter(mode = ParameterMode.IN, name = "arg", type = Integer.class),
@StoredProcedureParameter(mode = ParameterMode.OUT, name = "res", type = Integer.class) })
public class User {}
请注意,@NamedStoredProcedureQuery
存储过程有两个不同的名称。 name
是 JPA 使用的名称。procedureName
是存储过程在数据库中的名称。
您可以通过多种方式从存储库方法中引用存储过程。要调用的存储过程可以使用注解的value
orprocedureName
属性直接定义。@Procedure
这直接引用数据库中的存储过程,并忽略任何通过@NamedStoredProcedureQuery
.
或者,您可以将属性指定@NamedStoredProcedureQuery.name
为@Procedure.name
属性。如果既未配置value
,procedureName
也未name
配置,则将存储库方法的名称用作name
属性。
以下示例显示了如何引用显式映射的过程:
@Procedure("plus1inout")
Integer explicitlyNamedPlus1inout(Integer arg);
@Procedure(procedureName = "plus1inout")
Integer callPlus1InOut(Integer arg);
@Procedure
Integer plus1inout(@Param("arg") Integer arg);
@Procedure(name = "User.plus1IO")
Integer entityAnnotatedCustomNamedProcedurePlus1IO(@Param("arg") Integer arg);
JpaSpecificationExecutor
Spring Data JPA 采用 Eric Evans 的书“领域驱动设计”中的规范概念,遵循相同的语义并提供 API 以使用 JPA 标准 API 定义此类规范。为了支持规范,您可以使用接口扩展您的存储库接口JpaSpecificationExecutor
,如下所示:
public interface CustomerRepository extends CrudRepository<Customer, Long>, JpaSpecificationExecutor<Customer> {
List<Customer> findAll(Specification<Customer> spec);
}
Example Query
public class Person {
@Id
private String id;
private String firstname;
private String lastname;
private Address address;
// … getters and setters omitted
}
Person person = new Person();
person.setFirstname("Dave");
Example<Person> example = Example.of(person);
Example Matcher
Person person = new Person(); //1
person.setFirstname("Dave"); //2
ExampleMatcher matcher = ExampleMatcher.matching() //3
.withIgnorePaths("lastname") //4
.withIncludeNullValues() //5
.withStringMatcher(StringMatcher.ENDING); //6
Example<Person> example = Example.of(person, matcher); //7
1 | 创建域对象的新实例。 |
---|---|
2 | 设置属性。 |
3 | 创建一个ExampleMatcher 以期望所有值都匹配。即使没有进一步的配置,它也可以在这个阶段使用。 |
4 | 构造一个新ExampleMatcher 的忽略lastname 属性路径。 |
5 | 构造一个新ExampleMatcher 的忽略lastname 属性路径并包含空值。 |
6 | 构造一个新ExampleMatcher 的来忽略lastname 属性路径,包括空值,并执行后缀字符串匹配。 |
7 | Example 根据域对象和配置的ExampleMatcher . |
事务
默认情况下,继承自的存储库实例上的 CRUD 方法SimpleJpaRepository
是事务性的。对于读取操作,事务配置readOnly
标志设置为true
。所有其他人都配置了一个普通的@Transactional
,以便应用默认的事务配置。由事务存储库片段支持的存储库方法从实际片段方法继承事务属性。
如果您需要调整存储库中声明的方法之一的事务配置,请在存储库接口中重新声明该方法,如下所示:
public interface UserRepository extends CrudRepository<User, Long> {
@Override
@Transactional(timeout = 10)
public List<User> findAll();
// Further query method declarations
}
锁
interface UserRepository extends Repository<User, Long> {
// Plain query method
@Lock(LockModeType.READ)
List<User> findByLastname(String lastname);
// Redeclaration of a CRUD method
@Lock(LockModeType.READ)
List<User> findAll();
}
审计
基于注释的审计元数据
Spring提供@CreatedBy
并@LastModifiedBy
捕获创建或修改实体的用户,以及@CreatedDate
捕获@LastModifiedDate
更改发生的时间。
class Customer {
@CreatedBy
private User user;
@CreatedDate
private Instant createdDate;
// … further properties omitted
}
基于接口的审计元数据
如果您不想使用注释来定义审核元数据,您可以让您的域类实现该Auditable
接口。它公开了所有审计属性的 setter 方法。
AuditorAware
如果您使用@CreatedBy
或@LastModifiedBy
,审计基础架构需要以某种方式了解当前主体。为此,我们提供了一个AuditorAware<T>
SPI 接口,您必须实现该接口来告诉基础设施当前与应用程序交互的用户或系统是谁。泛型类型T
定义了属性注释@CreatedBy
或@LastModifiedBy
必须是什么类型。
class SpringSecurityAuditorAware implements AuditorAware<User> {
@Override
public Optional<User> getCurrentAuditor() {
return Optional.ofNullable(SecurityContextHolder.getContext())
.map(SecurityContext::getAuthentication)
.filter(Authentication::isAuthenticated)
.map(Authentication::getPrincipal)
.map(User.class::cast);
}
}
声明
该文章摘抄于Habernate官网预Spring Data JPA官网,仅作为个人的重点笔记和分享使用。