2015-06-07 50 views
3

所以這裏是我的情況:我想用Jackson和Hibernate構建一個簡單的CRUD webservice。看起來像是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: ...) 

好了,所以懶惰不工作呢。夠簡單。只是讓它渴望。

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:乍一看,它似乎工作正常!但是,只有博士端點才能工作 - 它正在獲得服務。服務端點不起作用 - 它沒有得到醫生(空集)。它可能基於哪個引用定義了連接表。這又不符合法案。

Contestant #6:沒有。

一些其他的解決方案,是錯誤的,但值得一提:

  1. 創建一個新的組中不被休眠包裹的JSON序列化對象,然後在控制器複製的屬性。這是很多額外的工作。任何地方強制使用這種模式都會影響使用Hibernate的目的。

  2. 加載Doctor後,循環遍歷每個Service並將service.doctors設置爲null,以防止進一步的延遲加載。我正在嘗試建立一套最佳實踐,而不是提出駭人的解決方法。

因此......RIGHT解決方案是什麼?我可以遵循什麼樣的模式,看起來很乾淨,讓我爲使用Hibernate和Jackson感到驕傲?還是這種技術的組合如此矛盾以至於無法提出新的範式?

+0

你試過Hibernate.initialize(service.getDoctors())嗎? –

+0

是的。這是參賽者#1提出的解決方案之一。但是每個醫生都會再次提到服務,所以我得到了一個稍微不同的錯誤信息,但有着相同的根本原因。 –

+0

發佈實體的實際代碼,而不是它的某些派生部分。一般來說,如果你不休眠可能會感到困惑,你必須指定'mappedBy'。你也提到堆棧溢出,請添加堆棧跟蹤以查看它出錯的地方。理論上,當你正確地進行了設置時,急切的加載應該可行,我懷疑你的'@ Transactional'幾乎沒用,因爲它是一個JAX-RS而不是一個spring bean。 –

回答

2

我發現了一個似乎很優雅的解決方案。

  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應用程序。我甚至認爲它可以很好地擴展到更大,更復雜的應用程序。但它需要謹慎維護。

2

首先,關於你的聲明「......在複製在控制器中性能這是一個很多額外的工作迫使這個模式失敗到處使用Hibernate的目的。」:

它不打敗了使用Hibernate的目的。創建ORM是爲了消除將從JDBC接收的數據庫行轉換爲POJO的必要性。 Hibernate的延遲加載目的是消除在您不需要很好的性能或者您能夠緩存實體時將自定義查詢寫入RDBMS的冗餘工作。

這個問題不在休眠&傑克遜,但事實上,你試圖將該儀器用於某種目的,它從來沒有設計。

我想你的項目往往會增長(通常他們都會這樣做)。如果這是真的,那麼你將不得不分開layers有一天,並且比以後更快。所以我建議你堅持「錯誤的解決方案#1」(創建一個DTO)。您可以使用類似ModelMapper的東西來防止將實體手寫到DTO轉換邏輯。

同時認爲,沒有DTO的項目可能會變得難以維持:

  • 數據模式將演變,你將永遠有更新您的前端與變化。
  • 數據模型可能包含一些字段,您可能希望省略發送給用戶(如用戶的密碼字段)。你總是可以創建額外的實體,但他們將需要額外的DAO等。
  • 有一天,你可能需要返回用戶一個數據,這是一個實體的組成。您可以編寫一個新的JPQL,如SELECT new ComplexObject(entity1, entity2, entity3) ...,但這比調用少量服務的方法並將結果合併到DTO中要困難得多。
+0

我不喜歡這個解決方案,因爲涉及過多的工程。現在我有兩種不同的映射邏輯,一種給醫生提供服務,另一種給醫生服務,但是有共同的代碼。我需要保持乾燥的映射邏輯。自動映射「基於約定」聽起來像一個可怕的想法,並可能會打破無限循環的惰性加載的參考。 –