当前位置 : 首页 » 文章分类 :  开发  »  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


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));

可以直接用 q类 的字段排序,更方便:

return findAll(booleanBuilder, qStatisticDO.statDate.desc());

@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 {
}

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


JPA EntityManager

数据库持久化中间件中都有一个操作数据库的对象,在原生的 Hibernate 中是 Session,在 JPA 中是 EntityManager,在 MyBatis 中是 SqlSession,通过这个对象来操作数据库。

EntityManager是 JPA 中用于增删改查的接口,连接内存中的 java 对象和数据库的数据存储。
HibernateEntityManager(Hibernate 5.2 后此类已标为废弃,使用直接实现 EntityManager 接口的 Session 或 SessionImplementor 代替)是 Hibernate 对 EntityManager 接口的实现。

EntityManager 由 EntityManagerFactory 所创建。EntityManagerFactory 作为 EntityManager 的工厂,包含有当前 O-R 映射的元数据信息,每个 EntityManagerFactory 可称为一个持久化单元(PersistenceUnit),每个持久化单元可认为是一个数据源的映射(每个数据源,可理解为一个数据库,可以在应用服务器中配置多个数据源,同时使用不同的 PersistenceUnit 来映射这些数据源,从而能够很方便的实现跨越多个数据库之间的事务操作)

PersistenceContext 称为持久化上下文,它一般包含有当前事务范围内的,被管理的实体对象(Entity)的数据。每个 EntityManager 都会跟一个 PersistenceContext 相关联。PersistenceContext 中存储的是实体对象的数据,而关系数据库中存储的是记录,EntityManager 正是维护这种OR映射的中间者,它可以把数据从数据库中加载到 PersistenceContext 中,也可以把数据从PersistenceContext 中持久化到数据库,EntityManager 通过 persist, merge, remove, refresh, flush 等操作来操纵 PersistenceContext 与数据库数据之间的同步

通过容器来传递 PersistenceContext,而不是应用程序自己来传递 EntityManager。这种方式(由容器管理着 PersistenceContext 并负责传递到不同的 EntityManager)称为 容器管理的实体管理器(Container-Managed EntityManager),它的生命周期由容器负责管理,编程人员不需要考虑 EntityManger 的连接,释放以及复杂的事务问题等。

JPA 之 Hibernate EntityManager 使用指南
https://blog.csdn.net/footless_bird/article/details/129294444


EntityManager 执行 Native SQL

public Query createNativeQuery(String sqlString);
public Query createNativeQuery(String sqlString, Class resultClass);
public Query createNativeQuery(String sqlString, String resultSetMapping);

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() 执行原生delete示例

String deleteSql = String.format("delete from %s where %s < '%s'", tableName, fieldName, deleteBefore);
try {
    int result = entityManager.createNativeQuery(deleteSql).executeUpdate();
} catch (Exception e) {
    log.error("delete sql error {}", deleteSql, e);
}

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 必须开启事务

执行 update/delete 报错:

javax.persistence.TransactionRequiredException: Executing an update/delete query
    at org.hibernate.internal.AbstractSharedSessionContract.checkTransactionNeededForUpdateOperation(AbstractSharedSessionContract.java:422)

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

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

Not allowed to create transaction on shared EntityManager

Spring 中通过 entityManager 手动操作事务

entityManager.getTransaction().begin();
entityManager.getTransaction().commit();

报错:
Not allowed to create transaction on shared EntityManager - use Spring transactions or EJB CMT
改为使用 Spring 的事务注解 @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)

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


native sql like setParameter 报错

Query q = em.createQuery("SELECT x FROM org.SomeTable x WHERE x.someString LIKE '%:someSymbol%'");
q.setParameter("someSymbol", "someSubstring");

报错:org.hibernate.QueryParameterException: could not locate named parameter

原因:
jpa native sql 中拼接 like 语句比较特别,不能直接拼

解决:
1、在参数中拼接 %%

Query q = em.createQuery( "SELECT x FROM org.SomeTable x WHERE x.someString LIKE :someSymbol" );
q.setParameter("someSymbol", "%someSubstring%");

2、或者使用 concat 连接占位符和 %

Query q = em.createQuery( "SELECT x FROM org.SomeTable x WHERE x.someString like CONCAT('%', :someSymbol, '%')" );
q.setParameter("someSymbol", "%someSubstring%");

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 中的重复字段。


createQuery 执行 CriteriaQuery 条件查询

public <T> TypedQuery<T> createQuery(CriteriaQuery<T> criteriaQuery); 
public Query createQuery(CriteriaUpdate updateQuery);
public Query createQuery(CriteriaDelete deleteQuery);
public List<UserDO> query() {
    CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
    CriteriaQuery<UserDO> criteriaQuery = criteriaBuilder.createQuery(UserDO.class);
    Root<UserDO> root = criteriaQuery.from(UserDO.class);
    Predicate predicate = criteriaBuilder.lessThanOrEqualTo(root.get("createdDate").as(Date.class), DateUtil.parse("2023-10-26 14:05:27"));
    criteriaQuery.where(predicate);
    criteriaQuery.select(root);
    criteriaQuery.orderBy(criteriaBuilder.asc(root.get("id")));
    return entityManager.createQuery(criteriaQuery)
                        .setMaxResults(10)
                        .getResultList();
}

JPA Criteria Queries
https://www.baeldung.com/hibernate-criteria-queries


contains() 判断实体是否在session中

内部通过 persistenceContext.getEntry(object).getStatus() 获取对象状态,是 MANAGED,READ_ONLY,DELETED,GONE,LOADING,SAVING 之一,只要不是 DELETED 和 GONE 状态就是在 session 中。

public boolean contains(Object entity) {
    ...
    EntityEntry entry = persistenceContext.getEntry(object);
    ...
    return entry.getStatus() != Status.DELETED && entry.getStatus() != Status.GONE;
}

Spring Repository 和 Jpa EntityManager

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


从 EntityManager 获取 Hibernate Session

Session session = entityManager.unwrap(org.hibernate.Session.class);

从 EntityManager 获取 JDBC Connection

java.sql.Connection connection = entityManager.unwrap(java.sql.Connection.class);


从 EntityManager 获取指定实体类对应的数据库表名

JPA 规范包含 Metamodel API,该API使您可以查询有关托管类型及其托管字段的信息。

public static <T> String getTableName(EntityManager em, Class<T> entityClass) {
    Metamodel meta = em.getMetamodel();
    EntityType<T> entityType = meta.entity(entityClass);
    Table t = entityClass.getAnnotation(Table.class);
    return Optional.ofNullable(t).map(Table::name).orElse(entityType.getName().toUpperCase());
}

How to retrieve mapping table name for an entity in JPA at runtime?
https://stackoverflow.com/questions/2342075/how-to-retrieve-mapping-table-name-for-an-entity-in-jpa-at-runtime

根据JPA EntityManager获取实体类与表的映射关系
https://blog.csdn.net/bang2tang2/article/details/118543196


从 EntityManager 获取实体类字段名对应的数据表列名

public String getColumnName(Class entityClass, Field field) {
    MetamodelImplementor metamodel = (MetamodelImplementor) entityManager.getMetamodel();
    AbstractEntityPersister entityPersister = (AbstractEntityPersister) metamodel.entityPersister(entityClass);
    return entityPersister.getPropertyColumnNames(field.getName())[0];
}

How to get Column names of JPA entity
https://stackoverflow.com/questions/46335682/how-to-get-column-names-of-jpa-entity


JPA 事务

Not allowed to create transaction on shared EntityManager

报错:

java.lang.IllegalStateException: Not allowed to create transaction on shared EntityManager - use Spring transactions or EJB CMT instead
    at org.springframework.orm.jpa.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler.invoke(SharedEntityManagerCreator.java:259)

原因:
使用 Spring 的 @Transactional 开启了事务,同时 JPA 方法内又调用了 entityManager.getTransaction() 打算操作事务,就会报这个错。

解决:删除自己手动写的 JPA 事务代码或去掉 @Transactional 注解


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

JPA

@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

@javax.persistence.Table

name 指定表名

indexes/@Index 索引

单字段索引:

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

多字段索引、多索引:

@Table(name = "user", indexes = {
        @Index(name = "idx_name_age", columnList = "name,age", unique = true),
        @Index(name = "idx_type", columnList = "type")
})

uniqueConstraints/@UniqueConstraint 唯一约束

方法1、在表级别 @Table 注解上加 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 {
}

多字段唯一索引+多字段索引

@Table(name = "school_class_user_relation",
        indexes = {@Index(name = "idx_class_user", columnList = "classId, userId")},
        uniqueConstraints = {@UniqueConstraint(name = "uk_school_class_user",
                columnNames = {"schoolId", "classId", "userId"})
})

方法2、在列级别 @Column 注解上加 unique=true/false 属性,只能指定单列唯一索引

@MappedSuperclass

@MappedSuperclass 是类级别注解,该注解没有任何参数,被该注解标注的类不会映射到数据库中单独的表,但该类所拥有的属性都将映射到其子类的数据库表的列中。
@MappedSuperclass 一般放在多个表结构的通用父类上

@Column

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

@Column 注解定义了将成员属性映射到关系表中的哪一列和该列的结构信息,属性如下:
secondaryTable 从表名。如果此列不建在主表上(默认是主表),该属性定义该列所在从表的名字。

name 列名

name 映射的列名。如:映射 tbl_user 表的 name 列,可以在 name 属性的上面或 getName 方法上面加入;

nullable 是否可为null

nullable 是否允许为空;

unique 是否唯一键

unique 是否唯一;
unique=true 时会自动创建唯一索引。
如果想指定多列唯一索引,需要在表级别 @Table 注解上加 uniqueConstraints 属性

length 列长度

length 对于字符型列,length 属性指定列的最大字符长度;

columnDefinition

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

@Column(nullable = false, unique = true, columnDefinition = "varchar(255) COMMENT '唯一编码'")
private String code;

insertable,updatetable

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

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

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
);

@Lob LongText 字段

@javax.persistence.Lob
Jpa + MySQL 中,@Lob 注解的 String 实体类字段会 autoddl 自动生成 LongText 类型的 MySQL 列。
例如

@Lob
private String context;

自动创建的 MySQL 表中 context 字段为

context            longtext                       null,

如果想生成 Text 类型的 MySQL 字段,可以将实体字段定义为如下,这里 @Lob 加不加都可以,没影响

@Lob
@Column(columnDefinition = "TEXT COMMENT '上下文'")
private String context;

@Id 指定主键

@Id 注解指定表的主键,它可以有多种生成方式:
TABLE 使用一个特定的数据库表格来保存主键。
SEQUENCE 根据底层数据库的序列来生成主键,条件是数据库支持序列,Oracle支持,Mysql不支持。
IDENTITY 由底层数据库生成标识符。一般为自增主键,需要底层数据库支持自动增长字段类型,如DB2、SQL Server、MySQL等,Oracle这类没有自增字段的不支持。
AUTO 默认的配置。如果不指定主键生成策略,默认为AUTO。把主键生成策略交给持久化引擎(persistence engine)决定。Oracle 默认是序列方式,Mysql 默认是主键自增长方式。

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@GeneratedValue

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

@Transient

@Transient 映射忽略的字段,该字段不会保存到 mysql 中。
比如某个字段想做临时变量,不想存储到 msyql 中,不想给这个字段映射列,可以使用此注解。


@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 类型


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

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 注解来设置列默认值,加了这个注解的字段自动生成的 MySQL 表结构有 default 'enabled' 属性。
需要注意的是,设置 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;//地址
    ...
}

@MapsId 共享主键

在子表实体上使用 @MapsId


@OneToMany 一对多关联

假设订单 order 和 产品 product 是一对多的关系,即一个订单对应多个产品。

@JoinColumn 外键关联

@JoinColumn 注解通常和 @ManyToOne、@OneToOne 和 @OneToMany 注解一起使用,用于指定外键列的名称。
如果不使用 @JoinColumn 注解,Hibernate 会默认使用关联的两个实体类的名字来生成外键列的名称

@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 外键列,不会生成中间表。


Cannot add foreign key constraint

现象:
user 表
user_relation 表,其中 user_id 引用 user 表的 id
springboot 服务启动时报错

2021-10-27 18:44:32.361 [main] WARN  o.h.t.schema.internal.ExceptionHandlerLoggedImpl.handleException:27 - GenerationTarget encountered exception accepting command : Error executing DDL "alter table user_relation add constraint FKg0rru97p2392h3ghnwtd30ph7 foreign key (user_id) references user (id)" via JDBC Statement
org.hibernate.tool.schema.spi.CommandAcceptanceException: Error executing DDL "alter table ne_prototype_element add constraint FKg0rru97p2392h3ghnwtd30ph7 foreign key (schema_id) references ne_prototype_schema (id)" via JDBC Statement
    at org.hibernate.tool.schema.internal.exec.GenerationTargetToDatabase.accept(GenerationTargetToDatabase.java:67)

Caused by: java.sql.SQLException: Cannot add foreign key constraint
    at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:965)

原因:
由于设置了 autoddl, springboot 服务启动时 jpa 会尝试维护mysql中的表结构,userDO 中有 @OneToMany 映射指定 @JoinColumn ,会尝试给 user_relation 表的 user_id 加外键
但 user_relation 表 user_id 字段的类型错了,是 varchar 类型,和 user 表主键 id 的类型 bigint 不匹配,导致添加外键失败。

解决:
alter table user_relation modify column user_id bigint not null; 修改 user_relation 表的 user_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 表


A collection with cascade=”all-delete-orphan” was no longer referenced by the owning entity instance

问题:
父类实体里有个子类的实体列表

@Entity
class Parent {
    @OneToMany(mappedBy="parent", cascade=CascadeType.ALL, orphanRemoval=true)
    private List<Children> children;
}

单独创建了几个 Children 实体,然后又更新了 Parent 几个字段(没操作 children 字段),save 时报错:
org.springframework.orm.jpa.JpaSystemException: A collection with cascade=”all-delete-orphan” was no longer referenced by the owning entity instance: com.persistence.model.ParentDO.children;

解决:
Children 不单独 save,实体先保存到 list,set 到 parent 的 children 字段后,随 parent 一起保存。

https://codippa.com/how-to-resolve-a-collection-with-cascadeall-delete-orphan-was-no-longer-referenced-by-the-owning-entity-instance/


@ManyToMany 多对多关联

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

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

@ManyToMany 多对多关系属性
mappedBy 声明于关系的被维护方,带有此属性的是被维护端
fetch 加载策略

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


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

object references an unsaved transient instance

User 表里 @ManyToMany @JoinTable 关联 一个 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;
}

插入数据时报错:
org.hibernate.TransientObjectException: object references an unsaved transient instance - save the transient instance before flushing:

原因:
没有级联保存关联的标签表的数据

解决:
增加级联配置: @ManyToMany(cascade=CascadeType.ALL)

How to fix the Hibernate “object references an unsaved transient instance - save the transient instance before flushing” error
https://stackoverflow.com/questions/2302802/how-to-fix-the-hibernate-object-references-an-unsaved-transient-instance-save


mappedBy 关联关系由谁维护

在 @OneToMany 上使用 mappedBy,


fetch 加载策略

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

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

hibernate 中默认是懒加载

加缓存报错 LazyInitializationException

主实体DO 的查询加上缓存后报错
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.xxDO, no session or session was closed
Hibernate Annotation 的默认的 FetchType 在 ManyToOne 是 EAGER 的,在 OneToMany 上默认的是 LAZY
解决:
@OneToMany 增加 fetch = FetchType.EAGER

MultipleBagFetchException

@OneToMany 增加 fetch = FetchType.EAGER 后启动报错
Caused by: javax.persistence.PersistenceException: [PersistenceUnit: default] Unable to build Hibernate SessionFactory; nested exception is org.hibernate.loader.MultipleBagFetchException: cannot simultaneously fetch multiple bags: [child1, child2]
Caused by: org.hibernate.loader.MultipleBagFetchException: cannot simultaneously fetch multiple bags: [child1, child2]

解决:
增加 @Fetch(value = FetchMode.SUBSELECT)

@OneToMany(mappedBy="parent", fetch=FetchType.EAGER)
@Fetch(value = FetchMode.SUBSELECT)
private List<Child> childs;

https://stackoverflow.com/questions/4334970/hibernate-throws-multiplebagfetchexception-cannot-simultaneously-fetch-multipl

单测或非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

FetchType.EAGER 导致子类删除无效

@Entity
class Parent {
    @OneToMany(mappedBy="parent", cascade=CascadeType.ALL, fetch = FetchType.EAGER)
    private List<Children> children;
}

单独删除 children 不生效,去掉 fetch = FetchType.EAGER 解决

Spring JPA repository delete method doesn’t work
https://stackoverflow.com/questions/63030917/spring-jpa-repository-delete-method-doesnt-work


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>

Idea 找不到生成的Q类

在 DO 类所在的模块 Maven 上点右键 -> Generate Sources and Update Folders 即可

升级Java 17

Error:java: java.lang.NoClassDefFoundError: javax/annotation/Generated

https://stackoverflow.com/questions/48238014/how-can-be-solved-java-lang-noclassdeffounderror-javax-annotation-generated

Spring Boot 3.x 升级中遇到的 querydsl 与 JDK 17 兼容性问题
https://blog.csdn.net/m0_37970303/article/details/131151971


BooleanBuilder 动态查询

like 模糊匹配

注意:
1、需要自己拼接包含 % 的模糊匹配串
2、**String.format() 需要两个百分号 %% 来转义百分号,嫌麻烦可以用+号拼接 "%" + name + "%"**

例如:

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


JPAQueryFactory join 关联查询示例

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


JPAQueryFactory limit 查询示例

public TaskDO findLatestTask(String id) {
    return jpaQueryFactory.selectFrom(qTaskDO)
            .where(qTaskDO.id.eq(imageId))
            .orderBy(qTaskDO.createdDate.desc())
            .limit(1)
            .fetchOne();
}

JPAQueryFactory Projections 投影select部分字段示例

1、通过 Projections.bean 工具构造投影类
2、分页和排序需要手动处理

public Page<UserDO> findList(BooleanBuilder builder, Pageable pageable) {
    JPAQuery<UserDO> jpaQuery = jpaQueryFactory.select(Projections.bean(UserDO.class,
                                                        qUser.id,
                                                        qUser.createdDate,
                                                        qUser.name,
                                                        qUser.province,
                                                        qUser.city,
                                                        qUser.county))
                                                .from(qUser)
                                                .where(builder);
    long count = jpaQuery.fetchCount();
    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())));
    }
    return new PageImpl<>(jpaQuery.fetch(), pageable, count);
}

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


JPAQueryFactory 使用 MySQL 函数做 where 条件

使用 Expressions.booleanTemplate 可直接使用任何 MySQL 函数

JPAQuery<UserDO> jpaQuery = jpaQueryFactory
    .select(qUserDO)
    .from(qUserDO)
    .where(
        Expressions.booleanTemplate("find_in_set({0}, {1}) > 0", 1, "1,2,3")
        .and(Expressions.booleanTemplate("UNIX_TIMESTAMP({0}) >= {1}", qUserDO.birthdayDate, new Date().getTime() / 1000))
    );

https://stackoverflow.com/questions/22984343/how-to-call-mysql-function-using-querydsl


JPAQueryFactory 实现 count = count + 1 语句

有多线程并发更新 count 字段时,使用 count = count + 1 这样的语句来更新,数据库会保证这个操作的原子性,不会出现并发问题。

JPAQueryFactory queryFactory = new JPAQueryFactory(entityManager);
QMyTable myTable = QMyTable.myTable;

queryFactory.update(myTable)
    .set(myTable.count, myTable.count.add(1))
    .where(myTable.id.eq(someId))
    .execute();

JPAQueryFactory 实现 set status = (status = bad) ? bad: good

JPAQueryFactory queryFactory = new JPAQueryFactory(entityManager);
QMyTable myTable = QMyTable.myTable;
jpaQueryFactory.update(myTable)
                .set(myTable.result, new CaseBuilder()
                        .when(myTable.result.eq("bad"))
                        .then("bad")
                        .otherwise(inputResult))
                .where(myTable.id.eq(id))
                .execute();

JPAQueryFactory order by field 指定排序

NumberExpression<Integer> sortCaseExpression = new CaseBuilder()
        .when(qUserDO.status.eq("College")).then(1)
        .when(qUserDO.status.eq("HighSchool")).then(2)
        .when(qUserDO.status.eq("MiddleSchool")).then(3)
        .when(qUserDO.status.eq("PrimarySchool")).then(4)
        .otherwise(5);
JPAQuery<UserDO> jpaQuery = jpaQueryFactory.selectFrom(qUserDO);
jpaQuery.orderBy(sortCaseExpression.asc());
List<UserDO> instances = jpaQuery.where(predicate)
                                .offset(0)
                                .limit(10))
                                .fetch();

JPAQueryFactory sum 查询示例

public Integer sumUserAge(String classId) {
    return jpaQueryFactory.select(qTaskDO.age.sum())
                            .from(qTaskDO)
                            .where(qTaskDO.classId.eq(classId))
                            .fetchOne();
}

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

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

阅读
评论
12.5k
阅读预计56分钟
创建日期 2020-07-16
修改日期 2024-09-10
类别
目录
  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. 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. SpringBoot JPA打印SQL及参数到文件
  3. JPA EntityManager
    1. EntityManager 执行 Native SQL
      1. createNativeQuery() 执行原生select示例
      2. createNativeQuery() 执行原生update示例
      3. createNativeQuery() 执行原生delete示例
      4. createNativeQuery() 原生sql查询部分字段示例
      5. JPA中 update/delete 必须开启事务
      6. Not allowed to create transaction on shared EntityManager
      7. 带冒号的时间参数报错QueryException: Named parameter not bound
      8. native sql like setParameter 报错
      9. NonUniqueDiscoveredSqlAliasException 字段重复
    2. createQuery 执行 CriteriaQuery 条件查询
    3. contains() 判断实体是否在session中
    4. Spring Repository 和 Jpa EntityManager
    5. 从 EntityManager 获取 Hibernate Session
    6. 从 EntityManager 获取 JDBC Connection
    7. 从 EntityManager 获取指定实体类对应的数据库表名
    8. 从 EntityManager 获取实体类字段名对应的数据表列名
  4. JPA 事务
    1. Not allowed to create transaction on shared EntityManager
  5. JPA审计
  6. JPA MySQL 类型映射表
  7. JPA
    1. @Entity
    2. @Table
      1. name 指定表名
      2. indexes/@Index 索引
      3. uniqueConstraints/@UniqueConstraint 唯一约束
    3. @MappedSuperclass
    4. @Column
      1. name 列名
      2. nullable 是否可为null
      3. unique 是否唯一键
      4. length 列长度
      5. columnDefinition
      6. insertable,updatetable
      7. JPA设置自动维护创建时间和更新时间
    5. @Lob LongText 字段
    6. @Id 指定主键
    7. @GeneratedValue
    8. @Transient
    9. @Temporal 日期字段
    10. @Enumerated 枚举持久化
    11. @ColumnTransformer 读写转换
    12. JPA 设置列默认值
      1. 实体属性默认值
      2. DDL级默认值(columnDefinition)
      3. DDL级默认值(@ColumnDefault)
    13. @SecondaryTable
    14. @OneToOne 一对一关联
      1. @JoinColumn 外键关联
      2. @JoinTable 中间表关联
      3. @MapsId 共享主键
    15. @OneToMany 一对多关联
      1. @JoinColumn 外键关联
      2. Cannot add foreign key constraint
      3. @JoinTable 中间表关联
      4. A collection with cascade=”all-delete-orphan” was no longer referenced by the owning entity instance
    16. @ManyToMany 多对多关联
      1. @JoinTable 关联表
      2. 单向关联与双向关联
    17. cascade 级联关系
      1. CascadeType.REMOVE和orphanRemoval=true区别
      2. object references an unsaved transient instance
    18. mappedBy 关联关系由谁维护
    19. fetch 加载策略
      1. 加缓存报错 LazyInitializationException
      2. MultipleBagFetchException
      3. 单测或非web请求时报错 LazyInitializationException
      4. FetchType.EAGER 导致子类删除无效
    20. Hibernate 映射规则
  8. JPA 中复杂查询的几种方案
  9. QueryDSL
    1. maven依赖
    2. apt-maven-plugin插件(生成Q类)
      1. Idea 找不到生成的Q类
    3. 升级Java 17
    4. BooleanBuilder 动态查询
      1. like 模糊匹配
    5. JPAQueryFactory join 关联查询示例
    6. JPAQueryFactory limit 查询示例
    7. JPAQueryFactory Projections 投影select部分字段示例
    8. JPASQLQuery
    9. SpringDataJpa Sort 转换为 querydsl OrderSpecifier
    10. JPAQueryFactory 使用 MySQL 函数做 where 条件
    11. JPAQueryFactory 实现 count = count + 1 语句
    12. JPAQueryFactory 实现 set status = (status = bad) ? bad: good
    13. JPAQueryFactory order by field 指定排序
    14. JPAQueryFactory sum 查询示例

页面信息

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

评论