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@Basicprivate Integer id;@Basicprivate String sku;@Basicprivate 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[] or Byte[]
  • char[] or Character[]
  • enums

::: tip 提示

如果考虑到提供者的可移植性,您应该只使用这些基本类型。
:::

枚举映射

基本映射

public enum PhoneType {LAND_LINE,MOBILE;
}
@Entity(name = "Phone")
public static class Phone {@Idprivate 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 {@Idprivate 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> {@Overridepublic Long convertToDatabaseColumn(Money attribute) {return attribute == null ? null : attribute.getCents();}@Overridepublic 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@GeneratedValueprivate 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 {@Idprivate 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 {@Idprivate 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 {@Idprivate 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@GeneratedValueprivate Long id;@Column(name = "`timestamp`")@CreationTimestampprivate 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 {@Idprivate Long id;@NaturalIdprivate 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 {@Idprivate 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 {@Idprivate 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@GeneratedValueprivate 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@GeneratedValueprivate 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@GeneratedValueprivate 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()

queryUserEntity便是。

@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 {@Idprivate 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@GeneratedValueprivate Long id;private String title;private String author;//Getters and setters are omitted for brevity@Overridepublic 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 );}@Overridepublic 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 )@Versionprivate int version;@Idpublic 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 {@Idprivate 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@GeneratedValueprivate Long id;private String title;private String author;//Getters and setters are omitted for brevity
}

GeneratedValueGenerationType的属性有以下值:

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@GeneratedValueprivate 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@GeneratedValueprivate 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@GeneratedValueprivate 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 brevitypublic 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@GeneratedValueprivate 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@GeneratedValueprivate Long id;@OneToMany(mappedBy = "person", cascade = CascadeType.ALL, orphanRemoval = true)private List<Phone> phones = new ArrayList<>();//Getters and setters are omitted for brevitypublic 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@GeneratedValueprivate Long id;@NaturalId@Column(name = "`number`", unique = true)private String number;@ManyToOneprivate Person person;//Getters and setters are omitted for brevity@Overridepublic 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 );}@Overridepublic 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@GeneratedValueprivate Long id;//Getters and setters are omitted for brevity}@Entity(name = "Phone")
public static class Phone {@Id@GeneratedValueprivate 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还会影响HQLCriteria中如何将关联视为“隐式连接”。当存在物理外键时,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 {@Idprivate 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 {@Idprivate 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 PhoneALTER 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 {@Idprivate Long id;@OneToMany(mappedBy = "person", cascade = CascadeType.ALL)private List<Phone> phones = new ArrayList<>();//Getters and setters are omitted for brevitypublic 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 {@Idprivate Long id;private String type;@Column(name = "`number`", unique = true)@NaturalIdprivate String number;@ManyToOneprivate Person person;//Getters and setters are omitted for brevity@Overridepublic 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 );}@Overridepublic 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,如果我们要执行删除操作,那就把OneToManyorphanRemoval 属性声明为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 {@Idprivate 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 {@Idprivate Long id;private String type;@Column(name = "`number`")private String number;//Getters and setters are omitted for brevity}
SELECTphones0_.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_
FROMPerson_Phone phones0_
INNER JOINPhone unidirecti1_ ON phones0_.phones_id=unidirecti1_.id
WHEREphones0_.Person_id = 1
ORDER BYunidirecti1_."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 )
)
selectphones0_.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_
fromPerson_Phone phones0_
inner joinPhone unidirecti1_on phones0_.phones_id=unidirecti1_.id
wherephones0_.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 = 1UPDATE Phone
SET order_id = 101
WHERE id = 2

自定义排序

@Entity(name = "Person")
public static class Person {@Idprivate 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@GeneratedValueprivate 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()
);
selecta.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_
fromArticle a
wherea.person_id = ?
order byCHAR_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 {@Idprivate Long id;@OneToMany(cascade = CascadeType.ALL)@SortNaturalprivate SortedSet<Phone> phones = new TreeSet<>();//Getters and setters are omitted for brevity}@Entity(name = "Phone")
public static class Phone implements Comparable<Phone> {@Idprivate Long id;private String type;@NaturalId@Column(name = "`number`")private String number;//Getters and setters are omitted for brevity@Overridepublic int compareTo(Phone o) {return number.compareTo( o.getNumber() );}@Overridepublic 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 );}@Overridepublic int hashCode() {return Objects.hash( number );}
}

MappedSuperclass

当使用MappedSuperclass时,继承只在域模型中可见,并且每个数据库表都包含基类和子类属性。

@MappedSuperclass
public static class Account {@Idprivate 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 {@Idprivate 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 messageSELECT 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 {@Idprivate 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@GeneratedValueprivate 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_nameon author (first_name, last_name)

Hibernate提供了在应用程序中实现这两种锁定的机制,您的锁定策略可以是乐观的,也可以是悲观的:

  • Optimistic 乐观锁定假设多个事务可以在不相互影响的情况下完成,因此事务可以在不锁定它们所影响的数据资源的情况下继续进行。在提交之前,每个事务都要验证是否没有其他事务修改了其数据。如果检查显示有冲突的修改,则提交事务回滚。
  • Pessimistic悲观锁定假设并发事务将相互冲突,并要求在读取资源后锁定资源,只有在应用程序使用完数据后才解锁资源。

Hibernate提供了两种不同的机制来存储版本信息,专用的版本号时间戳

::: tip 游离态

对于分离实例,版本或时间戳属性永远不能为空。不管您指定的其他未保存值策略是什么,Hibernate都将null版本或时间戳检测为瞬态的任何实例。声明一个可为空的版本或时间戳属性是避免Hibernate中传递性重连接问题的一种简单方法,在使用分配的标识符或组合键时尤其有用。

:::

乐观锁映射

JPA基于版本(顺序数字)或时间戳策略定义了对乐观锁定的支持。要启用这种风格的乐观锁定,只需将javax.persistence.Version添加到定义乐观锁定值的persistent属性中。根据JPA,这些属性的有效类型限制为:

  • int or Integer
  • short or Short
  • long or Long
  • java.sql.Timestamp

乐观锁定的版本号机制是通过@Version注释提供的。

@Entity(name = "Person")
public static class Person {@Id@GeneratedValueprivate Long id;@Column(name = "`name`")private String name;@Versionprivate long version;//Getters and setters are omitted for brevity}
@Entity(name = "Person")
public static class Person {@Id@GeneratedValueprivate 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@GeneratedValueprivate Long id;@Column(name = "`name`")@OptimisticLock( excluded = true )   private String name;@Versionprivate 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 {@Idprivate 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." );
UPDATEPerson
SETcity=?
WHEREid=?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子句中使用了相关数据库行的所有列。如果在行加载后有任何列发生了更改,则不会有任何匹配,并且将抛出StaleStateExceptionOptimisticLockException

@Entity(name = "Person")
@OptimisticLocking(type = OptimisticLockType.DIRTY)
@DynamicUpdate
@SelectBeforeUpdate
public static class Person {@Idprivate 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." );
UPDATEPerson
SETcity=?
WHEREid=?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.ALLOptimisticLockType.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还提供了特定于持久性技术的抽象,例如JpaRepositoryMongoRepository。一般我们直接继承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)

特殊参数处理

要处理查询中的参数,请定义前面示例中已经看到的方法参数。除此之外,该基础架构还可以识别某些特定类型,例如Pageableand 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 接受SortPageable期望将非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,您可以互换使用它们。您可以将可选数值附加到topfirst指定要返回的最大结果大小。如果省略该数字,则假定结果大小为 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 IterableListSet. 除此之外,我们还支持返回 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;                                          //1import org.springframework.lang.Nullable;interface UserRepository extends Repository<User, Long> {User getByEmailAddress(EmailAddress emailAddress);          //2@NullableUser findByEmailAddress(@Nullable EmailAddress emailAdress);       //3Optional<User> findOptionalByEmailAddress(EmailAddress emailAddress);   //4
}
1 存储库位于我们为其定义了非空行为的包(或子包)中。
2 EmptyResultDataAccessException当查询不产生结果时抛出一个。IllegalArgumentExceptionemailAddress交给方法时抛出一个null
3 null当查询没有产生结果时返回。也接受null作为 的值emailAddress
4 Optional.empty()当查询没有产生结果时返回。IllegalArgumentExceptionemailAddress交给方法时抛出一个null

基于 Kotlin 的存储库中的可空性

interface UserRepository : Repository<User, String> {fun findByUsername(username: String): User     fun findByFirstname(firstname: String?): User?
}
该方法将参数和结果都定义为不可为空(Kotlin 默认)。Kotlin 编译器拒绝传递给方法的方法调用null。如果查询产生空结果,EmptyResultDataAccessException则抛出 an。 1
此方法接受null参数firstnamenull在查询未产生结果时返回。 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 ATOMICset 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是存储过程在数据库中的名称。

您可以通过多种方式从存储库方法中引用存储过程。要调用的存储过程可以使用注解的valueorprocedureName属性直接定义。@Procedure这直接引用数据库中的存储过程,并忽略任何通过@NamedStoredProcedureQuery.

或者,您可以将属性指定@NamedStoredProcedureQuery.name@Procedure.name属性。如果既未配置valueprocedureName也未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 {@Idprivate 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");                            //2ExampleMatcher matcher = ExampleMatcher.matching()      //3.withIgnorePaths("lastname")                          //4.withIncludeNullValues()                              //5.withStringMatcher(StringMatcher.ENDING);             //6Example<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 {@CreatedByprivate User user;@CreatedDateprivate Instant createdDate;// … further properties omitted
}

基于接口的审计元数据

如果您不想使用注释来定义审核元数据,您可以让您的域类实现该Auditable接口。它公开了所有审计属性的 setter 方法。

AuditorAware

如果您使用@CreatedBy@LastModifiedBy,审计基础架构需要以某种方式了解当前主体。为此,我们提供了一个AuditorAware<T>SPI 接口,您必须实现该接口来告诉基础设施当前与应用程序交互的用户或系统是谁。泛型类型T定义了属性注释@CreatedBy@LastModifiedBy必须是什么类型。

class SpringSecurityAuditorAware implements AuditorAware<User> {@Overridepublic 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官网,仅作为个人的重点笔记和分享使用。

Spring Boot JPA 2.7.2相关推荐

  1. spring boot jpa级联保存

    spring boot jpa级联保存 CascadeType oneToMany关系 one的一方中加 @OneToMany(fetch = FetchType.EAGER, cascade = C ...

  2. 解决spring boot+JPA实现操作数据库时编辑时也变成了新增

    场景:使用spring boot+JPA框架开发项目的时候,新增数据是正常的,但是编辑有时候会变成新增,JPA判断是否新增对象有两个方法:1根据id,2根据版本号.我在开发项目中用的是根据版本号进行判 ...

  3. Spring Boot JPA 中transaction的使用

    文章目录 @Transactional的实现 @Transactional的使用 Transaction的传播级别 REQUIRED SUPPORTS MANDATORY NEVER NOT_SUPP ...

  4. Spring Boot JPA中关联表的使用

    文章目录 添加依赖 构建Entity 构建Repository 构建初始数据 测试 Spring Boot JPA中关联表的使用 本文中,我们会将会通过一个Book和Category的关联关系,来讲解 ...

  5. Spring Boot JPA的查询语句

    文章目录 准备工作 Containing, Contains, IsContaining 和 Like StartsWith EndsWith 大小写不敏感 Not @Query Spring Boo ...

  6. Spring Boot JPA中使用@Entity和@Table

    文章目录 默认实现 使用@Table自定义表格名字 在JPQL Queries中重写表格名字 Spring Boot JPA中使用@Entity和@Table 本文中我们会讲解如何在Spring Bo ...

  7. Spring Boot JPA中java 8 的应用

    文章目录 Optional Stream API CompletableFuture Spring Boot JPA中java 8 的应用 上篇文章中我们讲到了如何在Spring Boot中使用JPA ...

  8. Spring Boot + JPA + Freemarker 实现后端分页 完整示例

    Spring Boot + JPA + Freemarker 实现后端分页 完整示例 界面效果 螢幕快照 2017-07-28 15.34.42.png 螢幕快照 2017-07-28 15.34.2 ...

  9. (转)Spring Boot(五):Spring Boot Jpa 的使用

    http://www.ityouknow.com/springboot/2016/08/20/spring-boot-jpa.html 在上篇文章Spring Boot(二):Web 综合开发中简单介 ...

  10. JPA的单向一对多关联(oneToMany)实现示例(基于Spring Boot + JPA +MySQL,表自动维护)

    本篇的环境 本篇基于Spring Boot + JPA+ MySQL. 表自动维护: 配置 ddl-auto: update,使用 Hibernate 根据类自动维护表. 本篇的示例 这里有两个类: ...

最新文章

  1. 2022-2028年全球与中国氢碘化物市场智研瞻分析报告
  2. JavaScript基础笔记集合(转)
  3. android开发常见的设计模式,Android开发有哪些常用设计模式?
  4. 关于istringstream用法的一个坑
  5. 新版ffmpeg PCM编码到AAC,swr_convert转换采样精度,稍微修改兼容PCM编码为G711A及MP3,记录下。
  6. WinDbg学习笔记(二)--字符串访问断点
  7. C++11 统一初始化(Uniform Initialization)
  8. oracle中区间大小,Oracle的逻辑结构(表空间、段、区间、块)——总结
  9. getopt java_Java命令行界面(第28部分):getopt4j
  10. 关于OC中的block自己的一些理解(二)
  11. java中的criteria_java-jpa-criteriaBuilder使用入门
  12. R语言ETL工程:插入与合并(add/bind)
  13. ubuntu下screen的使用
  14. 网络爬虫的基本原理(一)
  15. ALtera DE2开发板学习04
  16. java 阿拉伯数字日期转换为中文大写日期方法_java方法转换大写日期及人民币大写转换方式 .txt...
  17. Win 批处理生成文件目录树
  18. Molten 功能简介以及使用指南
  19. html没有注册类,电脑提示没有注册类别的解决方法大全
  20. 布法罗计算机专业怎么样,布法罗大学 University at Buffalo

热门文章

  1. 机器学习笔记之深度信念网络(三)贪心逐层预训练算法
  2. Kaggle教程 机器学习入门学习笔记
  3. matlalb与python编程进行动力总成悬置模态计算对比——困惑待解
  4. 镁光SDRAM Verilog模型使用
  5. 网络篇 EIGRP协议-27
  6. 教你如何删除流氓软件(以一个压缩软件为例)
  7. 红尘自是有情痴,天涯望断不言悔
  8. 汉语字典数据表 mysql_mysql数据库字典表
  9. 【NCL】ENSO冷暖事件海温异常合成与t检验
  10. 编译原理 C语言词法分析程序的设计与实现