Jackson + Hibernate =很多问题

时间:2015-06-07 03:10:22

标签: java hibernate jackson jax-rs spring-boot

所以这是我的情况:我想使用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;
}

简单的数据模型。我们有一个简单的要求:在webservice上,当我们获得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: ...)

好吧,懒惰不起作用。很简单。只是让它急切。

Caused by: java.lang.StackOverflowError: 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:解决方案不相关,因为它们适用于一对多,而不是多对多,所以我仍然得到StackOverflowError。

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

Contestant #3:相同(没有人使用多对多???)

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

Contestant #5:乍一看,似乎工作正常!但是,只有Doctor端点可以工作 - 它正在获取服务。服务端点不起作用 - 它没有获得Doctors(空集)。它可能基于哪个引用定义了连接表。这再次不符合法案。

Contestant #6:没有。

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

  1. 为json序列化创建一组未被hibernate包装的新对象,然后在控制器中复制属性。这是一项额外的工作。强制使用这种模式无法实现使用Hibernate的目的。

  2. 加载Doctor后,遍历每个Service并将service.doctors设置为null,以防止进一步延迟加载。我正在尝试建立一套最佳实践,而不是提出破解性的解决方法。

  3. 那么...... RIGHT 解决方案是什么?我可以遵循哪种模式看起来干净,让我为使用Hibernate和Jackson感到自豪?或者这种技术组合是否与建议新范式不相容?

2 个答案:

答案 0 :(得分:2)

首先,关于你的陈述&#34; ...在控制器中复制属性。这是一项额外的工作。强制使用这种模式无法实现使用Hibernate的目的。&#34;:

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

问题不在于Hibernate&amp; Jackson,而是因为你试图将仪器用于某个目的,它从未被设计用于。

我猜你的项目往往会增长(通常他们都会这样做)。如果那是真的,那么你将不得不在某一天分开layers,而且要早一点再分开。所以我建议你坚持使用错误的解决方案#1&#34; (创建一个DTO)。您可以使用类似ModelMapper的内容来阻止将实体手写到DTO转换逻辑。

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

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

答案 1 :(得分:2)

我找到了一个看似优雅的解决方案。

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

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

  3. 使用 @JsonView See this helpful article.

  4. 首先,弄清楚这些观点是什么(一个针对医生,一个针对患者)

    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应用程序。我甚至认为它可以扩展到更大,更复杂的应用程序。但需要仔细维护。