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

Spring-Data-JPA

JPA(Java Persistence API) 使用笔记


SpringBoot2 JPA 多数据源配置

application.properties

两个数据源 db1 和 db2 的连接参数配置

# db1
spring.datasource.db1.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.db1.jdbc-url=jdbc:mysql://localhost:3306/db1?characterEncoding=UTF-8&autoReconnect=true&useSSL=false
spring.datasource.db1.username=root
spring.datasource.db1.password=123456

# db1
spring.datasource.db1.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.db1.jdbc-url=jdbc:mysql://localhost:3306/db1?characterEncoding=UTF-8&autoReconnect=true&useSSL=false
spring.datasource.db1.username=root
spring.datasource.db1.password=123456

spring.datasource.hikari.maximum-pool-size=100
spring.datasource.hikari.connection-timeout=120000

spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
spring.jpa.showSql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.hibernate.ddl-auto=update
spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl

HibernateVendorConfig

两个数据源的 Hibernate 属性配置
注意我这里 db2 是只读库,防止误修改 db2 的表结构,获取公共的 hibernate 配置后,将 hibernate.hbm2ddl.auto 设为 none 来关闭自动更新表结构
或者也可以分别配置两个数据源的 hibernate 配置,不共用。

@Configuration
public class HibernateVendorConfig {

    @Autowired
    private JpaProperties jpaProperties;

    @Autowired
    private HibernateProperties hibernateProperties;

    /**
     * db1 Hibernate 配置
     */
    @Bean(name = "db1HibernateVendorProperties")
    public Map<String, Object> db1HibernateVendorProperties() {
        return hibernateProperties.determineHibernateProperties(jpaProperties.getProperties(), new HibernateSettings());
    }

    /**
     * db2 Hibernate 配置
     *
     * @return
     */
    @Bean(name = "db2HibernateVendorProperties")
    public Map<String, Object> db2HibernateVendorProperties() {
        Map<String, Object> db2HibernateVendorProperties = hibernateProperties.determineHibernateProperties(
                jpaProperties.getProperties(), new HibernateSettings());
        // db2 是只读的,防止误修改 db2 的表结构,获取公共的 hibernate 配置后,覆盖 hibernate.hbm2ddl.auto 关闭自动更新表结构
        db2HibernateVendorProperties.put("hibernate.hbm2ddl.auto", "none");
        return db2HibernateVendorProperties;
    }
}

db1的JPA配置

配置 db1 的 JPA 属性
DataSource 数据源
LocalContainerEntityManagerFactoryBean 实体管理器工厂,。
PlatformTransactionManager 事务管理器
指定 db1 的 Repository 接口所在包
指定 db1 的 实体 entity 所在包
指定 db1 的 持久化单元的名字,需要唯一,用于区别两个数据源。

同时,也要将 db1 的实体类和 db2 的实体类分别放在指定的不同包中用于区分。
将 db1 的 Repository 接口和 db2 的 Repository 接口也分别放在不同的包中。

@Configuration
@EnableJpaRepositories(entityManagerFactoryRef = "db1EntityManagerFactory",
        transactionManagerRef = "db1TransactionManager",
        basePackages = {"com.masikkk.persistence.repository.db1"}) // 设置 Repository 接口所在包
public class JpaDB1Config {

    @Resource(name = "db1HibernateVendorProperties")
    private Map<String, Object> hibernateVendorProperties;

    /**
     * 创建 db1 数据源
     */
    @Bean(name = "db1DataSource")
    @ConfigurationProperties(prefix = "spring.datasource.db1")
    @Primary // 需要特殊添加,否则初始化会有问题
    public DataSource dataSource() {
        return DataSourceBuilder.create().build();
    }

    /**
     * 创建 LocalContainerEntityManagerFactoryBean
     */
    @Bean(name = "db1EntityManagerFactory")
    @Primary
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(EntityManagerFactoryBuilder builder) {
        return builder.dataSource(this.dataSource()) // 数据源
                .properties(hibernateVendorProperties) // 获取并注入 Hibernate Vendor // 相关配置
                .packages("com.masikkk.persistence.model.db1") // 数据库实体 entity 所在包
                .persistenceUnit("db1PersistenceUnit") // 设置持久单元的名字,需要唯一
                .build();
    }

    /**
     * 创建 PlatformTransactionManager
     */
    @Bean(name = "db1TransactionManager")
    @Primary
    public PlatformTransactionManager transactionManager(EntityManagerFactoryBuilder builder) {
        return new JpaTransactionManager(entityManagerFactory(builder).getObject());
    }
}

db2的JPA配置

配置和 db1 大体相同

@Configuration
@EnableJpaRepositories(entityManagerFactoryRef = "db2EntityManagerFactory",
        transactionManagerRef = "db2TransactionManager",
        basePackages = {"com.masikkk.persistence.repository.db2"}) // 设置 Repository 接口所在包
public class JpaDB2Config {

    @Resource(name = "db2HibernateVendorProperties")
    private Map<String, Object> hibernateVendorProperties;

    /**
     * 创建 db2 数据源
     */
    @Bean(name = "db2DataSource")
    @ConfigurationProperties(prefix = "spring.datasource.db2")
    public DataSource dataSource() {
        return DataSourceBuilder.create().build();
    }

    /**
     * 创建 LocalContainerEntityManagerFactoryBean
     */
    @Bean(name = "db2EntityManagerFactory")
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(EntityManagerFactoryBuilder builder) {
        return builder.dataSource(this.dataSource()) // 数据源
                .properties(hibernateVendorProperties) // 获取并注入 Hibernate Vendor 相关配置
                .packages("com.masikkk.persistence.model.db2") // 数据库实体 entity 所在包
                .persistenceUnit("db2PersistenceUnit") // 设置持久单元的名字,需要唯一
                .build();
    }

    /**
     * 创建 PlatformTransactionManager
     */
    @Bean(name = "db2TransactionManager")
    public PlatformTransactionManager transactionManager(EntityManagerFactoryBuilder builder) {
        return new JpaTransactionManager(entityManagerFactory(builder).getObject());
    }
}

多数据源下获取 EntityManager 实例

通过 @PersistenceContext 注解从 容器实体管理器工厂 内获取 EntityManager 实例,由于有两个数据源,通过 unitName 属性指定持久化单元,这里的 unitName 要和构造 LocalContainerEntityManagerFactoryBean 时指定的 persistenceUnit 属性相同。
有了 EntityManager 就可以构造 查询工厂 JPAQueryFactory 了。

@PersistenceContext(unitName = "db1PersistenceUnit")
private EntityManager entityManager;

@PostConstruct
public void initFactory() {
    jpaQueryFactory = new JPAQueryFactory(entityManager);
}

jdbcUrl is required with driverClassName

SpringBoot2.x 默认使用的数据源是 HikariCP,它使用的连接参数是 jdbc-url 而不是 url
spring.datasource.db1.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


SpringBoot JPA打印SQL及参数到文件

1 在 properties 中配置打印sql和格式化sql后,也只能在本地启动的控制台日志中看到,日志文件中是没有的。

# 打印 sql,但是只能打印到 控制台,无法打印到日志文件
spring.jpa.showSql=true
# 格式化SQL
spring.jpa.properties.hibernate.format_sql=true

2 想同时打印出sql参数,增加如下配置,但在我这里始终不起作用
无论是 properties 中配置

logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE
logging.level.org.hibernate.type=TRACE

3 如果想在日志文件中也看到,在 logback.xml 中增加如下配置,在我这里也始终不起作用

<!-- 1. 输出 SQL 到控制台和文件-->
<logger name="org.hibernate.SQL" additivity="false"  level="debug">
   <appender-ref ref="file" />
   <appender-ref ref="console" />
</logger>

  <!-- 2. 输出 SQL 的参数到控制台和文件-->
<logger name="org.hibernate.type.descriptor.sql.BasicBinder" additivity="false" level="TRACE" >
   <appender-ref ref="file" />
   <appender-ref ref="console" />
</logger>

How to log SQL statements in Spring Boot?
https://stackoverflow.com/questions/30118683/how-to-log-sql-statements-in-spring-boot

logback 配置打印 JPA SQL日志到文件
https://blog.csdn.net/sinat_25295611/article/details/81073011


Spring Data

Pageable 和 Sort

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

注意这里的 String... properties 是 DO 类中的字段名,不是数据库的字段名

分页查询示例

// 根据更新时间范围分页查询档案
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 不执行任何操作。将不会生成架构。适用于只读库。

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

Hibernate命名策略

Hibernate隐式命名策略和物理命名策略

hibernate 5.1 之前,命名策略通过 NamingStrategy 接口实现,但是 5.1 之后该接口已废弃。
hibernate 5.1 之后,命名策略通过 隐式命名策略 接口 ImplicitNameSource物理命名策略 接口 PhysicalNamingStrategy 共同作用实现。

在 Spring 中使用时通过下面两个步骤来确定:
第一步:如果我们没有使用 @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实现根据SpringBoot命令行参数动态设置表名

背景:
使用 JPA 读取的一个表名经常变化(表结构不变),过一段时间就升级一次,使用 @Table(name = "xxx") 注解实体 bean 来做映射。
表名变化后可以改配置参数重启,但尽量不要改代码,因为改代码还得重新发包。
有如下几种方案:
1 JPA 中通过 @Query 手动拼 sql,这样不仅不需要重启,利用配置项字典表还能运行中动态改表名。但不想这样做,都使用 JPA 了还手动拼sql,太麻烦。
2 Hibernate 拦截器改表名,只是偶然搜到了能这么做,没实现过。
Hibernate 拦截器的使用–动态表名
https://my.oschina.net/cloudcross/blog/831277

3 自定义物理命名策略 SpringPhysicalNamingStrategy,在其中读取配置项,修改指定的表名,参考下面这篇文章。
我完全照着这篇文章实现的话,总是无法成功,每次运行到 toPhysicalTableName() 方法时,之前注入的 parser 就变为 null 了,后来仔细看了看,发现被 ApplicationContextAware 回调的,和运行 toPhysicalTableName() 方法的,是两个不同的 MySpringPhysicalNamingStrategy 实例,导致无法读取配置。
Spring Data JPA自定义实现动态表名映射
https://blog.csdn.net/u014229347/article/details/88892559

最后改了改实现,简化为在命令行参数中 -Dtable.name=xxx 配置表名,在自定义命名策略中直接 System.getProperty("table.name") 读取命令行参数。

完整步骤:
1 自定义物理命名策略

@Slf4j
@Component
public class ConfigurableNamingStrategy extends SpringPhysicalNamingStrategy {
    @Override
    public Identifier toPhysicalTableName(Identifier name, JdbcEnvironment jdbcEnvironment) {
        if (StringUtils.equals(name.getText(), "table_name_placeholder")) {
            String tableName = System.getProperty("table.name");
            log.info("新表名: {}", tableName);
            return Identifier.toIdentifier(tableName);
        } else {
            // 其他表不变
            return super.toPhysicalTableName(name, jdbcEnvironment);
        }
    }
}

2 实体上直接注解为假的表名占位符

@Data
@Entity
@Table(name = "table_name_placeholder")
public class MyDO {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY, generator = "JDBC")
    private Long id;
}

3 Spring 配置文件中设置自定义命名策略

spring.jpa.hibernate.naming.physical-strategy=com.masikkk.persistence.config.ConfigurableNamingStrategy

4 SpringBoot 启动参数中添加表名配置项

nohup java -jar -Dtable.name=xxx myapp.jar &

注意,这种 -D System 参数必须放在 myapp.jar 之前才能被 System.getProperty() 读取到

Hibernate 自定义表名映射
https://segmentfault.com/a/1190000015305191


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

name 指定表名

indexes 索引

@Data
@Entity
@Table(name = "student", indexes = {
        @Index(name = "idx_stu_no", columnList = "stu_no")})
public class StudentDO {
}

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 级联关系

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

CascadeType.REMOVEorphanRemoval=true区别

1、orphanRemoval=true 是一种更加激进的级联删除属性,所有断开连接关系的 Address 数据都会被删除,比如将一个 Employee 实例的 address 设为 null 或者修改为其他地址时,断开连接关系的 Address 会自动被删除。

2、如果只设置了 cascade=CascadeType.REMOVE,将一个 Employee 实例的 address 设为 null 或者修改为其他地址时,断开连接关系的 Address 不会被自动删除,因为这不是由 Employee 的删除操作触发的。

@Entity
class Employee {
    @OneToOne(cascade=CascadeType.REMOVE)
    private Address address;
}

@Entity
class Employee {
    @OneToOne(orphanRemoval=true)
    private Address address;
}

What is the difference between CascadeType.REMOVE and orphanRemoval in JPA?
https://stackoverflow.com/questions/18813341/what-is-the-difference-between-cascadetype-remove-and-orphanremoval-in-jpa

@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

QueryDSL 关联查询示例

user 用户表 (id, real_name, create_time)
user_tag 用户和标签映射表 (id, user_id, tag_id)
实现根据 标签 ID 关联搜索用户

@Service
public class UserService {
    // user 表的 Q 类
    private final QUserDO qUserDO = QUserDO.userDO;
    // user_tag 表的 Q 类
    private final QUserTagDO qUserTagDO = QUserTagDO.userTagDO;

    @PersistenceContext(unitName = "db1Unit")
    private EntityManager entityManager;

    private JPAQueryFactory jpaQueryFactory;

    @PostConstruct
    public void initFactory() {
        jpaQueryFactory = new JPAQueryFactory(entityManager);
    }

    // user 搜索,返回是个 Pair,left 是总个数,right 是当前页 DO 列表
    public Pair<Long, List<UserDO>> search(List<Long> userIds, List<Long> tagIds, String realName,
                                               Timestamp createTimeStart, Timestamp createTimeEnd,
                                               String sortColumn, String sortDirection,
                                               long offset, int limit) {
        JPAQuery<UserDO> jpaQuery = jpaQueryFactory.selectDistinct(qUserDO).from(qUserDO);
        // 根据是否有 tagIds 参数决定是否关联 user_tag 表
        if (CollectionUtils.isNotEmpty(tagIds)) {
            jpaQuery.join(qUserTagDO)
                    .on(qUserTagDO.id.eq(qUserDO.id))
                    .where(qUserTagDO.tagId.in(tagIds));
        }
        if (CollectionUtils.isNotEmpty(userIds)) {
            jpaQuery.where(qUserDO.id.in(userIds));
        }
        if (StringUtils.isNotBlank(realName)) {
            jpaQuery.where(qUserDO.realName.eq(realName));
        }
        if (Objects.nonNull(createTimeStart)) {
            jpaQuery.where(qUserDO.createTime.goe(createTimeStart));
        }
        if (Objects.nonNull(createTimeEnd)) {
            jpaQuery.where(qUserDO.createTime.loe(createTimeEnd));
        }
        // 按创建时间或 id 排序
        if (StringUtils.equalsIgnoreCase(sortColumn, "create_time")) {
            jpaQuery.orderBy(new OrderSpecifier<>(Order.valueOf(sortDirection.toUpperCase()), qUserDO.createTime));
        } else {
            jpaQuery.orderBy(new OrderSpecifier<>(Order.valueOf(sortDirection.toUpperCase()), qUserDO.id));
        }
        // 分页查询
        jpaQuery.offset(offset).limit(limit);
        List<UserDO> userDOList = jpaQuery.fetch();
        // count 查个数
        long count = jpaQuery.fetchCount();
        return Pair.of(count, userDOList);
    }
}

第四章:使用QueryDSL与SpringDataJPA实现多表关联查询
https://www.jianshu.com/p/6199e76a5485


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

阅读
评论
6,880
阅读预计29分钟
创建日期 2020-07-16
修改日期 2020-09-18
类别

页面信息

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

评论