当前位置 : 首页 » 文章分类 :  开发  »  Spring-Data-JPA

Spring-Data-JPA

JPA(Java Persistence API) 使用笔记


SpringBoot2 JPA 多数据源配置

SpringBoot2.x 默认使用的数据源是 HikariCP,它使用的连接参数是 jdbc-url 而不是 url
spring.datasource.realinfo.jdbc-url=jdbc:mysql://localhost:3306/test?characterEncoding=UTF-8&autoReconnect=true&useSSL=false
否则报错
java.lang.IllegalArgumentException: jdbcUrl is required with driverClassName

SpringBoot2.1之JPA多数据源配置
https://blog.csdn.net/qq_30643885/article/details/96143586

SpringBoot和JPA多数据源整合
https://zhuanlan.zhihu.com/p/91448889


Spring Data

Pageable 和 Sort

Pageable pageable = PageRequest.of(0, 10, Sort.by("id"));
Pageable pageable2 = PageRequest.of(0, 10, Sort.by(Sort.Direction.DESC, "update_time"));

分页查询示例

// 根据更新时间范围分页查询档案
public Page<ArchiveDO> getByUpdateTimePageable(Date startTime, Date endTime, Pageable pageable) {
    if (Objects.isNull(pageable)) {
        pageable = PageRequest.of(0, 100, Sort.by("id"));
    }
    BooleanBuilder booleanBuilder = new BooleanBuilder();
    booleanBuilder.and(qArchiveDO.updateTime.goe(startTime.getTime()));
    booleanBuilder.and(qArchiveDO.updateTime.lt(endTime.getTime()));
    return findAll(booleanBuilder, pageable);
}

查不到数据时返回的 Page<T> 也不是 null,有分页信息

{
    "content":[

    ],
    "pageable":{
        "sort":{
            "sorted":true,
            "unsorted":false,
            "empty":false
        },
        "offset":0,
        "pageNumber":0,
        "pageSize":100,
        "paged":true,
        "unpaged":false
    },
    "totalElements":0,
    "last":true,
    "totalPages":0,
    "size":100,
    "number":0,
    "first":true,
    "sort":{
        "sorted":true,
        "unsorted":false,
        "empty":false
    },
    "numberOfElements":0,
    "empty":true
}

CrudRepository

@NoRepositoryBean
public interface CrudRepository<T, ID> extends Repository<T, ID> {
    <S extends T> S save(S entity);
    <S extends T> Iterable<S> saveAll(Iterable<S> entities);
    Optional<T> findById(ID id);
    boolean existsById(ID id);
    Iterable<T> findAll();
    Iterable<T> findAllById(Iterable<ID> ids);
    long count();
    void deleteById(ID id);
    void delete(T entity);
    void deleteAll(Iterable<? extends T> entities);
    void deleteAll();
}

save

当 POJO 的 id 存在时,调用 save 方法可能有两种情况
若 db 中这个 id 对应的字段不存在,则插入
若 db 中这个 id 对应的字段存在,则更新


Hibernate配置项

hibernate.hbm2ddl.auto

hibernate.hbm2ddl.auto 参数的作用主要用于:自动创建,更新,验证数据库表结构。
在 SpringBoot 中的配置项是 spring.jpa.properties.hibernate.hbm2ddl.auto
有几种配置:
update 最常用的属性值,第一次加载 Hibernate 时创建数据表(前提是需要先有数据库),以后加载 Hibernate 时不会删除上一次生成的表,会根据实体更新,只新增字段,不会删除字段(即使实体中已经删除)。
validate 每次加载 Hibernate 时都会验证数据表结构,只会和已经存在的数据表进行比较,根据 model 修改表结构,但不会创建新表。
create 每次加载 Hibernate 时都会删除上一次生成的表(包括数据),然后重新生成新表,即使两次没有任何修改也会这样执行。适用于每次执行单测前清空数据库的场景。
create-drop 每次加载 Hibernate 时都会生成表,但当 SessionFactory 关闭时,所生成的表将自动删除。
none 不执行任何操作。将不会生成架构。适用于只读库。

不配置此项,表示禁用自动建表功能

命名策略

命名策略分两步走:
第一步:如果我们没有使用 @Table 或 @Column 指定了表或字段的名称,则由 SpringImplicitNamingStrategy 为我们隐式处理,表名隐式处理为类名,列名隐式处理为字段名。如果指定了表名列名,SpringImplicitNamingStrategy 不起作用。
第二步:将上面处理过的逻辑名称解析成物理名称。无论在实体中是否显示指定表名列名 SpringPhysicalNamingStrategy 都会被调用。

JPA大写表名自动转换为小写提示表不存在

有个全大写的表名 MYTABLE, 实体类如下,通过 @Table 注解的 name 属性指定了大写的表名

@Data
@Entity
@Table(name = "MYTABLE")
public class MYTABLEDO {
    ...
}

运行报错,提示找不到小写的表名
com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: Table ‘mydb.mytable’ doesn’t exist
貌似 jpa 的默认表命名策略是都转为小写表名。

原因:
spring data jpa 是基于 hibernate5.0 , 而 Hibernate5 关于数据库命名策略的配置与之前版本略有不同:
不再支持早期的 hibernate.ejb.naming_strategy,而是改为两个配置项分别控制命名策略:
hibernate.physical_naming_strategy
hibernate.implicit_naming_strategy
在 spring 中的配置项是

spring.jpa.hibernate.naming.implicit-strategy=org.hibernate.boot.model.naming.ImplicitNamingStrategyJpaCompliantImpl
spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl

implicit-strategy 负责模型对象层次的处理,将对象模型处理为逻辑名称
physical-strategy 负责映射成真实的数据名称的处理,将上述的逻辑名称处理为物理名称。
当没有使用 @Table 和 @Column 注解时,implicit-strategy 配置项才会被使用,当对象模型中已经指定 @Table 和 @Column 时,implicit-strategy 并不会起作用。
physical-strategy 一定会被应用,与对象模型中是否显式地指定列名或者已经被隐式决定无关。

physical-strategy 策略常用的两个实现有
org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy 这个是 Spring data jpa 的默认数据库命名策略。
org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl

解决方法一:
可以在 springboot 项目中配置文件内加上配置行,设置命名为 无修改命名策略:
spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl

解决方法二:
1 重写命名策略中改表名为小写的方法:

import org.hibernate.boot.model.naming.Identifier;
import org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl;
import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment;

/**
 * 重写 hibernate 对于命名策略中改表名大写为小写的方法
 */
public class MySQLUpperCaseStrategy extends PhysicalNamingStrategyStandardImpl {
    @Override
    public Identifier toPhysicalTableName(Identifier name, JdbcEnvironment context) {
        String tableName = name.getText().toUpperCase();
        return name.toIdentifier(tableName);
    }
}

2 在对应配置文件中 使用自己实现的策略
spring.jpa.hibernate.naming.physical-strategy=com.xxx.xxx.util.MySQLUpperCaseStrategy

解决 springboot + JPA + MySQL 表名全大写 出现 “表不存在” 问题(Table ‘XXX.xxx’ doesn’t exist)
https://blog.csdn.net/jiangyu1013/article/details/80409082

当JPA遇上MySQL表名全大写+全小写+驼峰+匈牙利四风格
https://blog.51cto.com/yerikyu/2440787


JPA MySQL 类型映射表

数据库类型 JAVA类型
VARCHAR java.lang.String
CHAR java.lang.String
BLOB java.lang.byte[]
VARCHAR java.lang.String
INTEGER UNSIGNED
TINYINT UNSIGNED
SMALLINT UNSIGNED
MEDIUMINT UNSIGNED
BIT java.lang.Boolean
BIGINT UNSIGNED
FLOAT java.lang.Float
DOUBLE java.lang.Double
DECIMAL java.math.BigDecimal
TINYINT UNSIGNED
DATE java.sql.Date
TIME java.sql.Time
DATETIME java.sql.Timestamp
TIMESTAMP java.sql.Timestamp
YEAR java.sql.Date

Hibernate

@Entity

hibernate 中 @javax.persistence.Entity@javax.persistence.Table 的区别:
@Entity 说明这个 class 是实体类,并且使用默认的 orm 规则,即 class 名即数据库表中表名,class 字段名即表中的字段名
如果想改变这种默认的 orm 规则,就要使用 @Table 来改变 class 名与数据库中表名的映射规则, @Column 来改变 class 中字段名与 db 中表的字段名的映射规则

@Entity 注解指名这是一个实体 Bean,@Table 注解指定了 Entity 所要映射带数据库表,其中 @Table.name() 用来指定映射表的表名。
如果缺省 @Table 注释,系统默认根据 类名结合命名策略 生成映射表的表名。实体 Bean 的每个实例代表数据表中的一行数据,行中的一列对应实例中的一个属性。

@Table

indexes 索引

@Data
@Entity
@Table(name = "archives", indexes = {
        @Index(name = "idx_archive_id", columnList = "archive_id")})
public class ArchivesRealInfoD {
}

uniqueConstraints 唯一约束

@Data
@Entity
@Table(name = "tag", uniqueConstraints = {
        @UniqueConstraint(name = "uk_tag_type", columnNames = {"tag_type"})
})
public class TagDO implements java.io.Serializable, Persistable<Long> {
}

多字段唯一索引

@Table(name = "tag", uniqueConstraints = {
    @UniqueConstraint(name = "uk_tag_id_name_type", columnNames = {"tag_id", "tag_name", "tag_type"})
})
public class ProjectDO {
}

@Column

@javax.persistence.Column 注解,定义了列的属性,你可以用这个注解改变数据库中表的列名(缺省情况下表对应的列名和类的字段名同名);指定列的长度;或者指定某列是否可以为空,或者是否唯一,或者能否更新或插入。

@Column 注解定义了将成员属性映射到关系表中的哪一列和该列的结构信息,属性如下:
name 映射的列名。如:映射 tbl_user 表的 name 列,可以在 name 属性的上面或 getName 方法上面加入;
unique 是否唯一;
nullable 是否允许为空;
length 对于字符型列,length 属性指定列的最大字符长度;
insertable 是否允许插入;
updatetable 是否允许更新;
columnDefinition 定义建表时创建此列的DDL;
secondaryTable 从表名。如果此列不建在主表上(默认是主表),该属性定义该列所在从表的名字。

@Id 注解指定表的主键,它可以有多种生成方式:
TABLE:容器指定用底层的数据表确保唯一;
SEQUENCE:使用数据库德SEQUENCE列莱保证唯一(Oracle数据库通过序列来生成唯一ID);
IDENTITY:使用数据库的IDENTITY列莱保证唯一;
AUTO:由容器挑选一个合适的方式来保证唯一;
NONE:容器不负责主键的生成,由程序来完成。

@GeneratedValue 注释定义了标识字段生成方式。

@Temporal 注释用来指定 java.util.Date 或 java.util.Calender 属性与数据库类型 date, time, timestamp 中的那一种类型进行映射。
@Temporal(value=TemporalType.TIME)

JPA 设置列默认值

有两种方式
1 如果只是想在当前应用 insert 数据的时候有默认值,直接在 Bean 的字段上加初始值即可,比如
private Integer gender=0;

2 如果操作的表是自己生成的,想要在表的 DDL 级别有默认值,以便无论谁往这个表插入数据都会有默认值,则需要使用 columnDefinition 属性,例如

@Column(name="gender",columnDefinition="int default 0")
private Integer gender=0;

@SecondaryTable

@SecondaryTable 用于将一个实体类映射到数据库两张或更多表中

例如

@Data
@Entity
@SecondaryTables({
   @SecondaryTable(name = "Address"),
   @SecondaryTable(name = "Comments")
})
public class Forum implements Serializable {
   private static final long serialVersionUID = 1L;
   @Id
   @GeneratedValue
   private Long id;
   private String username;
   private String password;
   @Column(table = "Address", length = 100)
   private String street;
   @Column(table = "Address", nullable = false)
   private String city;
   @Column(table = "Address")
   private String conutry;
   @Column(table = "Comments")
   private String title;
   @Column(table = "Comments")
   private String Comments;
   @Column(table = "Comments")
   private Integer comments_length;
}

上面代码定义了两个 Secondary 表,分别为 Address 表和 Comments 表,
同时在 Forum 实体类中也通过 @Column 注解将某些子段分别分配给了这两张表,那些 table 属性得值是 Adress 的就会存在于 Address 表中,
同理 table 属性的值是 Comments 的就会存在于 Comments 表中。那些没有用 @Column 注解改变属性默认的字段将会存在于 Forum 表中。


@OneToOne 一对一关联

JPA使用 @OneToOne 注解来标注一对一的关系。

有两种方式描述一对一关系:
1 通过外键的方式,一个实体通过外键关联到另一个实体的主键
2 通过中间表来保存两个实体一对一的关系

假如 People 和 Address 是一对一的关系

@JoinColumn 外键关联

1 通过外键关联

@Entity
public class People {
    @OneToOne(cascade=CascadeType.ALL)//People是关系的维护端,当删除 people,会级联删除 address
    @JoinColumn(name = "address_id", referencedColumnName = "id")//people中的address_id字段参考address表中的id字段
    private Address address;//地址
    ...
}

默认使用关联表的主键 id 作为外键,所以可以省略 referencedColumnName = "id"

@JoinTable 中间表关联

2 通过中间表关联

@Entity
public class People {
    @OneToOne(cascade=CascadeType.ALL)//People是关系的维护端
    @JoinTable(name = "people_address",
            joinColumns = @JoinColumn(name="people_id"),
            inverseJoinColumns = @JoinColumn(name = "address_id"))//通过关联表保存一对一的关系
    private Address address;//地址
    ...
}

@OneToMany 一对多关联

假设订单 order 和 产品 product 是一对多的关系

@JoinColumn 外键关联

@Entity
@Table(name = "orders")
public class Order {
    @OneToMany(cascade = {CascadeType.ALL})
    @JoinColumn(name = "order_id")
    private List<Product> productList;
    ...
}

@Entity
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    ...
}

在表 product 中会增加 order_id 外键列,不会生成中间表。

@JoinTable 中间表关联

@Entity
@Table(name = "orders")
public class Order {
    @OneToMany(cascade = {CascadeType.ALL})
    @JoinTable(name = "order_has_product",
        joinColumns = {@JoinColumn(name = "order_id", referencedColumnName = "id")},
        inverseJoinColumns = {@JoinColumn(name = "product_id", referencedColumnName = "id")})
    private List<Product> productList;
    ...
}

在 product 表中不会增加任何外键,而是新建了一张 order_has_product 表


@ManyToMany 多对多关联

JPA 中使用 @ManyToMany 来注解多对多的关系,由 @JoinTable 指定的关联表来维护。
该注解可以在 Collection, Set, List, Map 上使用,我们可以根据业务需要选择。

多对多关系中,有主表和从表的概念,主表是关系维护端,从表是被维护端

@ManyToMany 多对多关系属性
mappedBy 声明于关系的被维护方,带有此属性的是被维护端
fetch 加载策略
cascade 级联关系
CascadeType.PERSIST 级联保存操作,可以保存关联实体数据,比如 User 和 Tag,给用户加 Tag 时,如果对应的 Tag 不存在,可以自动在 tag 表增加对应数据。
CascadeType.REMOVE 级联删除操作,删除当前实体时,与它有映射关系的实体也会跟着被删除。
CascadeType.MERGE 级联更新(合并)操作,当前数据变动时会更新关联实体中的数据。
CascadeType.DETACH 级联脱管/游离操作,如果你要删除一个实体,但是它有外键无法删除,你就需要这个级联权限了。它会撤销所有相关的外键关联。
CascadeType.REFRESH 级联刷新操作
CascadeType.ALL 拥有以上所有级联操作权限

@JoinTable 关联表属性
name 关联表名,默认是 主表名_从表名
joinColumns 维护端外键,默认是 主表名+下划线+主表中的主键列名
inverseJoinColumns 被维护端外键,默认是 从表名+下划线+从表中的主键列名

关系的维护端可以对关系(在多对多为中间关联表)做 CRUD 操作。关系的被维护端没有该操作,不能维护关系。
关系维护端删除时,如果中间表存在些纪录的关联信息,则会删除该关联信息;
关系被维护端删除时,如果中间表存在些纪录的关联信息,则会删除失败

比如用户 User 和 标签 Tag 是多对多的关系,一个用户可以有多个标签,一个标签也可以对应多个用户。

1 多对多关系中一般不设置级联保存、级联删除、级联更新等操作。
2 可以随意指定一方为关系维护端,在这个例子中,指定 User 为关系维护端,所以生成的关联表名称为: user_tag,关联表的字段为:user_id 和 tag_id。
3 多对多关系的绑定由关系维护端来完成,即由 User.setTags(tags) 来绑定多对多的关系。关系被维护端不能绑定关系,即 Tag 不能绑定关系。
4 多对多关系的解除由关系维护端来完成,即由 User.getTags().remove(tag) 来解除多对多的关系。关系被维护端不能解除关系,即 Tag 不能解除关系。
5 如果 User 和 Tag 已经绑定了多对多的关系,那么不能直接删除 Tag,需要由 User 解除关系后,才能删除 Tag。但是可以直接删除 User,因为 User 是关系维护端,删除 User 时,会先解除 User 和 Tag 的关系。

@Entity
public class User {
    @ManyToMany
    @JoinTable(name = "user_tag",
        joinColumns = @JoinColumn(name = "user_id"),
        inverseJoinColumns = @JoinColumn(name = "tag_id"))
    private List<Tag> tagList;
}

@Entity
public class Tag {
    @ManyToMany(mappedBy = "tagList")
    private List<User> userList;
}

1 关系维护端,负责多对多关系的绑定和解除
2 @JoinTable 注解的 name 属性指定关联表的名字,joinColumns 指定外键的名字,关联到关系维护端(User)
3 inverseJoinColumns 指定外键的名字,要关联的关系被维护端(Tag)
4、其实可以不使用 @JoinTable 注解,默认生成的关联表名称为 主表表名+下划线+从表表名,即表名为 user_tag
关联到主表的外键名:主表名+下划线+主表中的主键列名,即 user_id
关联到从表的外键名:主表中用于关联的属性名+下划线+从表的主键列名,即 tag_id
主表就是关系维护端对应的表,从表就是关系被维护端对应的表
5 从表 @ManyToMany 的 mappedBy 属性表明主表是关系维护端,当前表示被维护端。

单向关联与双向关联

单向关联和双向关联
单向关联 单向关联指的是实体类 A 中有一个实体类 B 变量,但是实体类 B 中没有实体类 A 变量,即为单向关联。
双向关联 双向关联指的是实体类 A 中有一个实体类 B 变量,而实体类 B 中也含有一个实体类A变量,即为双向关联。

双向关联时还需要考虑对象序列化为 JSON 字符串时的死循环问题。

如果在主表和从表都进行 @ManyToMany 关联声明,就是双向关联,也就是主表和从表都互相知道对方的存在。

此外,多对多关系也可以通过单向关联来声明。
当使用单向关联时,由主表管理关联关系,从表无法管理。此时,主表知道自己的从表,但是从表不知道主表是谁。
单向关联时,只指定 @OneToMany 即可

Spring Data JPA中的一对一,一对多,多对多查询
https://super-aviator.github.io/2019/06/22/Spring-Data-JPA%E4%B8%AD%E7%9A%84%E4%B8%80%E5%AF%B9%E4%B8%80%EF%BC%8C%E4%B8%80%E5%AF%B9%E5%A4%9A%EF%BC%8C%E5%A4%9A%E5%AF%B9%E5%A4%9A%E6%9F%A5%E8%AF%A2/


Hibernate 映射规则

  1. 实体类必须用 @javax.persistence.Entity 进行注解;

  2. 必须使用 @javax.persistence.Id 来注解一个主键;

  3. 实体类必须拥有一个 public 或者 protected 的无参构造函数,之外实体类还可以拥有其他的构造函数;

  4. 实体类必须是一个顶级类(top-level class)。一个枚举(enum)或者一个接口(interface)不能被注解为一个实体;

  5. 实体类不能是 final 类型的,也不能有 final 类型的方法;

  6. 如果实体类的一个实例需要用传值的方式调用(例如,远程调用),则这个实体类必须实现(implements) java.io.Serializable 接口。


QueryDSL

JPA 中复杂查询的几种方案

1 使用注解 @Query ,在其中拼接 SQL 或 HQL
2 使用 JPA 提供的 Specification,通过 PredicateCriteriaBuilder 拼接查询条件
3 使用 QueryDSL

maven依赖

无需指定版本号,spring-boot-dependencies 中已规定好。

<!--QueryDSL支持-->
<dependency>
    <groupId>com.querydsl</groupId>
    <artifactId>querydsl-apt</artifactId>
    <scope>provided</scope>
</dependency>
<dependency>
    <groupId>com.querydsl</groupId>
    <artifactId>querydsl-jpa</artifactId>
</dependency>

apt-maven-plugin插件

添加这个插件是为了让程序自动生成 query type (查询实体,命名方式为 Q+对应实体名 即 Q类 )。
依赖中 querydsl-apt 即是为此插件服务的
在使用过程中,如果遇到 query type 无法自动生成的情况,用 maven 更新一下项目即可解决(右键项目 -> Maven -> Update Folders)。
生成的 Q类 位于 target 包中,所以新加载的项目会出现找不到 Q类 的错误,需要手动 mvn compile 一下。

<build>
    <plugins>
        <plugin>
            <groupId>com.mysema.maven</groupId>
            <artifactId>apt-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

BooleanBuilder 动态查询

Spring Boot (六): 为 JPA 插上翅膀的 QueryDSL
https://juejin.im/post/5d8fff4051882509563a0430


上一篇 LeetCode.097.Interleaving String 交错字符串

下一篇 LeetCode.063.Unique Paths II 不同路径 II

阅读
评论
4,597
阅读预计19分钟
创建日期 2020-07-16
修改日期 2020-07-28
类别

页面信息

location:
protocol:
host:
hostname:
origin:
pathname:
href:
document:
referrer:
navigator:
platform:
userAgent:

评论