JPA问题记录

1,120 阅读3分钟

上个月试用了下 JPA,发现了一些莫名其妙的问题,记录一下。

版本:spring-boot-starter-data-jpa:2.2.0.RELEASE,

MySQL:8.0.16,InnoDB 引擎,RR 隔离级别。

0. 初始数据

实体类:

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

1. 在同一事务中更新数据后读不到最新的值

测试代码如下:

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) 时,返回结果如下:

p1: People(id=1, username=zhangsan)
p2: People(id=1, username=zhangsan)

但是数据库中的数据已更改。

原因:一个事务(Propagation.REQUIRED 模式)对应一个 EntityManager。 执行步骤 1 的查询时,EntityManager 【SessionImpl】中已经存在 id=1 的实体类缓存,步骤 3 再次查询时,直接从缓存中获取,实际上并没有查询数据库。

解决方法(参考)

  • 步骤 2 的手写 sql 改成 JpaRepository 提供的 save(S entity) 方法;
  • 或者在执行步骤 2 后,清空下该 session 的缓存,@Modifying(clearAutomatically = true)

2. 懒加载导致的 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,表之间的关联控制通过程序实现(涉及到数据库设计中是否需要外键之争了。。。)