私はHibernate永続性を備えたDropwizardを使用しています。Hibernateは、オブジェクトを永続化するSQLを生成する前に、オブジェクトの生成されたIDを「失っている」ように見えるため、頭痛の種になります。
私は3つのクラスがあります:Checklist
、Checkpoint
、ChecklistItem
、私は双方向の1対多の関係としてモデル化していました。
ドメインオブジェクト、DAO、それを公開するためのリソース、および一連のテスト(DAOテスト、およびすべての統合テスト)を構築しました。
私はいくつかの奇妙な(壊れた)振る舞いに気づきました:
Checklist
DAO単体テストでは、トランザクション内でオブジェクト(および子オブジェクト)を問題なくラウンドトリップできます。これが私がログに見るべきだと思うものです(良いDAOテストから取られました):
[main] DEBUG org.hibernate.engine.spi.ActionQueue - Executing identity-insert immediately
[main] DEBUG org.hibernate.SQL - insert into Checklists (id, description, locked, name, ownerUserId) values (null, ?, ?, ?, ?)
[main] DEBUG o.h.id.IdentifierGeneratorHelper - Natively generated identity: 1
[main] DEBUG org.hibernate.engine.spi.ActionQueue - Executing identity-insert immediately
[main] DEBUG org.hibernate.SQL - insert into Checkpoints (id, name, checklistId) values (null, ?, ?)
[main] DEBUG o.h.id.IdentifierGeneratorHelper - Natively generated identity: 1
[main] DEBUG org.hibernate.engine.spi.ActionQueue - Executing identity-insert immediately
[main] DEBUG org.hibernate.SQL - insert into ChecklistItems (id, description, name, checkpointId, state, url) values (null, ?, ?, ?, ?, ?)
[main] DEBUG o.h.id.IdentifierGeneratorHelper - Natively generated identity: 1
[main] DEBUG org.hibernate.engine.spi.ActionQueue - Executing identity-insert immediately
[main] DEBUG org.hibernate.SQL - insert into ChecklistItems (id, description, name, checkpointId, state, url) values (null, ?, ?, ?, ?, ?)
[main] DEBUG o.h.id.IdentifierGeneratorHelper - Natively generated identity: 2
[main] DEBUG org.hibernate.engine.spi.ActionQueue - Executing identity-insert immediately
[main] DEBUG org.hibernate.SQL - insert into ChecklistItems (id, description, name, checkpointId, state, url) values (null, ?, ?, ?, ?, ?)
[main] DEBUG o.h.id.IdentifierGeneratorHelper - Natively generated identity: 3
[main] DEBUG o.h.e.i.AbstractFlushingEventListener - Processing flush-time cascades
統合テストを実行したときに得られる(悪い)ものは次のとおりです。
127.0.0.1 - - "PUT /api/cl/v0.1/users HTTP/1.1" 201 - "-" "checklists-server (integration test client)" 240
DEBUG com.misys.uk.checklists.resources.ChecklistResource: Invoked ChecklistResource#createChecklist(uriInfo, newChecklist)
DEBUG org.hibernate.SQL: /* insert com.misys.uk.checklists.core.Checklist */ insert into Checklists (id, description, locked, name, ownerUserId) values (null, ?, ?, ?, ?)
DEBUG org.hibernate.SQL: /* insert com.misys.uk.checklists.core.Checkpoint */ insert into Checkpoints (id, name, checklistId) values (null, ?, ?)
WARN org.hibernate.engine.jdbc.spi.SqlExceptionHelper: SQL Error: 23502, SQLState: 23502
ERROR org.hibernate.engine.jdbc.spi.SqlExceptionHelper: NULL not allowed for column "CHECKLISTID"; SQL statement:
/* insert com.misys.uk.checklists.core.Checkpoint */ insert into Checkpoints (id, name, checklistId) values (null, ?, ?) [23502-189]
DEBUG org.hibernate.SQL: /* insert com.misys.uk.checklists.core.ChecklistItem */ insert into ChecklistItems (id, description, name, checkpointId, state, url) values (null, ?, ?, ?, ?, ?)
WARN org.hibernate.engine.jdbc.spi.SqlExceptionHelper: SQL Error: 23502, SQLState: 23502
ERROR org.hibernate.engine.jdbc.spi.SqlExceptionHelper: NULL not allowed for column "CHECKPOINTID"; SQL statement:
/* insert com.misys.uk.checklists.core.ChecklistItem */ insert into ChecklistItems (id, description, name, checkpointId, state, url) values (null, ?, ?, ?, ?, ?) [23502-189]
ERROR io.dropwizard.jersey.errors.LoggingExceptionMapper: Error handling a request: 319c21e9a8198202
! org.h2.jdbc.JdbcSQLException: NULL not allowed for column "CHECKPOINTID"; SQL statement:
! /* insert com.misys.uk.checklists.core.ChecklistItem */ insert into ChecklistItems (id, description, name, checkpointId, state, url) values (null, ?, ?, ?, ?, ?) [23502-189]
! at org.h2.message.DbException.getJdbcSQLException(DbException.java:345) ~[h2-1.4.189.jar:1.4.189]
これが私のドメインオブジェクトです:
@Entity
@Table(name = "Checklists")
public class Checklist {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(unique = true)
private long id = Constants.UNASSIGNED;
@Column(nullable = false)
private long ownerUserId;
@NotEmpty @Length(max = 64) @Column(nullable = false, length = 64)
private String name;
@Length(max = 2000) @Column(length = 2000)
private String description;
@Column(nullable = false)
private boolean locked;
@OneToMany(fetch = FetchType.EAGER, cascade = {CascadeType.ALL}) @OrderColumn(name = "cpOrderWithinCl")
@JoinTable(name = "ChecklistToCheckpoint",
joinColumns = @JoinColumn(name = "checklistId"),
inverseJoinColumns = @JoinColumn(name = "checkpointId"))
private List<Checkpoint> checkpoints;
public Checklist() {
// needed for Jackson
}
@JsonProperty
public long getId() {
return id;
}
public void setId(final long id) {
this.id = id;
}
@JsonProperty
public long getOwnerUserId() {
return ownerUserId;
}
public void setOwnerUserId(final long ownerUserId) {
this.ownerUserId = ownerUserId;
}
@JsonProperty
public String getName() {
return name;
}
public void setName(final String name) {
this.name = name;
}
@JsonProperty @JsonInclude(Include.NON_NULL)
public String getDescription() {
return description;
}
public void setDescription(final String description) {
this.description = description;
}
@JsonProperty
public List<Checkpoint> getCheckpoints() {
return checkpoints;
}
public void setCheckpoints(final List<Checkpoint> checkpoints) {
this.checkpoints = Preconditions.checkNotNull(checkpoints);
}
private void addCheckpoint(final Checkpoint newCheckpoint) {
if (checkpoints == null) {
checkpoints = new LinkedList<>();
}
newCheckpoint.setParent(this);
checkpoints.add(Preconditions.checkNotNull(newCheckpoint));
}
private void removeCheckpoint(final Checkpoint checkpoint) {
if (checkpoints != null) {
checkpoints.remove(Preconditions.checkNotNull(checkpoint));
}
}
@JsonProperty
public boolean isLocked() {
return locked;
}
public void setLocked(final boolean locked) {
this.locked = locked;
}
// equals() and hashCode() ...
}
エンティティCheckpoint
。これは、以下と1対多の関係にありChecklistItem
ます。
@Entity
@Table(name = "Checkpoints")
public class Checkpoint {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(unique = true)
private long id = Constants.UNASSIGNED;
@NotEmpty @Length(max = 64) @Column(nullable = false, length = 64)
private String name;
@ManyToOne(fetch = FetchType.EAGER) @JoinColumn(name = "checklistId", nullable = false)
private Checklist parent;
@OneToMany(fetch = FetchType.EAGER, cascade=CascadeType.ALL) @OrderColumn(name = "ciOrderWithinCp")
@JoinTable(name = "CheckpointToChecklistItem",
joinColumns = @JoinColumn(name = "checkpointId"),
inverseJoinColumns = @JoinColumn(name = "checklistItemId"))
private List<ChecklistItem> items;
public Checkpoint() {
// Needed for Jackson
}
@JsonProperty
public long getId() {
return id;
}
public void setId(final long id) {
this.id = id;
}
@JsonProperty
public String getName() {
return name;
}
public void setName(final String name) {
this.name = name;
}
@JsonIgnore
public Checklist getParent() {
return parent;
}
public void setParent(final Checklist parent) {
this.parent = parent;
}
@JsonProperty
public List<ChecklistItem> getItems() {
return items;
}
public void setItems(final List<ChecklistItem> items) {
this.items = Preconditions.checkNotNull(items);
}
public void addItem(final ChecklistItem newItem) {
if (items == null) {
items = new LinkedList<>();
}
Preconditions.checkNotNull(newItem).setParent(this);
items.add(newItem);
}
private void removeItem(final ChecklistItem item) {
if (items != null) {
items.remove(Preconditions.checkNotNull(item));
}
}
// equals() and hashCode() ...
}
エンティティChecklistItem
:
@Entity
@Table(name = "ChecklistItems")
public class ChecklistItem {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(unique = true)
private long id = Constants.UNASSIGNED;
@NotEmpty @Length(max = 64) @Column(nullable = false, length = 64)
private String name;
@Length(max = 2000) @Column(length = 2000)
private String description;
@Length(max = 2000) @Column(length = 2000)
private String url;
@ManyToOne(fetch = FetchType.EAGER) @JoinColumn(name = "checkpointId", nullable = false)
private Checkpoint parent;
@Column(nullable = false, length=16) @NotNull @StringEnumeration(enumClass = ChecklistItemState.class)
private String state = ChecklistItemState.UNCHECKED.name();
public ChecklistItem() {
// needed for Jackson
}
@JsonProperty
public long getId() {
return id;
}
public void setId(final long id) {
this.id = id;
}
@JsonProperty
public String getName() {
return name;
}
public void setName(final String name) {
this.name = name;
}
@JsonProperty @JsonInclude(Include.NON_NULL)
public String getDescription() {
return description;
}
public void setDescription(final String description) {
this.description = description;
}
@JsonProperty @JsonInclude(Include.NON_NULL)
public String getUrl() {
return url;
}
public void setUrl(final String url) {
this.url = url;
}
@JsonIgnore
public Checkpoint getParent() {
return parent;
}
public void setParent(final Checkpoint parent) {
this.parent = parent;
}
@JsonProperty
public ChecklistItemState getState() {
return ChecklistItemState.valueOf(state);
}
// equals() and hashCode() ...
}
私はそれがDBMSに関連していないことをかなり確信しています(私はいくつかの異なるものを試しましたが、同じ問題が発生します)、そしてそれがトランザクションの下で実行することとは何の関係もないことを確信しています(私はデバッグでコードをペッパーしましたprintln()
sこれを確認する)。
これは、ドメインオブジェクト間で1対多のマッピングを実行しようとしている方法と関係があると思われます。
何か案は?
問題は非常に単純でした。エンティティ間に双方向の関係が存在するHibernateを介してオブジェクトグラフを永続化する場合、それらの関係はHibernateによって両方向にナビゲートされます。それらが正しくない場合、Hibernateは失敗します。
ユニットテストで、それ自体が徹底的にテストされたBuilderを使用してオブジェクトを構築していたため、最初はこれをキャッチできませんでした。これについては説明したと思いました。明らかに違います!
バグはこの双方向の関係を処理するためにJSONアノテーション(またはその欠如)にあるため、統合テストは失敗していました-そしてこれは私の全体的なテストでのみ明らかになりました、それはそれが壊れたJSONを実行していたからです後方参照の動作!
@JsonManagedReference
および@JsonBackReference
を使用してオブジェクトに適切に注釈を付け、双方向参照(子->親、および親->子)を処理します。@JsonIgnore
。この記事はインターネットから収集されたものであり、転載の際にはソースを示してください。
侵害の場合は、連絡してください[email protected]
コメントを追加