JPA 使用过程中问题汇总(持续更新...)
文章目录
- 有事务时,JPA save方法无法捕获异常
- JPA引起cpu过高,转换成entity时太慢,使用原生JDBC查询
- JPA将已持久化的对象在开启事务时调用set方法重新设置某些属性字段值时,库里数据会发生改变
- 使用jpa自动生成表
- jpa.generate-ddl和jpa.hibernate.ddl-auto
- JPA save数据时,如果数据没有赋值,即使数据库层面设置了default值,但仍然会插入null
- JPA的save和saveAndFlush可以返回存储的实例,以获得存入数据的主键
- Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1
- javax.persistence.TransactionRequiredException: Executing an update/delete query
- JPA修改属性后再查询未生效问题
- JPA使用find自定义方法时,只能查出entity,不能查指定字段
有事务时,JPA save方法无法捕获异常
问题:
在类上有事务注解,在该类中,有一个方法中调用JPA save方法,但因为各种原因save应该报错,此时想捕获该异常,并不进行回滚和向上层抛出异常,但发现异常无法被捕获
情景再现:
entity实体类如下:
@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Table(name = "employee", uniqueConstraints = {@UniqueConstraint(columnNames={"department_id","company_id"})})
public class Employee {
@Id
@Column(name = "id", columnDefinition = "varchar(64)")
@GenericGenerator(name="idGenerator", strategy="uuid")
@GeneratedValue(generator = "idGenerator")
private String id;
@Column(name = "name", columnDefinition = "varchar(128) default null")
private String name;
@Column(name = "department_id", columnDefinition = "varchar(32) default null")
private String departmentId;
@Column(name = "company_id", columnDefinition = "varchar(32) default null")
private String companyId;
}
service层如下:
@Service
@Transactional(rollbackFor = Exception.class)
public class EmpServiceImpl implements EmpService {
@Resource
private EmployeeRepository employeeRepository;
@Override
public void updateName(String name, String companyId, String departmentId) {
Employee employee = employeeRepository.findByDepartmentIdAndCompanyId(departmentId, companyId);
if(Objects.isNull(employee)){
employee = Employee.builder().name(name).companyId(companyId).departmentId(departmentId).build();
} else {
employee.setName(name);
}
try {
employeeRepository.save(employee);
}catch (Exception e){
System.out.println("exception = " + e.getMessage());
}
}
}
controller层就是对该service层方法进行调用
当库里没有数据时,使用jmeter并发调用该接口,会存在并发问题,即都需要save一个新的Employee对象,当save同companyId和departmentId时,由于唯一索引的限制,会save失败。使用try-catch捕获异常,却未成功。
并发调用5次接口,有4次失败了,从输出看并没有打印catch中的语句,接口也报错了。
![在这里插入图片描述](https://img-blog.csdnimg.cn/4a349374d4f242e5a7e2b2f1717eaf7b.png#pic_center
原因分析
当employeeRepository.save时,JPA并没有马上把数据实体存储到数据库中,而是先存储在缓存中,然后正常执行后面代码逻辑,因为没将缓存中的数据刷到数据库,此时不会报错,try-catch没有异常可捕获,在提交事务时,尝试刷新缓存中的数据到数据库,发生联合索引重复的异常,此时异常无法被save的那个try-catch捕获。从打印的异常堆栈来看,异常是发生在flush后,提交事务时会先flush,把缓存中数据刷到数据库中时报错了。
** 解决**
去除该方法的事务,在方法前加上@Transactional(propagation = Propagation.NOT_SUPPORTED),事务传播属性改为不使用事务
再次用jmeter测试,发现接口都不再报错,并且异常也被catch住了
注意
之前看到网上的解决该问题的方式是把save改成saveAndFlush,经过测试,虽然try-catch能捕获异常,但是会报错回滚异常,接口仍然是会报回滚异常的错误.
经查询,saveAndFlush应该只是把sql数据刷到库里,并不提交事务,怀疑可能当时刷库时有问题,已经标记了一次需要回滚,而由于异常被catch住了,外层service提交事务时导致报错了。
JPA引起cpu过高,转换成entity时太慢,使用原生JDBC查询
遇到问题是CPU过高,通过火焰图看到是某些jpa的方法占用大量cpu。对此进行优化,改为使用原生JDBC查询,并自己转化成entity。
举个例子:
entity代码如下:
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Table(name = "user_info")
@DynamicInsert
@DynamicUpdate
public class User {
@Id
@Column(name = "id")
@GenericGenerator(name = "idGenerator", strategy = "uuid")
@GeneratedValue(generator = "idGenerator")
private String id;
@Column(name = "name")
private String name;
@Column(name = "age")
private Integer age;
}
根据group id 查用户的jpa方法:
@Repository
public interface UserRepository extends JpaRepository<User, Serializable>, JpaSpecificationExecutor<User> {
List<User> findAllByGroupId(String groupId);
}
改为使用原生JDBC查询
@Autowired
private JdbcTemplate jdbcTemplate;
public List<User> findUsers(String groupId) {
StringBuilder sql = new StringBuilder();
sql.append("select a.id, a.name name, a.age age, a.groupId groupId "
"from site_info a " +
"where a.group_id = :groupId");
NamedParameterJdbcTemplate npj = new NamedParameterJdbcTemplate(jdbcTemplate.getDataSource());
List<User> list = npj.query(sql.toString(), MapBuilder.create(new HashMap()).put("groupId", groupId).build(), (resultSet, rowNum) ->
Site.builder()
.id(resultSet.getString("id"))
.name(resultSet.getString("name"))
.age(resultSet.getInt("age"))
.groupId(resultSet.getString("groupId"))
.build());
return list;
}
注意
值得注意的是,接口ResultSet中的getInt,如果库里数据是null,则使用该方法会返回0。方法官方说明如下:
/**
* Retrieves the value of the designated column in the current row
* of this <code>ResultSet</code> object as
* an <code>int</code> in the Java programming language.
*
* @param columnLabel the label for the column specified with the SQL AS clause. If the SQL AS clause was not specified, then the label is the name of the column
* @return the column value; if the value is SQL <code>NULL</code>, the
* value returned is <code>0</code>
* @exception SQLException if the columnLabel is not valid;
* if a database access error occurs or this method is
* called on a closed result set
*/
int getInt(String columnLabel) throws SQLException;
若不想取默认值0,想取null,则需要使用getObject方法判断。如果不为空,再使用getInt。
resultSet.getObject("age") != null ? resultSet.getInt("age") : null
JPA将已持久化的对象在开启事务时调用set方法重新设置某些属性字段值时,库里数据会发生改变
开启了数据库事务时,查询一个已持久化对象,调用set方法设置属性,还并未调用save方法更新属性,但库里的属性会被更新。查询以及对查询后的实体set值都必须在一个事务里,并且在方法执行结束,事务提交前,不管有没有显示调用Update,JPA都会自动调用Update。
如下例,数据库中姓名和createtime会更新
@Transactional
@Override
public void testSet() {
User before = userRepository.findById("1");
before.setName("李四");
before.setCreateTime(System.currentTimeMillis());
}
若只是想set属性,并不更新库里的数据的话,可以:
- 提交事务之前执行EntityManager.clear()方法,清理缓存对象
@PersistenceContext
private EntityManager entityManager;
@Transactional
@Override
public void testSet() {
User before = userRepository.findById("1");
before.setName("李四");
before.setCreateTime(System.currentTimeMillis());
entityManager.clear();
}
- 避免直接对查询对象进行set操作,使用BeanUtils.copyProperties或mapper先转换成另一个类对象,再对该对象进行属性set。
@Transactional
@Override
public void testSet() {
User user = userRepository.findById("1");
UserVo res = new UserVo();
BeanUtils.copyProperties(user, res);
res.setName("新名字");
System.out.println(res);
}
- 使用EntityManager.evict()方法清理指定对象
@PersistenceContext
private EntityManager entityManager;
@Transactional
@Override
public void testSet() {
User before = userRepository.findById("1");
before.setName("李四");
before.setCreateTime(System.currentTimeMillis());
HibernateEntityManager hibernateEntityManager = (HibernateEntityManager) entityManager;
Session session = hibernateEntityManager.getSession();
session.evict(before);
}
使用jpa自动生成表
配置文件增加:spring.jpa.hibernate.ddl-auto=update
写Entity实体类
@Entity(name = "user")
@Data
public class User {
@Id
@Column(name = "id", columnDefinition = "varchar(64)")
@GenericGenerator(name="idGenerator", strategy="uuid")
@GeneratedValue(generator = "idGenerator")
private String id;
@Column(name = "name", columnDefinition = "varchar(128) default null")
private String name;
@Basic
private Integer age;
@Column(name = "create_time")
private Long createTime;
@Column(name = "remark", columnDefinition = "varchar(255) default null")
private String remark;
}
启动工程,会自动创建表格
开启打印sql,在控制台输出可以看到sql语句:
Hibernate: create table user (id varchar(64) not null, age integer, create_time bigint, name varchar(128) default null, remark varchar(255) default null, primary key (id)) engine=InnoDB
注意,create表格时可能会报如下错误:Specified key was too long; max key length is 767 bytes
是因为MySQL的InnoDB引擎表索引字段长度的限制为767字节,因此对于多字节字符集的大字段或者多字段组合,创建索引时会出现该问题。
以utf8mb4字符集字符串类型字段为例。utf8mb4是4字节字符集,默认支持的索引字段最大长度是191字符(767字节/4字节每字符≈191字符),因此在varchar(255)或char(255)类型字段上创建索引会失败。详情请参见MySQL官网文档。
所以在主键id上增加columnDefinition = “varchar(64)”,设置主键长度为64。避免出现该问题,可以自动创建表格成功。
jpa.generate-ddl和jpa.hibernate.ddl-auto
jpa:
database: mysql #数据库类型MySQL
show-sql: false #不打印sql语句
generate-ddl: false
hibernate:
ddl-auto: none #不执行datasource.schema脚本
jpa.generate-ddl和jpa.hibernate.ddl-auto都可以控制是否执行datasource.schema脚本,来初始化数据库结构,只要有一个为可执行状态就会执行,比如ddl为true,或者update。他们相互没有制约关系。
必须二者都为false或none,才能不执行schema脚本。
数据库中的Schema是什么?
JPA save数据时,如果数据没有赋值,即使数据库层面设置了default值,但仍然会插入null
增加表中一个字段时的sql如下:
ALTER TABLE account ADD status tinyint(1) DEFAULT b'1' COMMENT '是否加入黑名单';
调用jpa的save方法时,并没有build实体类中status,即没有给该字段赋值,导致入库的值是空值,而不是预想中的1。
所以,修改为save时传true。sql保证的是原生sql insert时会设置默认值,使用JPA时是不行的。
JPA的save和saveAndFlush可以返回存储的实例,以获得存入数据的主键
通常主键都自动生成,我们存数据时是不设置主键的,但有业务要拿到插入数据的主键,需要接收save的返回的实例,再getId
Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1
解决Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1的一种方法
javax.persistence.TransactionRequiredException: Executing an update/delete query
解决javax.persistence.TransactionRequiredException: Executing an update/delete query的一种方法
JPA修改属性后再查询未生效问题
JPA的Repository提供一种非常易用的机制用于ORM方式处理数据,但是如果需要一次性更新一批数据的部分字段,构造所有实体并逐个修改字段再存回数据库就显得有些臃肿。在spring-data-JPA中提供了@Query注解用于使用JPQL执行数据库操作(可以使用nativeQuery属性改用原生sql),如果数据库操作是修改数据而非查询数据,则需要再额外使用@Modifying注解提示JPA该操作是修改操作。
当进行 find 操作时,JPA 在 EntityManager 中缓存了 find 生成的对象,当再次 find 时会直接返回该对象。于是可能会出现下面这种情况:
实体类是User类,包含id,姓名,年龄等属性。
用 @Query 定义一个修改姓名的方法
@Repository
public interface UserRepository extends JpaRepository<User, Serializable> {
User findById(String id);
@Transactional
@Modifying
@Query(value = "update user set name = :name where id = :id", nativeQuery = true)
int updateNameById(String name, String id);
}
user对象id为1,name为王五,先读取该对象,再修改对象状态,再次读取对象,使用save方法修改对象属性,再读取对象
User before = userRepository.findById("1");
System.out.println("before " + before);
userRepository.updateNameById("张三","1");
User after = userRepository.findById("1");
System.out.println("after " + after);
after.setName("李四");
userRepository.save(after);
User afterSave = userRepository.findById("1");
System.out.println("afterSave " + afterSave);
控制台输入如下:
before User(id=1, name=王五, age=18, createTime=null, remark=null)
after User(id=1, name=王五, age=18, createTime=null, remark=null)
afterSave User(id=1, name=李四, age=18, createTime=null, remark=null)
结果会发现before和after中虽然id相同,但name并没有修改过来。修改后使用save方法更新,才得以成功。
原因是@Query跟find和save系列方法是两套不同的体系,@Query引起的数据库变更,但EntityManager并不能发现,不能及时反应到JPA的find系列方法上来,因此find时仍取的缓存中的数据,也就是修改前的数据。
解决方式:
在需要时显式清理EntityManager的缓存,@Modifying(clearAutomatically = true),clearAutomatically性为true时,执行完modifying query之后就会清理缓存,从而在下次find时就可以读取到数据库中的最新值。
注意
自动清理之后还会带来一个新的问题,clear操作清理的缓存中,还包括提交后未flush的数据,例如调用 save 而不是saveAndFlush就有可能不会立即将修改内容更新到数据库中,在save后flush前进行了clearAutomatically,有可能导致修改丢失。可以使用另一个属性@Modifying(clearAutomatically = true, flushAutomatically = true),@Modifying 的flushAutomatically属性为 true 时,执行modifying query前会先调用flush操作,从而避免数据丢失问题。
存疑:使用junit test测试时,不会出现此问题
JPA使用find自定义方法时,只能查出entity,不能查指定字段
当只想查询数据库里某个字段的值时,不能自己写findXXXByXXX方法,JPA会报类型转换错误,因为JPA的find只能查出entity,不能指定查出某字段。
示例如下,想查出指定姓名的全部备注数据:
entity类
@Entity(name = "user")
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@DynamicInsert
@DynamicUpdate
public class User {
@Id
@Column(name = "id", columnDefinition = "varchar(64)")
@GenericGenerator(name="idGenerator", strategy="uuid")
@GeneratedValue(generator = "idGenerator")
private String id;
@Column(name = "name", columnDefinition = "varchar(128) default null")
private String name;
@Basic
private Integer age;
@Column(name = "create_time")
private Long createTime;
@Column(name = "remark", columnDefinition = "varchar(255) default null")
private String remark;
}
repository接口
public interface UserRepository extends JpaRepository<User, Serializable>, JpaSpecificationExecutor<User> {
List<User> findByName(String name);
// 这样写会报错
List<String> findRemarkByName(String name);
}
上层调用
// 本意是想直接调用方法查出remark list
userRepository.findRemarkByName("Mary");
// 先查出所有符合的entity,再用stream处理返回
userRepository.findByName("Mary").stream().map(User::getRemark).collect(Collectors.toList());
结果:
调用findRemarkByName,会报错,如下图,而另外一个方法不会报错。
解决方法
改为原生sql查询
@Query(value = "SELECT remark FROM user WHERE name = :name ", nativeQuery = true)
List<String> findRemarkByName(String name);