データベースからエンティティのインスタンスを削除しようとしているSpring 4アプリがあります。次のエンティティがあります。
@Entity
public class Token implements Serializable {
@Id
@SequenceGenerator(name = "seqToken", sequenceName = "SEQ_TOKEN", initialValue = 500, allocationSize = 1)
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "seqToken")
@Column(name = "TOKEN_ID", nullable = false, precision = 19, scale = 0)
private Long id;
@NotNull
@Column(name = "VALUE", unique = true)
private String value;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "USER_ACCOUNT_ID", nullable = false)
private UserAccount userAccount;
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "EXPIRES", length = 11)
private Date expires;
...
// getters and setters omitted to keep it simple
}
JpaRepositoryインターフェースが定義されています:
public interface TokenRepository extends JpaRepository<Token, Long> {
Token findByValue(@Param("value") String value);
}
インメモリデータベース(H2)で機能する単体テストのセットアップがあり、データベースに2つのトークンを事前に入力しています。
@Test
public void testDeleteToken() {
assertThat(tokenRepository.findAll().size(), is(2));
Token deleted = tokenRepository.findOne(1L);
tokenRepository.delete(deleted);
tokenRepository.flush();
assertThat(tokenRepository.findAll().size(), is(1));
}
最初のアサーションは成功し、2番目のアサーションは失敗します。トークン値を変更し、それをデータベースに保存する別のテストを試してみましたが、実際に機能するので、削除が機能しない理由はわかりません。例外もスローせず、データベースに永続化しません。 Oracleデータベースに対しても機能しません。
まだこの問題があります。これをTokenRepositoryインターフェースに追加することで、削除をデータベースに保持することができました。
@Modifying
@Query("delete from Token t where t.id = ?1")
void delete(Long entityId);
ただし、これは理想的なソリューションではありません。この余分な方法なしで動作させるために私がする必要があることについてのアイデアはありますか?
同じ問題があった
おそらく、UserAccountエンティティには、いくつかの属性にCascadeを持つ@OneToManyがあります。
削除するときに持続するよりも、カスケードを削除しました...
おそらく、このような動作は、双方向の関係があり、親と子の両方が永続化されている(現在のセッションに接続されている)間に両側を同期していない場合に発生します。
これには注意が必要です。次の例で説明します。
@Entity
public class Parent {
@Id
@GeneratedValue(strategy = IDENTITY)
@Column(name = "id", unique = true, nullable = false)
private Long id;
@OneToMany(cascade = CascadeType.PERSIST, mappedBy = "parent")
private Set<Child> children = new HashSet<>(0);
public void setChildren(Set<Child> children) {
this.children = children;
this.children.forEach(child -> child.setParent(this));
}
}
@Entity
public class Child {
@Id
@GeneratedValue(strategy = IDENTITY)
@Column(name = "id", unique = true, nullable = false)
private Long id;
@ManyToOne
@JoinColumn(name = "parent_id")
private Parent parent;
public void setParent(Parent parent) {
this.parent = parent;
}
}
テスト(トランザクション1つ)を書きましょう
public class ParentTest extends IntegrationTestSpec {
@Autowired
private ParentRepository parentRepository;
@Autowired
private ChildRepository childRepository;
@Autowired
private ParentFixture parentFixture;
@Test
public void test() {
Parent parent = new Parent();
Child child = new Child();
parent.setChildren(Set.of(child));
parentRepository.save(parent);
Child fetchedChild = childRepository.findAll().get(0);
childRepository.delete(fetchedChild);
assertEquals(1, parentRepository.count());
assertEquals(0, childRepository.count()); // FAILS!!! childRepostitory.counts() returns 1
}
}
非常に簡単なテストですよね?親と子を作成し、データベースに保存してから、データベースから子を取得し、削除して、すべてが期待どおりに機能することを確認します。そして、そうではありません。
ここでの削除は機能しませんでした。なぜなら、現在のセッションで保持されている関係の他の部分を同期しなかったからです。 Parentが現在のセッションに関連付けられていなかった場合、テストに合格します。
@Component
public class ParentFixture {
...
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void thereIsParentWithChildren() {
Parent parent = new Parent();
Child child = new Child();
parent.setChildren(Set.of(child));
parentRepository.save(parent);
}
}
そして
@Test
public void test() {
parentFixture.thereIsParentWithChildren(); // we're saving Child and Parent in seperate transaction
Child fetchedChild = childRepository.findAll().get(0);
childRepository.delete(fetchedChild);
assertEquals(1, parentRepository.count());
assertEquals(0, childRepository.count()); // WORKS!
}
もちろん、それは私のポイントを証明するだけであり、OPが直面した動作を説明します。適切な方法は、明らかに関係の両方の部分を同期させることです:
class Parent {
...
public void dismissChild(Child child) {
this.children.remove(child);
}
public void dismissChildren() {
this.children.forEach(child -> child.dismissParent()); // SYNCHRONIZING THE OTHER SIDE OF RELATIONSHIP
this.children.clear();
}
}
class Child {
...
public void dismissParent() {
this.parent.dismissChild(this); //SYNCHRONIZING THE OTHER SIDE OF RELATIONSHIP
this.parent = null;
}
}
ここでは明らかに@PreRemoveを使用できます。
PreRemove関数を追加する必要があります。たとえば、UserProfile Education.Javaと関係があるEducation Classの属性として多くのオブジェクトがあるクラスに追加します。
private Set<UserProfile> userProfiles = new HashSet<UserProfile>(0);
@ManyToMany(fetch = FetchType.EAGER, mappedBy = "educations")
public Set<UserProfile> getUserProfiles() {
return this.userProfiles;
}
@PreRemove
private void removeEducationFromUsersProfile() {
for (UsersProfile u : usersProfiles) {
u.getEducationses().remove(this);
}
}
私もこれを経験しました。私の場合、子テーブルにnull入力可能な外部キーフィールドを作成し、nullを設定してから親を削除し、saveおよびdeleteおよびflushを呼び出す必要がありました。
これを実行する前に、ログに削除または例外が表示されませんでした。
Spring Dataの新しいバージョンを使用する場合、deleteBy構文を使用できます。そのため、注釈の1つを削除できます。
次は、ビラはすでにJiraチケットによるものです: https://jira.spring.io/browse/DATAJPA-727
Idの初期値は500です。つまり、idは500で始まります。
@SequenceGenerator(name = "seqToken", sequenceName = "SEQ_TOKEN",
initialValue = 500, allocationSize = 1)
ここで、ID 1のアイテムを1つ選択します
Token deleted = tokenRepository.findOne(1L);
だからそれを明確にするためにデータベースをチェックしてください
1つの方法は、userAccountサービスで次のように_cascade = CascadeType.ALL
_を使用することです。
_@OneToMany(cascade = CascadeType.ALL)
private List<Token> tokens;
_
次に、次のような(または同様のロジック)を行います
_@Transactional
public void deleteUserToken(Token token){
userAccount.getTokens().remove(token);
}
_
_@Transactional
_注釈に注意してください。これにより、Spring(Hibernate)は、永続化するか、マージするか、メソッドで何をしているのかを知ることができます。上記の例を知ってくださいshould CascadeTypeが設定されていないかのように動作し、JPARepository.delete(token)
を呼び出します。
私は同じ問題を抱えています、テストは大丈夫ですが、データベース行は削除されません。
メソッドに@Transactionalアノテーションを追加しましたか?私にとって、この変更はそれを機能させる
私の場合はCASCADE.PERSISTで、CASCADE.ALLに変更し、カスケードを介して変更を行いました(父オブジェクトを変更)。
CascadeType.PERSISTとorphanRemoval = trueは一緒に機能しません。