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

Spring-Data-JPA

JPA(Java Persistence API) 使用笔记

JPA(Java Persistence API) JAVA持久化API 定义了对象-关系映射(ORM)以及实体对象持久化的标准接口。
JPA 由 EJB 3.0 软件专家组开发,作为 JSR-220 实现的一部分。但它又不限于 EJB 3.0,可以作为 POJO 持久化的标准规范,可以脱离容器独立运行,开发,测试。
JPA 是一种规范,而 Hibernate 和 iBATIS 等是开源持久框架,是 JPA 的一种实现。

JPA 的总体思想和现有 Hibernate、TopLink,JDO 等ORM框架大体一致。总的来说,JPA包括以下3方面的技术:
1、ORM 映射元数据,JPA支持XML和JDK 5.0注解两种元数据的形式,元数据描述对象和表之间的映射关系,框架据此将实体对象持久化到数据库表中;
2、JPA的API,用来操作实体对象,执行CRUD操作,框架在后台替我们完成所有的事情,开发者从繁琐的JDBC和SQL代码中解脱出来。
3、查询语言,这是持久化操作中很重要的一个方面,通过面向对象而非面向数据库的查询语言查询数据,避免程序的SQL语句紧密耦合。

JPA和Hibernate的关系
JPA是需要Provider来实现其功能的,Hibernate就是JPA Provider中很强的一个。从功能上来说,JPA现在就是Hibernate功能的一个子集。可以简单的理解为JPA是标准接口,Hibernate是实现。

使用 Spring Data JPA 简化 JPA 开发
https://www.ibm.com/developerworks/cn/opensource/os-cn-spring-jpa/


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

# db2
db2.enable=true
spring.datasource.db2.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.db2.jdbc-url=jdbc:mysql://localhost:3306/db1?characterEncoding=UTF-8&autoReconnect=true&useSSL=false
spring.datasource.db2.username=root
spring.datasource.db2.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配置(db2.enable=true时启用)

配置和 db1 大体相同

@ConditionalOnProperty(prefix = "db2", name = "enable", havingValue = "true")
@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 JPA

Pageable 和 Sort 分页查询

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

// 根据配置生成分页参数
int pageIndex = 0;
Sort sort = Sort.by(Sort.Direction.fromString(properties.getSortDirection()), properties.getSortColumn());
Pageable pageable = PageRequest.of(pageIndex, properties.getPageSize(), sort);

注意:Sort.by()的第二个参数 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
}

JPA的分页查询包含数据和count两个查询

带分页查询的,都会自动生成2个sql,一个插数据,一个按同条件count个数,如果我们不需要count个数,就白浪费时间了。

OrderSpecifier findAll排序示例

BooleanBuilder booleanBuilder = new BooleanBuilder();
booleanBuilder.and(qStatisticDO.statDate.goe(statDateStart));
booleanBuilder.and(qStatisticDO.statDate.loe(statDateEnd));
booleanBuilder.and(qStatisticDO.fieldKey.equalsIgnoreCase(fieldKey));
return findAll(booleanBuilder, new OrderSpecifier<>(Order.DESC, qStatisticDO.statDate));

@PageableDefault

Pageable 是 Spring Data 库中定义的一个接口,该接口是所有分页相关信息的一个抽象,通过该接口,我们可以得到和分页相关所有信息(例如pageNumber、pageSize等)。

Pageable 定义了很多方法,但其核心的信息只有两个:一是分页的信息(page、size),二是排序的信息。

在 springmvc 的请求中只需要在方法的参数中直接定义一个 pageable 类型的参数,当 Spring 发现这个参数时,Spring 会自动的根据 request 的参数来组装该 pageable 对象,Spring 支持的 request 参数如下:
page 第几页,从0开始,默认为第0页
size 每一页的大小,默认为20
sort 排序相关的信息,以property,property(,ASC|DESC)的方式组织,例如sort=firstname&sort=lastname,desc表示在按firstname正序排列基础上按lastname倒序排列。

Spring data 提供了 @PageableDefault 帮助我们个性化的设置 pageable 的默认配置。例如 @PageableDefault(value = 15, sort = { “id” }, direction = Sort.Direction.DESC) 表示默认情况下我们按照 id 倒序排列,每一页的大小为15。


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 对应的字段存在,则更新

save和saveAndFlush

在 saveAndFlush 上,此命令中的更改将立即刷新到DB。
使用save,就不一定了,它可能只暂时保留在内存中,直到发出flush或commit命令。
但是要注意的是,即使在事务中刷新了更改并且未提交它们,这些更改对于外部事务仍然不可见,直到,提交这个事务。


@NoRepositoryBean

@NoRepositoryBean 注解用于避免给 XxRepository 接口生成实例,一般用于 Repository 基类上。

比如我们创建一个 BaseRepository 基类,包含其他具体 Repository 的公共方法,这个基类 Repository 上一般要注解 @NoRepositoryBean 用来告诉 Spring 不要创建此接口的代理实例。

@NoRepositoryBean
public interface BaseRepository<T, ID extends Serializable> extends JpaRepository<T, ID> {
    List<T> findByAttributeContainsText(String attributeName, String text);
}

public interface UserRepository extends BaseRepository<UserDO, Long> {
}

@EntityScan 和 @EnableJpaRepositories

在 SpringBoot 中使用 JPA 时,如果在主应用程序所在包或者其子包的某个位置定义我们的 Entity 和 Repository, 这样基于 Springboot 的自动配置,无需额外配置,我们定义的 Entity 和 Repository 即可被发现和使用。

但有时候我们需要定义 Entity 和 Repository 不在应用程序所在包及其子包,那么这时候就需要使用 @EntityScan 和 @EnableJpaRepositories 了。

@EntityScan 用来扫描和发现指定包及其子包中的 Entity 定义。
如果多处使用 @EntityScan, 它们的 basePackages 集合能覆盖所有被 Repository 使用的 Entity 即可,集合有交集也没有关系。

@EnableJpaRepositories 用来扫描和发现指定包及其子包中的 Repository 定义。
如果多处使用 @EnableJpaRepositories, 它们的 basePackages 集合不能有交集,并且要能覆盖所有需要的Repository定义。如果有交集,相应的 Repository 会被尝试反复注册,导致重复bean注册错误。

@Configuration
@EnableQuerydsl
@EnableJpaAuditing
@ComponentScan(basePackageClasses = {PersistenceServicePackage.class})
@EntityScan(basePackageClasses = {PersistenceDOPackage.class})
@EnableJpaRepositories(basePackageClasses = {PersistenceRepositoryPackage.class})
public class JpaConfiguration {
}

@Enumerated 枚举持久化

当我需要持久化一个枚举类字段的时候,就可以用@Enumerated来标注枚举类型。

@Enumerated(EnumType.ORDINAL) 持久化为 0,1 开始的枚举序号
@Enumerated(EnumType.STRING) 持久化为枚举的 name

当不使用任何注解的时候,默认情况下是使用ordinal属性,也就是Enum类型实例在Enum中声明的顺序来完成映射的

@ColumnTransformer 读写转换

枚举类型 StatusEnum 设置了直接用枚举 name 持久化,但如果有手动插入的小写数据,比如 enabled, 查询时就无法自动转换为 StatusEnum 枚举类型,会报错说无法识别的枚举值,加上 @ColumnTransformer(read = "UPPER(status)") 自动转大写就好了。

@Builder.Default
@Enumerated(EnumType.STRING)
@ColumnDefault("'ENABLED'")
@ColumnTransformer(read = "UPPER(status)")
private StatusEnum status = StatusEnum.ENABLED;

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

Spring Repository 和 Jpa EntityManager

Spring Repository 是在 Jpa EntityManager 之上的抽象,对开发者屏蔽了 Jpa 的底层细节,提供一系列易用的接口方法。

EntityManager 执行native SQL

createNativeQuery()执行原生select示例

class EntityManagerTest {
    @PersistenceContext
    EntityManager entityManager;

    public long select() {
        String querySQL = "SELECT COUNT(DISTINCT user_id) FROM user ";
        Object o = entityManager.createNativeQuery(querySQL).getSingleResult();
        Long count = 0L;
        if (o != null) {
            count = ((BigInteger) o).longValue();
        }
        return count;
    }
}

createNativeQuery()执行原生update示例

class EntityManagerTest {
    @PersistenceContext(unitName = "realinfosPersistenceUnit")
    private EntityManager entityManager;

    // 更新task的status
    @Transactional
    public boolean updateTask(String code, String status) {
        String sql = String.format("update task_table set status='%s' where code='%s'", status, code);
        int res = entityManager.createNativeQuery(sql).executeUpdate();
        log.info("[DB] update task_table {} to {}", code, status);
        return res == 1;
    }
}

我这里是双数据源,需要根据 unitName 指定 EntityManager,只有一个数据源时不需要

createNativeQuery()原生sql查询部分字段示例

public List<UserDTO> getNameAgeByIds(Collection<Long> ids) {
    Query query = entityManager.createNativeQuery(
            "SELECT name, age FROM user WHERE " +
                    "name IS NOT NULL AND name != '' " +
                    "AND id IN (" + Joiner.on(",").join(ids) + ")");
    List<Object[]> res = (List<Object[]>) query.getResultList();
    return res.stream().map(objectArray -> {
        UserDTO userDTO = new UserDTO();
        userDTO.setName(objectArray[0].toString());
        userDTO.setAge(Long.valueOf(objectArray[1].toString()));
        return userDTO;
    }).collect(Collectors.toList());
}

JPA中 update/delete必须开启事务

javax.persistence.TransactionRequiredException: Executing an update/delete query

原因:
jpa要求,’没有事务支持,不能执行更新和删除操作’。

解决:
在 Service 层或者 Repository 层上必须加 @Transactional 来开启事务


带冒号的时间参数报错QueryException: Named parameter not bound

entityManager.executeNativeQuery() 执行报错
org.hibernate.QueryException: Named parameter not bound : 00:00

sql如下

LocalDateTime todayStart = LocalDateTime.now().with(LocalTime.MIN);
String todayStartStr = todayStart.format(DateTimeFormatter.ofPattern(DateUtil.YYYY_MM_DD_MM_HH_SS));
String sql = String.format("select count(*) from user where update_time >= %s ", todayStartStr);
String result = entityManager.createNativeQuery(sql).getSingleResult().toString();

原因:
hibernate 执行 SQL 时遇到带冒号的都会认为是参数占位符,比如字符串 2020-07-09 12:02:12 被识别为占位符,因此报错(named parameter not bound::00:00)

解决方法:
时间参数外加上单引号 ‘’


NonUniqueDiscoveredSqlAliasException 字段重复

Jpa createNativeQuery getResultList 报错:

Caused by: org.hibernate.loader.custom.NonUniqueDiscoveredSqlAliasException: Encountered a duplicated sql alias [column_name] during auto-discovery of a native-sql query
    at org.hibernate.loader.custom.CustomLoader.validateAliases(CustomLoader.java:513)
    at org.hibernate.loader.custom.CustomLoader.autoDiscoverTypes(CustomLoader.java:490)
    at org.hibernate.loader.Loader.getResultSet(Loader.java:2124)
    at org.hibernate.loader.Loader.executeQueryStatement(Loader.java:1899)

原因:
Native sql 是代码内拼接的,由于代码 bug, 拼接的 SQL 中有重复的结果字段,类似
select column_name, column_name from table;
所以报错了

解决:
去掉拼接 sql 中的重复字段。


JPA审计

在 Spring JPA 中,支持在字段或者方法上进行注解 @CreateDate @CreatedBy @LastModifiedDate @LastModifiedBy

@CreateDate 表示该字段为创建时间时间字段,在这个实体被 insert 的时候,会设置默认值

@CreatedBy 表示该字段为创建人,在这个实体被insert的时候,会设置值。

指定 @EnableJpaAuditing 来启用JPA审计:

@Configuration
@EnableJpaAuditing
public class JpaConfiguration {
}

审计意味着跟踪和记录我们在持久记录中所做的每一项更改,这意味着跟踪每个插入,更新和删除操作并存储它。审计有助于我们维护历史记录,以后可以帮助我们跟踪用户操作系统的活动。

使用Spring Boot 2和Spring Data JPA实现审计
https://www.jdon.com/springboot/spring-data-jpa-auditing.html


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 是否唯一;
length 对于字符型列,length 属性指定列的最大字符长度;
secondaryTable 从表名。如果此列不建在主表上(默认是主表),该属性定义该列所在从表的名字。

nullable

nullable 是否允许为空;

columnDefinition

columnDefinition 定义建表时创建此列的DDL;

insertable,updatetable

insertable 是否允许插入;
updatetable 是否允许更新;

只读属性可以通过使用注解 @Column 的 updatable 和 insertable 来实现
如果两个都设置了 false, 属性列表就用于不会在 INSERT 或者 UPADATE 语句中出现了,这些列的数值就由数据库来产生值。

@Id

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

@GeneratedValue

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

@Temporal

@Temporal 注释用来指定 java.util.Date 或 java.util.Calender 属性与数据库类型 date, time, timestamp 中的哪一种类型进行映射。
@Temporal(value=TemporalType.TIME)
可选值
TemporalType.DATE 对应 MySQL 的 date 类型
TemporalType.TIME 对应 MySQL 的 time 类型
TemporalType.TIMESTAMP 对应 mysql 的 datetime 类型

JPA设置自动维护创建时间和更新时间

@Column(name = "create_time", nullable = false, insertable = false, updatable = false,
        columnDefinition = "DATETIME DEFAULT CURRENT_TIMESTAMP")
private Timestamp createTime;

@Column(name = "update_time", nullable = false, insertable = false, updatable = false,
        columnDefinition = "DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP")
private Timestamp updateTime;

对应 sql

create table user(
    id             bigint auto_increment primary key,
    create_time    datetime   default CURRENT_TIMESTAMP not null,
    update_time    datetime   default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP
);

JPA 设置列默认值

实体属性默认值

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

DDL级默认值(columnDefinition)

JPA/Hibernate 中,如果想要在表的 DDL 级别有默认值,以便无论谁往这个表插入数据都会有默认值,则需要使用 columnDefinition 属性,例如

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

DDL级默认值(@ColumnDefault)

Hibernate 提供了一个 @ColumnDefault 注解来设置列默认值。
需要注意的是,设置 String 默认值时必须加单引号,否则就变成了 status varchar(255) default enabled,在 h2 数据库做单测时会提示 Column “ENABLED” not found; 错误。

@ColumnDefault("'enabled'")
@Column(nullable = false)
private String status;

@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 关联表

@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/


fetch 加载策略

jpa 中定义关联表或关联字段时,可以指定关联数据的加载方式。

FetchType.LAZY 懒加载,加载一个实体时,定义懒加载的属性不会马上从数据库中加载。在同一个session中,什么时候要用,就什么时候取(再次访问数据库)。但是,在session外,就不能再取了。
FetchType.EAGER 急加载,加载一个实体时,定义急加载的属性会立即从数据库中加载。

hibernate 中默认是懒加载

单测或非web请求时报错 LazyInitializationException

单测时报错
org.hibernate.LazyInitializationException: could not initialize proxy - no Session in Hibernate

Unable to evaluate the expression Method threw ‘org.hibernate.LazyInitializationException’ exception.
但是,启动服务后通过接口访问没问题。

原因: @ManyToMany 关联数据默认是懒加载
@OneToMany(fetch = FetchType.LAZY)
hibernate 默认懒加载,关联字段只有在使用时(被get)才查询,再次查询需要在session中再次执行sql访问数据库,如果此时 session 关闭了,就会抛异常

但是为什么只是单测中会有这个问题,服务启动后是正常的,原因一直没找到,网上博客中都没有说为什么。

解决方法:
一、在实体类上增加注解 @Proxy(lazy = false)

@Entity
@Proxy(lazy = false)  //解决懒加载问题
public class XxxDO {
    ...
}

二、在测试类报错的方法上增加事务注解 @Transactional
这种方式是最好的,不需要改动实体的加载方式。

@Slf4j
@Transactional
@SpringBootTest
public class CollectionServiceTest {
    @Autowired
    private CollectionService collectionService;

    @Test
    public void testQuery() {
        CollectionDO collectionDo = collectionService.queryById("1234");
    }
}

三、在 springboot 配置中增加 spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true 配置项
不建议这样。
这个配置是 hibernate 中的(其它 JPA Provider 中无法使用)的一个 workaround ,当配置的值是 true 的时候,允许在没有 transaction 的情况下支持懒加载,实现原理是每次 fetch 懒加载的关联字段时,都临时开一个 session,在一个单独的事务中执行sql, 需要注意的是,这种方式有性能问题,如果一个user关联了n个tag,一共会进行1+n次查询,性能非常差

spring:
  jpa:
    properties:
      hibernate:
        enable_lazy_load_no_trans: true

Quick Guide to Hibernate enable_lazy_load_no_trans Property
https://www.baeldung.com/hibernate-lazy-loading-workaround


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 接口。


JPA 中复杂查询的几种方案

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


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插件(生成Q类)

添加这个插件是为了让程序自动生成 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 动态查询

like 模糊匹配

注意:
1、需要自己拼接包含 % 的模糊匹配串
2、**String.format() 需要两个百分号 %% 来转义百分号**

例如:

public Page<UserDO> query(Request req) {
    BooleanBuilder builder = new BooleanBuilder();
    if (StringUtils.isNotBlank(req.getName())) {
        builder.and(qUserDO.name.like(String.format("%%%s%%", req.getName())));
    }
    return findAll(builder, PageRequest.of(0,10));
}

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


QueryDSL JPAQuery 关联查询示例

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


JPASQLQuery

2.1.15. Using Native SQL in JPA queries
https://querydsl.com/static/querydsl/latest/reference/html/ch02.html

SQLTemplates templates = new DerbyTemplates();

// 单列
JPASQLQuery<?> query = new JPASQLQuery<Void>(entityManager, templates);
List<String> names = query.select(cat.name).from(cat).fetch();

// 多列
query = new JPASQLQuery<Void>(entityManager, templates);
List<Tuple> rows = query.select(cat.id, cat.name).from(cat).fetch();

// 实体
query = new JPASQLQuery<Void>(entityManager, templates);
List<Cat> cats = query.select(catEntity).from(cat).orderBy(cat.name.asc()).fetch();

// 关联查询
query = new JPASQLQuery<Void>(entityManager, templates);
cats = query.select(catEntity).from(cat)
    .innerJoin(mate).on(cat.mateId.eq(mate.id))
    .where(cat.dtype.eq("Cat"), mate.dtype.eq("Cat"))
    .fetch();

SpringDataJpa Sort 转换为 querydsl OrderSpecifier

Pageable pageable = PageRequest.of(0, 10, Sort.by("id"));
JPAQuery<UserDO> jpaQuery = jpaQueryFactory.select(qUserDO).from(qUserDO);
jpaQuery.offset(pageable.getOffset());
jpaQuery.limit(pageable.getPageSize());
PathBuilder<UserDO> orderByExpression = new PathBuilder<>(UserDO.class, "userDO");
for (Sort.Order o : pageable.getSort()) {
    jpaQuery.orderBy(new OrderSpecifier(o.isAscending() ? Order.ASC : Order.DESC,
            orderByExpression.get(o.getProperty())));
}

How can I convert a spring data Sort to a querydsl OrderSpecifier?
https://stackoverflow.com/questions/13072378/how-can-i-convert-a-spring-data-sort-to-a-querydsl-orderspecifier


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

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

阅读
评论
10.9k
阅读预计47分钟
创建日期 2020-07-16
修改日期 2021-08-31
类别
目录
  1. SpringBoot2 JPA多数据源配置
    1. application.properties
    2. HibernateVendorConfig
    3. db1的JPA配置
    4. db2的JPA配置(db2.enable=true时启用)
    5. 多数据源下获取 EntityManager 实例
    6. jdbcUrl is required with driverClassName
  2. SpringBoot JPA打印SQL及参数到文件
  3. Spring Data JPA
    1. Pageable 和 Sort 分页查询
    2. JPA的分页查询包含数据和count两个查询
    3. OrderSpecifier findAll排序示例
    4. @PageableDefault
    5. CrudRepository
      1. save
      2. save和saveAndFlush
      3. @NoRepositoryBean
    6. @EntityScan 和 @EnableJpaRepositories
    7. @Enumerated 枚举持久化
    8. @ColumnTransformer 读写转换
  4. Hibernate配置项
    1. hibernate.hbm2ddl.auto
    2. Hibernate命名策略
      1. Hibernate隐式命名策略和物理命名策略
      2. JPA大写表名自动转换为小写提示表不存在
      3. JPA实现根据SpringBoot命令行参数动态设置表名
  5. JPA
    1. Spring Repository 和 Jpa EntityManager
    2. EntityManager 执行native SQL
      1. createNativeQuery()执行原生select示例
      2. createNativeQuery()执行原生update示例
      3. createNativeQuery()原生sql查询部分字段示例
      4. JPA中 update/delete必须开启事务
      5. 带冒号的时间参数报错QueryException: Named parameter not bound
      6. NonUniqueDiscoveredSqlAliasException 字段重复
  6. JPA审计
  7. JPA MySQL 类型映射表
  8. Hibernate
    1. @Entity
    2. @Table
      1. name 指定表名
      2. indexes 索引
      3. uniqueConstraints 唯一约束
    3. @Column
      1. nullable
      2. columnDefinition
      3. insertable,updatetable
      4. @Id
      5. @GeneratedValue
      6. @Temporal
      7. JPA设置自动维护创建时间和更新时间
    4. JPA 设置列默认值
      1. 实体属性默认值
      2. DDL级默认值(columnDefinition)
      3. DDL级默认值(@ColumnDefault)
    5. @SecondaryTable
    6. @OneToOne 一对一关联
      1. @JoinColumn 外键关联
      2. @JoinTable 中间表关联
    7. @OneToMany 一对多关联
      1. @JoinColumn 外键关联
      2. @JoinTable 中间表关联
    8. @ManyToMany 多对多关联
      1. cascade 级联关系
        1. CascadeType.REMOVE和orphanRemoval=true区别
      2. @JoinTable 关联表
      3. 单向关联与双向关联
    9. fetch 加载策略
      1. 单测或非web请求时报错 LazyInitializationException
    10. Hibernate 映射规则
  9. JPA 中复杂查询的几种方案
  10. QueryDSL
    1. maven依赖
    2. apt-maven-plugin插件(生成Q类)
    3. BooleanBuilder 动态查询
      1. like 模糊匹配
    4. QueryDSL JPAQuery 关联查询示例
    5. JPASQLQuery
    6. SpringDataJpa Sort 转换为 querydsl OrderSpecifier

页面信息

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

评论