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
每一页的大小,默认为20sort
排序相关的信息,以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 一起保存。
@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.REMOVE
和orphanRemoval=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;
单测或非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 映射规则
实体类必须用
@javax.persistence.Entity
进行注解;必须使用
@javax.persistence.Id
来注解一个主键;实体类必须拥有一个 public 或者 protected 的无参构造函数,之外实体类还可以拥有其他的构造函数;
实体类必须是一个顶级类(top-level class)。一个枚举(enum)或者一个接口(interface)不能被注解为一个实体;
实体类不能是 final 类型的,也不能有 final 类型的方法;
如果实体类的一个实例需要用传值的方式调用(例如,远程调用),则这个实体类必须实现(implements)
java.io.Serializable
接口。
JPA 中复杂查询的几种方案
1 使用注解 @Query
, 在其中拼接 SQL 或 HQL
2 使用 JPA 提供的 Specification
,通过 Predicate
和 CriteriaBuilder
拼接查询条件
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
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();
}
页面信息
location:
protocol
: host
: hostname
: origin
: pathname
: href
: document:
referrer
: navigator:
platform
: userAgent
: