web-dev-qa-db-fra.com

Gestion des suppressions douces avec Spring JPA

J'ai une table Stuff définie comme ...

id, <fields>..., active

Actif est l'indicateur de suppression logicielle et est toujours 1 ou 0. À long terme, cela peut disparaître au profit d'un tableau historique.

public interface StuffRepository extends JpaRepository<StuffEntity, Long> {} 

Dans le code, nous toujours utilisons des enregistrements actifs. Existe-t-il un moyen pour que Spring ajoute toujours une condition active=1 aux requêtes générées pour ce référentiel? Ou plus idéalement, me permettre d’étendre la grammaire utilisée pour générer les requêtes?

Je comprends que je peux créer un @queues nommé partout, mais je perds la commodité des requêtes générées. Je veux aussi éviter de polluer l'interface avec des méthodes "actives".

J'utilise Hibernate 4.2 comme implémentation de JPA si cela compte.

40
Andrew White

C'est une vieille question et vous avez probablement déjà trouvé la réponse. MAIS, pour tous les programmeurs Spring/JPA/Hibernate cherchant une réponse - 

Disons que vous avez une entité chien:

 @Entity
 public class Dog{

 ......(fields)....        

 @Column(name="is_active")
 private Boolean active;
 }

et un référentiel:

public interface DogRepository extends JpaRepository<Dog, Integer> {
} 

Tout ce que vous avez à faire est d'ajouter l'annotation @Where au niveau de l'entité, ce qui donne:

@Entity
@Where(clause="is_active=1")
public class Dog{

......(fields)....        

@Column(name="is_active")
private Boolean active;
}

Toutes les requêtes effectuées par le référentiel filtreront automatiquement les lignes "non actives".

62
Shay Elkayam

@Where(clause="is_active=1") n’est pas le meilleur moyen de gérer la suppression progressive avec des données de ressort jpa.

Premièrement, cela ne fonctionne qu'avec les outils Hibernate.

Deuxièmement, vous ne pouvez jamais extraire des entités supprimées douces avec des données de printemps.

Ma solution est fournie par les données du printemps. #{#entityName} expression peut être utilisée sur un référentiel générique représentant un nom de type d'entité concret.

Et le code sera comme ça:

//Override CrudRepository or PagingAndSortingRepository's query method:
@Override
@Query("select e from #{#entityName} e where e.deleteFlag=false")
public List<T> findAll();

//Look up deleted entities
@Query("select e from #{#entityName} e where e.deleteFlag=true")
public List<T> recycleBin(); 

//Soft delete.
@Query("update #{#entityName} e set e.deleteFlag=true where e.id=?1")
@Modifying
public void softDelete(String id); 
54
易天明

Basé sur 天明 answer, j'ai créé une implémentation de CrudRepository avec des méthodes de remplacement pour la suppression logicielle

@NoRepositoryBean
public interface SoftDeleteCrudRepository<T extends BasicEntity, ID extends Long> extends CrudRepository<T, ID> {
  @Override
  @Transactional(readOnly = true)
  @Query("select e from #{#entityName} e where e.isActive = true")
  List<T> findAll();

  @Override
  @Transactional(readOnly = true)
  @Query("select e from #{#entityName} e where e.id in ?1 and e.isActive = true")
  Iterable<T> findAll(Iterable<ID> ids);

  @Override
  @Transactional(readOnly = true)
  @Query("select e from #{#entityName} e where e.id = ?1 and e.isActive = true")
  T findOne(ID id);

  //Look up deleted entities
  @Query("select e from #{#entityName} e where e.isActive = false")
  @Transactional(readOnly = true)
  List<T> findInactive();

  @Override
  @Transactional(readOnly = true)
  @Query("select count(e) from #{#entityName} e where e.isActive = true")
  long count();

  @Override
  @Transactional(readOnly = true)
  default boolean exists(ID id) {
      return findOne(id) != null;
  }

  @Override
  @Query("update #{#entityName} e set e.isActive=false where e.id = ?1")
  @Transactional
  @Modifying
  void delete(Long id);


  @Override
  @Transactional
  default void delete(T entity) {
      delete(entity.getId());
  }

  @Override
  @Transactional
  default void delete(Iterable<? extends T> entities) {
      entities.forEach(entitiy -> delete(entitiy.getId()));
  }

  @Override
  @Query("update #{#entityName} e set e.isActive=false")
  @Transactional
  @Modifying
  void deleteAll();
}

Il pourrait être utilisé avec BasicEntity:

@MappedSuperclass
public abstract class BasicEntity {
  @Column(name = "is_active")
  private boolean isActive = true;

  public abstract Long getId();

  // isActive getters and setters...
}

Et entité finale:

@Entity
@Table(name = "town")
public class Town extends BasicEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "town_id_seq")
    @SequenceGenerator(name = "town_id_seq", sequenceName = "town_id_seq", allocationSize = 1)
    protected Long id;

    private String name;

    // getters and setters...
}
20
vadim_shb

Dans les versions actuelles (jusqu'à la version 1.4.1), il n'y a pas de support dédié aux suppressions progressives dans Spring Data JPA. Cependant, je vous encourage fortement à jouer avec la branche de fonctionnalités de DATAJPA-307 car il s'agit d'une fonctionnalité actuellement utilisée pour la prochaine version. 

Pour utiliser l'état actuel, mettez à jour la version que vous utilisez vers la version 1.5.0.DATAJPA-307-SNAPSHOT et assurez-vous de la laisser extraire la version spéciale de Spring Data Commons à utiliser. Vous devriez pouvoir suivre l'exemple - scénario de test nous devons voir comment le faire fonctionner.

P.S .: Je mettrai à jour la question une fois que nous aurons fini de travailler sur la fonctionnalité.

9
Oliver Drotbohm

Vous pouvez étendre à partir de SimpleJpaRepository et créer votre propre référentiel personnalisé dans lequel vous pouvez définir la fonctionnalité de suppression logicielle de manière générique.

Vous devrez également créer un JpaRepositoryFactoryBean personnalisé et l'activer dans votre classe principale.

Vous pouvez vérifier mon code ici https://github.com/dzinot/spring-boot-jpa-soft-delete

2
Dzinot

Je vous suggère d'utiliser une vue de base de données (ou son équivalent dans Oracle) si vous ne souhaitez pas importer des annotations spécifiques à hibernate. Dans MySQL 5.5, ces vues peuvent être mises à jour et insérées si les critères de filtre sont aussi simples que actif = 1

créer ou remplacer view active_stuff en tant que select * from Stuff où active = 1;

Que cela soit une bonne idée dépend probablement de votre base de données, mais cela fonctionne très bien dans mon implémentation.

La suppression a nécessité une entité supplémentaire qui accédait directement à 'Stuff', mais il en serait de même de @Where.

1
Chanoch

J'ai défini un référentiel comme celui-ci

@NoRepositoryBean
public interface SoftDeleteRepository<T, ID extends Serializable> extends JpaRepository<T, ID>,
    JpaSpecificationExecutor<T> {

    enum StateTag {
        ENABLED(0), DISABLED(1), DELETED(2);

        private final int tag;

        StateTag(int tag) {
            this.tag = tag;
        }

        public int getTag() {
            return tag;
        }
    }

    T changeState(ID id, StateTag state);

    List<T> changeState(Iterable<ID> ids, StateTag state);

    <S extends T> List<S> changeState(Example<S> example, StateTag state);

    List<T> findByState(@Nullable Iterable<StateTag> states);

    List<T> findByState(Sort sort, @Nullable Iterable<StateTag> states);

    Page<T> findByState(Pageable pageable, @Nullable Iterable<StateTag> states);

    <S extends T> List<S> findByState(Example<S> example, @Nullable Iterable<StateTag> states);

    <S extends T> List<S> findByState(Sort sort, Example<S> example, @Nullable Iterable<StateTag> states);

    <S extends T> Page<S> findByState(Pageable pageable, Example<S> example,
                                  @Nullable Iterable<StateTag> states);

    long countByState(@Nullable Iterable<StateTag> states);

    default String getSoftDeleteColumn() {
        return "disabled";
    }
}
0
mu.xufan

J'ai utilisé la solution de @vadim_shb pour étendre JpaRepository et voici mon code dans Scala. Upvote sa réponse, pas celle-ci. Je voulais juste montrer un exemple qui inclut la pagination et le tri. 

La pagination et le tri fonctionnent parfaitement avec les annotations de requête. Je n'ai pas tout testé, mais pour ceux qui posent des questions sur la pagination et le tri, ils semblent se superposer à l'annotation Query. Je mettrai cela à jour si je résous les problèmes. 

import Java.util
import Java.util.List

import scala.collection.JavaConverters._
import com.xactly.alignstar.data.model.BaseEntity
import org.springframework.data.domain.{Page, Pageable, Sort}
import org.springframework.data.jpa.repository.{JpaRepository, Modifying, Query}
import org.springframework.data.repository.NoRepositoryBean
import org.springframework.transaction.annotation.Transactional

@NoRepositoryBean
trait BaseRepository[T <: BaseEntity, ID <: Java.lang.Long] extends JpaRepository[T, ID] {

  /* additions */
  @Query("select e from #{#entityName} e where e.isDeleted = true")
  @Transactional(readOnly = true)
  def findInactive: Nothing

  @Transactional
  def delete(entity: T): Unit = delete(entity.getId.asInstanceOf[ID])

  /* overrides */
  @Query("select e from #{#entityName} e where e.isDeleted = false")
  override def findAll(sort: Sort):  Java.util.List[T]

  @Query("select e from #{#entityName} e where e.isDeleted = false")
  override def findAll(pageable: Pageable): Page[T]

  @Transactional(readOnly = true)
  @Query("select e from #{#entityName} e where e.isDeleted = false")
  override def findAll: util.List[T]

  @Transactional(readOnly = true)
  @Query("select e from #{#entityName} e where e.id in :ids and e.isDeleted = false")
  override def findAll(ids: Java.lang.Iterable[ID]): Java.util.List[T]

  @Transactional(readOnly = true)
  @Query("select e from #{#entityName} e where e.id = :id and e.isDeleted = false")
  override def findOne(id: ID): T

  @Transactional(readOnly = true)
  @Query("select count(e) from #{#entityName} e where e.isDeleted = false")
  override def count: Long

  @Transactional(readOnly = true)
  override def exists(id: ID): Boolean = findOne(id) != null

  @Query("update #{#entityName} e set e.isDeleted=true where e.id = :id")
  @Transactional
  @Modifying
  override def delete(id: ID): Unit

  @Transactional
  override def delete(entities: Java.lang.Iterable[_ <: T]): Unit = {
    entities.asScala.map((entity) => delete(entity))
  }

  @Transactional
  @Modifying
  override def deleteInBatch(entities: Java.lang.Iterable[T]): Unit = delete(entities)

  override def deleteAllInBatch(): Unit = throw new NotImplementedError("This is not implemented in BaseRepository")

  @Query("update #{#entityName} e set e.isDeleted=true")
  @Transactional
  @Modifying
  def deleteAll(): Unit
}
0
JMDenver