上个月试用了下 JPA,发现了一些莫名其妙的问题,记录一下。java
版本:spring-boot-starter-data-jpa:2.2.0.RELEASE,mysql
MySQL:8.0.16,InnoDB 引擎,RR 隔离级别。spring
实体类:sql
@Data
@Entity
@Table(name = "people")
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonIgnoreProperties({ "hibernateLazyInitializer", "handler" })
public class People {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(name = "username")
private String username;
}
@Data
@Entity
@Table(name = "cat")
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonIgnoreProperties({ "hibernateLazyInitializer", "handler" })
public class Cat {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(name = "name")
private String name;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "owner")
private People owner;
}
复制代码
表数据:数据库
mysql> select * from people;
+----+----------+
| id | username |
+----+----------+
| 1 | zhangsan |
| 2 | laoli |
+----+----------+
2 rows in set (0.00 sec)
mysql> select * from cat;
+----+-------+-------+
| id | name | owner |
+----+-------+-------+
| 1 | kitty | 1 |
| 2 | mao | 2 |
| 3 | mi | 2 |
+----+-------+-------+
3 rows in set (0.00 sec)
复制代码
测试代码以下:缓存
public interface PeopleRepo extends JpaRepository<People, Integer>, JpaSpecificationExecutor<People> {
@Transactional
@Modifying
@Query(value = "update people set username = :username where id = :id", nativeQuery = true)
void updateUsernameById(@Param("id") int id, @Param("username") String username);
}
@Service
public class PeopleAndCatService {
@Autowired
private PeopleRepo peopleRepo;
@Transactional
public String test1(int id) {
People p1 = peopleRepo.findById(id).get() // 1
System.out.println("p1: " + p1);
peopleRepo.updateUsernameById(id, "ceeeeb"); // 2
People p2 = peopleRepo.findById(id).get() // 3
System.out.println("p2: " + p2);
return null;
}
}
复制代码
当调用接口 test1(1) 时,返回结果以下:session
p1: People(id=1, username=zhangsan)
p2: People(id=1, username=zhangsan)
复制代码
可是数据库中的数据已更改。app
缘由:一个事务(Propagation.REQUIRED 模式)对应一个 EntityManager。 执行步骤 1 的查询时,EntityManager 【SessionImpl】中已经存在 id=1 的实体类缓存,步骤 3 再次查询时,直接从缓存中获取,实际上并无查询数据库。less
解决方法(参考):数据库设计
@Modifying(clearAutomatically = true)
。org.hibernate.LazyInitializationException: could not initialize proxy - no Session
错误测试代码:
@Service
public class PeopleAndCatService {
@Autowired
private CatRepo catRepo;
@Transactional
public Cat test2(int id) {
Cat c = catRepo.findById(id).get(); // 1
return c;
}
}
@RestController
@RequestMapping("/jpa")
public class PeopleAndCatController {
@Autowired
private PeopleAndCatService peopleAndCatService;
@GetMapping("/test2")
public Cat test2(@RequestParam("id") int id) {
Cat cat = peopleAndCatService.test2(id);
return cat; // 2
}
}
复制代码
错误的缘由在于:最终在 controller 里面返回响应的时候,jackson 序列化 Cat 对象失败,更确切的说是序列化 Cat 对象里面的 owner 字段的时候报错。owner 字段是一个 People 对象,因为咱们设置了懒加载,因此步骤 1 查询时,实际的 sql 语句是 select * from cat where id = ?
,并无关联查询 People 表(可是拿到 owner 的 id 了)。
当执行步骤 2 时,须要序列化 People 对象,调用 people.getUsername() 方法时,会触发数据库查询操做(懒加载!),而此时 session 已关闭,所以报错。
解决方法:
不用懒加载,更改成 @ManyToOne(fetch = FetchType.EAGER)
;
在事务未结束时,好比步骤 1 后面,调用 c.getOwner().getUsername()
触发查询 People 表操做;
上面的方案最终仍是查询了 People 表,若是我根本就不关心 owner,不想查 People 表呢;能够改写方法以下:
@Transactional
public Cat test2(int id) {
Cat c = catRepo.findById(id).get();
People people = new People();
people.setId(c.getOwner().getId());
c.setOwner(people);
return c;
}
复制代码
不用外键,更改 Cat 类的 owner 字段为 int 类型,涉及关联查询时须要手写 sql,表之间的关联控制经过程序实现(涉及到数据库设计中是否须要外键之争了。。。)