2018年4月11日 星期三

Spring Data JPA 的 JOIN 操作

其實在 Spring 上要做 JOIN,還挺麻煩的…
如果可以的話,我覺得能不要做就不要做比較好 XD

不過真的需要的時候,如果是需要 SELECT 時做 JOIN,推薦可以參考 [1-2] 的說明。
實務應用的話,假設以 logback 輸出到 SQL Server 的格式作為範例
想要一次取得整個 log 包含相關的 property 和 stack trace,以下的 Entity 範例是能夠套用。

logback 輸出 SQL Server 的 schema

schema 可以參考 [3],以下為 schema 的內容:

CREATE TABLE logging_event 
  ( 
    timestmp          DECIMAL(20) NOT NULL,
   	formatted_message VARCHAR(4000) NOT NULL,
    logger_name       VARCHAR(254) NOT NULL,
    level_string      VARCHAR(254) NOT NULL,
    thread_name       VARCHAR(254),
    reference_flag    SMALLINT,
    arg0              VARCHAR(254),
    arg1              VARCHAR(254),
    arg2              VARCHAR(254),
    arg3              VARCHAR(254),
    caller_filename   VARCHAR(254) NOT NULL,
    caller_class      VARCHAR(254) NOT NULL,
    caller_method     VARCHAR(254) NOT NULL,
    caller_line       CHAR(4) NOT NULL,
    event_id          DECIMAL(40) NOT NULL identity,
    PRIMARY KEY(event_id) 
  ) 

CREATE TABLE logging_event_property 
  ( 
    event_id          DECIMAL(40) NOT NULL, 
    mapped_key        VARCHAR(254) NOT NULL, 
    mapped_value      VARCHAR(1024), 
    PRIMARY KEY(event_id, mapped_key), 
    FOREIGN KEY (event_id) REFERENCES logging_event(event_id) 
  ) 

CREATE TABLE logging_event_exception 
  ( 
    event_id         DECIMAL(40) NOT NULL, 
    i                SMALLINT NOT NULL, 
    trace_line       VARCHAR(254) NOT NULL, 
    PRIMARY KEY(event_id, i), 
    FOREIGN KEY (event_id) REFERENCES logging_event(event_id) 
  )
Entity

假設我的 Entity 物件名字是 Activity、ActivityProperty、ActivityStackTrace,分別定義如下:

Activity

@Entity(name = "logging_event")
public class Activity implements Serializable {
  @Id
  @Column(name = "event_id")
  private Long eventId;
  
  @Column(name = "timestmp")
  private Long timestmp;
  
  @Column(name = "formatted_message")
  private String message;
  
  @Column(name = "logger_name")
  private String logger;
  
  @Column(name = "level_string")
  private String level;
  
  @Column(name = "thread_name")
  private String thread;
  
  @Column(name = "reference_flag")
  private Integer reference;
  
  @Column(name = "arg0")
  private String arg0;
  
  @Column(name = "arg1")
  private String arg1;
  
  @Column(name = "arg2")
  private String arg2;
  
  @Column(name = "arg3")
  private String arg3;
  
  @Column(name = "caller_filename")
  private String callerFile;
  
  @Column(name = "caller_class")
  private String callerClass;
  
  @Column(name = "caller_method")
  private String callerMethod;
  
  @Column(name = "caller_line")
  private String callerLine;
  
  @OneToMany(
      mappedBy = "eventId",
      cascade = CascadeType.ALL,
      fetch = FetchType.EAGER)
  private List<ActivityProperty> properties;
  
  @OneToMany(cascade = CascadeType.ALL)
  @JoinColumn(name = "event_id", referencedColumnName = "event_id")
  @LazyCollection(LazyCollectionOption.FALSE)
  private List<ActivityStackTrace> stackTraces;
}

ActivityProperty

@Entity(name = "logging_event_property")
public class ActivityProperty implements Serializable {
  private static final long serialVersionUID = -6532428503805543364L;

  @Id
  @Column(name = "event_id")
  private Long eventId;
  
  @Column(name = "mapped_key")
  private String key;
  
  @Column(name = "mapped_value")
  private String value;
}

ActivityStackTrace

@Entity(name = "logging_event_exception")
public class ActivityStackTrace implements Serializable {
  private static final long serialVersionUID = -50856985039937262L;

  @EmbeddedId
  private ActivityStackTraceId id;
  
  @Column(name = "trace_line")
  private String line;
}

ActivityStackTraceId

@Embeddable
public class ActivityStackTraceId implements Serializable {
  private static final long serialVersionUID = -4496556483758827756L;

  @Column(name = "event_id")
  private Long eventId;
  
  @Column(name = "i")
  private Integer i;
}

其中在 Activity 要連結 ActivityProperty 和 ActivityStackTrace 時,都是以 @OneToMany 來標注
用來表示一個 Activity 可以有多個 ActivityProperty 和 ActivityStackTrace。
此外,由於 ActivityStackTrace 並非是單一 ID,而是 Composite Primary Key 的形式
因此要利用 @Embeddable來組合。這部份可以參考 [4]。
同時,使用了 @Embeddable 之後,如果 JOIN 操作依然使用 mappedBy 的話,會出現 [5] 提到的錯誤
目前其實還沒有搞很懂這個錯誤是什麼意思,不過姑且使用 [5] 的作法,改成用 LazyCollection 可以解決。
未來如果有把問題搞懂的話,再回來追加補充了 @@a

除此之外,還得稍微注意一下,上述的 JOIN 寫法,有可能導致 [6] 描述的問題
也就是當要做資料刪除時,Hibernate 產生的 SQL Statement 會用很沒效率的方式在刪除關聯
而且某些狀況下有可能會刪除沒有預期要刪除的資料。

參考資料
  1. JPA,在@OneToMany里加入mappedBy属性
  2. hibernate基于注解的维护权反转:@OneToMany(mappedBy=)
  3. mssql.sql
  4. The best way to map a Composite Primary Key with JPA and Hibernate
  5. Hibernate cannot simultaneously fetch multiple bags
  6. Why you should avoid CascadeType.REMOVE for to-many associations and what to do instead
  7. @JoinColumn 详解

沒有留言: