Jackson + Hibernate = 很多问题

Jackson + Hibernate = lots of problems

这是我的情况:我想使用 Jackson 和 Hibernate 构建一个简单的 CRUD 网络服务。 Spring Boot 似乎是一个完美的工作。所以我们有以下内容:

(请注意,我正在压缩代码,因此它不可编译)

class Doctor {
  @Id
  long id;

  @ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
  @JoinTable(name = "doctor_service", joinColumns = { @JoinColumn(name = "doctor_id", nullable = false) }, inverseJoinColumns = { @JoinColumn(name = "service_id", nullable = false) })
  Set<Service> services;
}

class Service {
  @Id
  long id;

  @ManyToMany(fetch = FetchType.EAGER, mappedBy = "services")
  Set<Doctor> doctors;
}

一个简单的数据模型。我们有一个简单的要求:在 web 服务上,当我们获取服务对象时,我们应该获取关联的医生。当我们得到医生时,我们应该得到相关的服务。我们使用惰性是因为 [insert justification here].

所以现在让我们上菜:

@Path("/list")
@POST
@Produces(MediaType.APPLICATION_JSON)
@Transactional
public JsonResponse<List<Doctor>> list() {
  return JsonResponse.success(doctorCrudRepo.findAll());
}

忽略 JsonResponse 对象(目前只是一个方便的黑盒)并假设 doctorCrudRepo 是 CrudRepository 的有效实例。

风暴开始了:

failed to lazily initialize a collection of role: Doctor.services, could not initialize proxy - no Session (through reference chain: ...)

好的,那么 Lazy 就不起作用了。很简单。让它变得急切。

Caused by: java.lang.WhosebugError: null
    at java.lang.ClassLoader.defineClass1(Native Method)
    at java.lang.ClassLoader.defineClass(ClassLoader.java:760)
    at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
    at java.net.URLClassLoader.defineClass(URLClassLoader.java:455)
    at java.net.URLClassLoader.access0(URLClassLoader.java:73)
    at java.net.URLClassLoader.run(URLClassLoader.java:367)
    at java.net.URLClassLoader.run(URLClassLoader.java:361)
    at java.security.AccessController.doPrivileged(Native Method)
    at java.net.URLClassLoader.findClass(URLClassLoader.java:360)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:308)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
    at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:655)
    ... 1011 common frames omitted

所以让我们看看其他人是怎么说的:

Contestant #1:解决方案不相关,因为它们适用于一对多,不适用于多对多,所以我仍然得到 WhosebugError。

Contestant #2:和之前一样,还是一对多,还是Whosebug。

Contestant #3: 一样(没人用过多对多吗???)

Contestant #4:我不能使用@JsonIgnore,因为这意味着它永远不会被序列化。所以不符合要求。

Contestant #5: 乍一看,似乎工作正常!但是,只有 Doctor 端点有效——它正在获取服务。服务端点不起作用 - 它没有获得医生(空集)。它可能基于哪个引用定义了连接 table。这又不符合要求。

Contestant #6: 没有。

其他一些错误但值得一提的解决方案:

  1. 为 json 序列化创建一组未被休眠包装的新对象,然后将属性复制到控制器中。这是很多额外的工作。在任何地方强制使用这种模式都违背了使用 Hibernate 的目的。

  2. 加载医生后,循环每个服务并将service.doctors设置为空,以防止进一步延迟加载。我正在尝试建立一套最佳实践,而不是提出骇人听闻的解决方法。

那么...正确的解决方案是什么?我可以遵循什么样的模式看起来很干净并且让我以使用 Hibernate 和 Jackson 为荣?或者这种技术组合如此不相容以提出新的范例?

首先,关于您的声明“...将属性复制到控制器中。这是很多额外的工作。在任何地方强制使用这种模式都违背了使用 Hibernate 的目的。”:

这并没有违背使用 Hibernate 的初衷。 ORM 的创建是为了消除将从 JDBC 接收到的数据库行转换为 POJO 的必要性。 Hibernate 的延迟加载目的是在不需要出色性能或能够缓存实体时消除将自定义查询写入 RDBMS 的冗余工作。

问题不在于 Hibernate&Jackson,而是因为您试图将仪器用于某个目的,它从来没有设计过。

我猜你的项目会增长(通常他们都会)。如果这是真的,那么您将不得不分开 layers someday, and better sooner than later. So I would suggest you to stick to the "wrong solution #1" (create a DTO). You can use something like ModelMapper 以防止手写实体到 DTO 转换逻辑。

还要考虑到如果没有 DTO,您的项目可能会变得难以维护:

  • 数据模型会发展,您将始终需要根据更改更新您的前端。
  • 数据模型可能包含一些字段,您可能希望在发送给用户时忽略这些字段(例如用户的密码字段)。您始终可以创建额外的实体,但它们将需要额外的 DAO 等。
  • 有一天您可能需要return 用户一个由一些实体组成的数据。您可以编写一个新的 JPQL,例如 SELECT new ComplexObject(entity1, entity2, entity3) ...,但这比调用几个服务的方法并将结果组合到 DTO 中要困难得多。

我找到了一个看起来很优雅的解决方案。

  1. 使用 OpenEntityManagerInViewFilter。似乎不受欢迎(可能出于安全原因,但我还没有看到任何令人信服的理由不使用它)。使用简单,定义一个bean即可:

    @Component
    public class ViewSessionFilter extends OpenEntityManagerInViewFilter {
    }
    
  2. 对所有引用使用 LAZY。这就是我想要开始的,它特别重要,因为我的数据有很多参考,而我的服务很小。

  3. 使用@JsonViewSee this helpful article.

首先,弄清楚意见是什么(一种给医生,一种给患者)

public interface Views {
    public static interface Public {}
    public static interface Doctors extends Public {}
    public static interface Services extends Public {}
}

从医生视图看,您将看到服务。

@Entity
@Table(name = "doctor")
public class Doctor {

    @ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    @JoinTable(name = "doctor_service", joinColumns = { @JoinColumn(name = "doctor_id", nullable = false) },
            inverseJoinColumns = { @JoinColumn(name = "service_id", nullable = false) })
    @JsonView(Views.Doctors.class)
    private Set<Service> services;
}

从“服务”视图来看,您会看到医生。

@Entity
@Table(name = "service")
public class Service {

    @ManyToMany(fetch = FetchType.LAZY, mappedBy = "services")
    @JsonView(Views.Services.class)
    private Set<Doctor> doctors;

}

然后将视图分配给服务端点。

@Component
@Path("/doctor")
public class DoctorController {

    @Autowired
    DoctorCrudRepo doctorCrudRepo;

    @Path("/list")
    @POST
    @Produces(MediaType.APPLICATION_JSON)
    @JsonView(Views.Doctors.class)
    public JsonResponse<List<Doctor>> list() {
        return JsonResponse.success(OpsidUtils.iterableToList(doctorCrudRepo.findAll()));
    }

}

非常适合简单的 CRUD 应用程序。我什至认为它可以很好地扩展到更大、更复杂的应用程序。但需要小心维护。