Jackson + Hibernate = 很多问题

Posted

技术标签:

【中文标题】Jackson + Hibernate = 很多问题【英文标题】:Jackson + Hibernate = lots of problems 【发布时间】:2015-08-21 18:31:03 【问题描述】:

这是我的情况:我想使用 Jackson 和 Hibernate 构建一个简单的 CRUD Web 服务。似乎是 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 服务上,当我们获取 Service 对象时,我们应该获取相关的 Doctors。当我们得到医生时,我们应该得到相关的服务。我们使用惰性是因为 [在此处插入理由]。

现在让我们为它服务吧:

@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.***Error: 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.access$100(URLClassLoader.java:73)
    at java.net.URLClassLoader$1.run(URLClassLoader.java:367)
    at java.net.URLClassLoader$1.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:解决方案不相关,因为它们适用于一对多,而不是多对多,所以我仍然收到 ***Error。

Contestant #2:和以前一样,仍然是一对多,仍然是 ***。

Contestant #3: 一样(没有人用过many-to-many???)

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

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

Contestant #6:没有。

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

    为 json 序列化创建一组新的不被 hibernate 包装的对象,然后将属性复制到控制器中。这是很多额外的工作。在任何地方都强制使用这种模式会破坏使用 Hibernate 的目的。

    加载 Doctor 后,循环遍历每个 Service 并将 service.doctors 设置为 null,以防止进一步延迟加载。我正在尝试建立一套最佳实践,而不是想出一些骇人听闻的变通办法。

那么...正确的解决方案是什么?我可以遵循什么样的模式,看起来很干净,让我为使用 Hibernate 和 Jackson 感到自豪?还是这种技术组合如此格格不入,无法提出新的范式?

【问题讨论】:

你试过 Hibernate.initialize(service.getDoctors()) 吗? 是的。这是为参赛者 #1 提出的解决方案之一。但是每个医生都再次引用了服务,所以我收到的错误消息略有不同,但根本原因相同。 发布实体的实际代码,而不是它的派生部分。通常你必须指定mappedBy,如果你不休眠可能会混淆。您还提到堆栈溢出,请添加堆栈跟踪以查看哪里出错了。理论上,当您正确设置急切加载应该可以工作时,我怀疑您的 @Transactional 几乎没有用,因为它是 JAX-RS 而不是 spring bean。 查看我的编辑。如果它不是 spring bean 那么为什么 @Autowired 工作? 也许this 【参考方案1】:

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

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

问题不在于 Hibernate&Jackson,而在于您尝试将仪器用于某个目的这一事实,它从未被设计用于。

我猜您的项目往往会增长(通常它们都会增长)。如果那是真的,那么您将不得不在某天将layers 分开,而且最好还是尽快。所以我建议你坚持“错误的解决方案#1”(创建一个 DTO)。您可以使用 ModelMapper 之类的东西来防止手写实体到 DTO 转换逻辑。

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

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

【讨论】:

我不喜欢这个解决方案,因为涉及到过多的工程。现在我有 2 种不同的映射逻辑,一种用于带服务的医生,另一种用于带医生的服务,但其中有共享的公共代码。我需要保持这个映射逻辑干燥。 “基于约定”的自动映射听起来是个糟糕的主意,而且很可能会因延迟加载引用的无限循环而中断。【参考方案2】:

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

    使用 OpenEntityManagerInViewFilter。似乎不赞成(可能出于安全原因,但我没有看到任何令人信服的理由不使用它)。使用简单,只需要定义一个bean:

    @Component
    public class ViewSessionFilter extends OpenEntityManagerInViewFilter 
    
    

    在所有引用上使用 LAZY。这是我想要开始的,这尤其重要,因为我的数据有很多引用并且我的服务很小。

    使用 @JsonView。 See 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 应用。我什至认为它可以很好地扩展到更大、更复杂的应用程序。但需要小心维护。

【讨论】:

以上是关于Jackson + Hibernate = 很多问题的主要内容,如果未能解决你的问题,请参考以下文章

Hibernate + Jersey + Jackson 随机获得“org.hibernate.TransactionException:不支持嵌套事务”

序列化 Hibernate 对象时抛出奇怪的 Jackson 异常

延迟加载问题:Hibernate 3、Spring 4、Jackson 2.4.0

覆盖由 jackson-datatype-hibernate 生成的 id 名称

使用 Spring Boot、Jackson 和 Hibernate 的多对多关系

原因:org.hibernate.MappingException:无法确定类型:com.fasterxml.jackson.databind.JsonNode