it-swarm.com.ru

Удалить не работает с JpaRepository

У меня есть приложение 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), и я предварительно заполняю базу данных двумя токенами:

@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));
}

Первое утверждение проходит, второе не проходит. Я попробовал другой тест, который изменяет значение токена и сохраняет его в базе данных, и он действительно работает, поэтому я не уверен, почему удаление не работает. Он также не генерирует никаких исключений, просто не сохраняет его в базе данных. Это не работает против моей базы данных Oracle либо.


Правка

Все еще есть эта проблема. Я смог получить удаление для сохранения в базе данных, добавив это в мой интерфейс TokenRepository:

@Modifying
@Query("delete from Token t where t.id = ?1")
void delete(Long entityId);

Однако это не идеальное решение. Любые идеи относительно того, что мне нужно сделать, чтобы это работало без этого дополнительного метода?

27
Twisty McGee

У меня такая же проблема

Возможно, ваша сущность UserAccount имеет @OneToMany с Cascade для какого-либо атрибута.

Я только что удалил каскад, чем он мог сохраниться при удалении ...

19
Davi Arimateia

Вам необходимо добавить функцию PreRemove в классе, где у вас есть много объектов в качестве атрибута, например, в классе образования, которые имеют отношение к UserProfileEducation.Java

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);
    }
}
6
Taimur

Скорее всего, такое поведение возникает, когда у вас двунаправленные отношения, и вы не синхронизируете обе стороны, пока сохраняются оба родителя и потомок (привязанный к текущему сеансу). 

Это сложно, и я собираюсь объяснить это на следующем примере.

@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;
    }
}

Давайте напишем тест (транзакционный между прочим)

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 и child, сохраняем его в базе данных, затем извлекаем дочерний элемент из базы данных, удаляем его и наконец проверяем, что все работает так, как ожидалось. И это не так.

Удаление здесь не сработало, потому что мы не синхронизировали другую часть отношений, которая СОСТОИТСЯ В ТЕКУЩЕЙ СЕССИИ. Если родитель не был связан с текущим сеансом, наш тест прошел бы, то есть.

@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!
}

Конечно, это только подтверждает мою точку зрения и объясняет поведение, с которым столкнулся ОП. Правильный путь, очевидно, заключается в синхронизации обеих частей отношений, что означает:

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 можно использовать здесь.

4
pzeszko

Я только что прошел через это тоже. В моем случае я должен был сделать так, чтобы у дочерней таблицы было поле внешнего ключа, которое можно обнулять, а затем удалить родителя из отношения, установив значение null, затем вызвав save, delete и flush. 

Я не видел удаления в журнале или каких-либо исключений до этого. 

3
Lucas Holt

Если вы используете более новую версию Spring Data, вы можете использовать синтаксис deleteBy ... чтобы вы могли удалить одну из своих аннотаций: P

следующее, что поведение уже отслеживается билетом Jira: https://jira.spring.io/browse/DATAJPA-727

2
Eruvanos

Ваше начальное значение для идентификатора составляет 500. Это означает, что ваш идентификатор начинается с 500

@SequenceGenerator(name = "seqToken", sequenceName = "SEQ_TOKEN",
initialValue = 500, allocationSize = 1)

И вы выбираете один элемент с идентификатором 1 здесь

 Token deleted = tokenRepository.findOne(1L);

Итак, проверьте вашу базу данных, чтобы уточнить, что

1
Bitman

Одним из способов является использование cascade = CascadeType.ALL в вашем сервисе userAccount:

@OneToMany(cascade = CascadeType.ALL)
private List<Token> tokens;

Затем сделайте что-то вроде следующего (или аналогичной логики)

@Transactional
public void deleteUserToken(Token token){
    userAccount.getTokens().remove(token);
}

Обратите внимание на аннотацию @Transactional. Это позволит Spring (Hibernate) узнать, хотите ли вы сохранить, объединить или что-то еще, что вы делаете в методе. AFAIK Пример выше должен работать так, как будто у вас не установлен CascadeType, и вызывать JPARepository.delete(token).

0
venge