Java >> Java tutoriál >  >> Tag >> Spring

Nejlepší Spring Data JpaRepository

Úvod

V tomto článku vám ukážu nejlepší způsob, jak používat Spring Data JpaRepository, které se nejčastěji používá nesprávným způsobem.

Největší problém s výchozími daty Spring JpaRepository je skutečnost, že rozšiřuje obecný CrudRepository , která ve skutečnosti není kompatibilní se specifikací JPA.

Paradox metody ukládání JpaRepository

Nic takového jako save neexistuje metoda v JPA, protože JPA implementuje paradigma ORM, nikoli vzor Active Record.

JPA je v podstatě stroj stavu entity, jak ukazuje následující diagram:

Jak jasně vidíte, žádné save neexistuje metoda v JPA.

Nyní byl Hibernate vytvořen před JPA, takže kromě implementace specifikace JPA poskytuje také své vlastní specifické metody, jako je update jeden.

I když existují dvě metody zvané save a saveOrUpdate v režimu spánku Session , jak jsem vysvětlil v tomto článku, jsou pouze aliasem pro update .

Ve skutečnosti, počínaje Hibernate 6, save a saveOrUpdate metody jsou nyní zastaralé a budou odstraněny v budoucí verzi, protože jde pouze o chybu, která byla unesena z Hibernate 1.

Pokud vytvoříte novou entitu, musíte zavolat persist takže se entita stane spravovanou a flush vygeneruje INSERT prohlášení.

Pokud se entita oddělí a vy jste ji změnili, musíte změny přenést zpět do databáze, v takovém případě můžete použít buď merge nebo update . První metoda, merge , zkopíruje stav oddělené entity do nové entity, která byla načtena aktuálním kontextem perzistence, a nechá flush zjistit, zda UPDATE je dokonce nutné. Druhá metoda, update , vynutí flush pro spuštění UPDATE s aktuálním stavem entity.

remove metoda naplánuje odstranění a flush spustí DELETE prohlášení.

Ale JpaRepository zdědí save metoda z CrudRepository , stejně jako MongoRepository nebo SimpleJdbcRepository .

Nicméně MongoRepository a SimpleJdbcRepository zaujmout přístup aktivního záznamu, zatímco JPA ne.

Ve skutečnosti save metoda JpaRepository je implementován takto:

@Transactional
public <S extends T> S save(S entity) {
    if (this.entityInformation.isNew(entity)) {
        this.em.persist(entity);
        return entity;
    } else {
        return this.em.merge(entity);
    }
}

V zákulisí není žádná magie. Je to buď volání na persist nebo merge ve skutečnosti.

Anti-pattern metody ukládání

Protože JpaRepository obsahuje save Naprostá většina vývojářů softwaru s ní tak zachází a nakonec narazíte na následující anti-vzor:

@Transactional
public void saveAntiPattern(Long postId, String postTitle) {
        
    Post post = postRepository.findById(postId).orElseThrow();

    post.setTitle(postTitle);

    postRepository.save(post);
}

Jak moc je to známé? Kolikrát jste viděli tento „vzor“ používat?

Problém je v save linka, která je sice zbytečná, ale není bezplatná. Volání merge na spravované entitě vypálí cykly CPU spuštěním MergeEvent , který může být kaskádován dále v hierarchii entit, aby skončil v bloku kódu, který dělá toto:

protected void entityIsPersistent(MergeEvent event, Map copyCache) {
    LOG.trace( "Ignoring persistent instance" );

    final Object entity = event.getEntity();
    final EventSource source = event.getSession();
    
    final EntityPersister persister = source.getEntityPersister( 
        event.getEntityName(), 
        entity 
    );

    //before cascade!
    ( (MergeContext) copyCache ).put( entity, entity, true );  
    
    cascadeOnMerge( source, persister, entity, copyCache );
    copyValues( persister, entity, entity, source, copyCache );

    event.setResult( entity );
}

Nejen, že merge hovor nepřináší nic užitečného, ​​ale ve skutečnosti zvyšuje režii navíc k vaší době odezvy a dělá poskytovatele cloudu s každým takovým hovorem bohatším.

A to není vše. Jak jsem vysvětlil v tomto článku, obecný save metoda není vždy schopna určit, zda je entita nová. Pokud má například entita přiřazený identifikátor, Spring Data JPA zavolá merge místo persist , čímž se spustí zbytečné SELECT dotaz. Pokud se to stane v kontextu úlohy dávkového zpracování, pak je to ještě horší, můžete vygenerovat spoustu zbytečných SELECT dotazy.

Takže to nedělejte! Můžete to udělat mnohem lépe.

Nejlepší alternativa Spring Data JpaRepository

Pokud save metoda existuje, lidé ji zneužijí. To je důvod, proč je nejlepší ji vůbec nemít a poskytnout vývojáři lepší alternativy vhodné pro JPA.

Následující řešení používá vlastní idiom Spring Data JPA Repository.

Začneme tedy vlastním HibernateRepository rozhraní, které definuje nový kontrakt pro šíření změn stavu entity:

public interface HibernateRepository<T> {

    //Save methods will trigger an UnsupportedOperationException
    
    @Deprecated
    <S extends T> S save(S entity);

    @Deprecated
    <S extends T> List<S> saveAll(Iterable<S> entities);

    @Deprecated
    <S extends T> S saveAndFlush(S entity);

    @Deprecated
    <S extends T> List<S> saveAllAndFlush(Iterable<S> entities);

    //Persist methods are meant to save newly created entities

    <S extends T> S persist(S entity);

    <S extends T> S persistAndFlush(S entity);

    <S extends T> List<S> persistAll(Iterable<S> entities);

    <S extends T> List<S> peristAllAndFlush(Iterable<S> entities);

    //Merge methods are meant to propagate detached entity state changes
    //if they are really needed
    
    <S extends T> S merge(S entity);

    <S extends T> S mergeAndFlush(S entity);

    <S extends T> List<S> mergeAll(Iterable<S> entities);

    <S extends T> List<S> mergeAllAndFlush(Iterable<S> entities);

    //Update methods are meant to force the detached entity state changes

    <S extends T> S update(S entity);

    <S extends T> S updateAndFlush(S entity);

    <S extends T> List<S> updateAll(Iterable<S> entities);

    <S extends T> List<S> updateAllAndFlush(Iterable<S> entities);

}

Metody v HibernateRepository rozhraní jsou implementovány pomocí HibernateRepositoryImpl třídy takto:

public class HibernateRepositoryImpl<T> implements HibernateRepository<T> {

    @PersistenceContext
    private EntityManager entityManager;

    public <S extends T> S save(S entity) {
        return unsupported();
    }

    public <S extends T> List<S> saveAll(Iterable<S> entities) {
        return unsupported();
    }

    public <S extends T> S saveAndFlush(S entity) {
        return unsupported();
    }

    public <S extends T> List<S> saveAllAndFlush(Iterable<S> entities) {
        return unsupported();
    }

    public <S extends T> S persist(S entity) {
        entityManager.persist(entity);
        return entity;
    }

    public <S extends T> S persistAndFlush(S entity) {
        persist(entity);
        entityManager.flush();
        return entity;
    }

    public <S extends T> List<S> persistAll(Iterable<S> entities) {
        List<S> result = new ArrayList<>();
        for(S entity : entities) {
            result.add(persist(entity));
        }
        return result;
    }

    public <S extends T> List<S> peristAllAndFlush(Iterable<S> entities) {
        return executeBatch(() -> {
            List<S> result = new ArrayList<>();
            for(S entity : entities) {
                result.add(persist(entity));
            }
            entityManager.flush();
            return result;
        });
    }

    public <S extends T> S merge(S entity) {
        return entityManager.merge(entity);
    }

    public <S extends T> S mergeAndFlush(S entity) {
        S result = merge(entity);
        entityManager.flush();
        return result;
    }

    public <S extends T> List<S> mergeAll(Iterable<S> entities) {
        List<S> result = new ArrayList<>();
        for(S entity : entities) {
            result.add(merge(entity));
        }
        return result;
    }

    public <S extends T> List<S> mergeAllAndFlush(Iterable<S> entities) {
        return executeBatch(() -> {
            List<S> result = new ArrayList<>();
            for(S entity : entities) {
                result.add(merge(entity));
            }
            entityManager.flush();
            return result;
        });
    }

    public <S extends T> S update(S entity) {
        session().update(entity);
        return entity;
    }

    public <S extends T> S updateAndFlush(S entity) {
        update(entity);
        entityManager.flush();
        return entity;
    }

    public <S extends T> List<S> updateAll(Iterable<S> entities) {
        List<S> result = new ArrayList<>();
        for(S entity : entities) {
            result.add(update(entity));
        }
        return result;
    }

    public <S extends T> List<S> updateAllAndFlush(Iterable<S> entities) {
        return executeBatch(() -> {
            List<S> result = new ArrayList<>();
            for(S entity : entities) {
                result.add(update(entity));
            }
            entityManager.flush();
            return result;
        });
    }

    protected Integer getBatchSize(Session session) {
        SessionFactoryImplementor sessionFactory = session
            .getSessionFactory()
            .unwrap(SessionFactoryImplementor.class);
            
        final JdbcServices jdbcServices = sessionFactory
            .getServiceRegistry()
            .getService(JdbcServices.class);
            
        if(!jdbcServices.getExtractedMetaDataSupport().supportsBatchUpdates()) {
            return Integer.MIN_VALUE;
        }
        return session
            .unwrap(AbstractSharedSessionContract.class)
            .getConfiguredJdbcBatchSize();
    }

    protected <R> R executeBatch(Supplier<R> callback) {
        Session session = session();
        Integer jdbcBatchSize = getBatchSize(session);
        Integer originalSessionBatchSize = session.getJdbcBatchSize();
        try {
            if (jdbcBatchSize == null) {
                session.setJdbcBatchSize(10);
            }
            return callback.get();
        } finally {
            session.setJdbcBatchSize(originalSessionBatchSize);
        }
    }

    protected Session session() {
        return entityManager.unwrap(Session.class);
    }

    protected <S extends T> S unsupported() {
        throw new UnsupportedOperationException(
            "There's no such thing as a save method in JPA, so don't use this hack!"
        );
    }
}

Nejprve všechny save metody spouštějí UnsupportedOperationException , což vás nutí vyhodnotit, který přechod stavu entity máte místo toho zavolat.

Na rozdíl od figuríny saveAllAndFlush , persistAllAndFlush , mergeAllAndFlush a updateAllAndFlush můžete těžit z mechanismu automatického dávkování, i když jste jej dříve zapomněli nakonfigurovat, jak je vysvětleno v tomto článku.

Doba testování

Chcete-li použít HibernateRepository , vše, co musíte udělat, je rozšířit ji vedle standardního JpaRepository , takto:

@Repository
public interface PostRepository 
    extends JpaRepository<Post, Long>, HibernateRepository<Post> {

}

To je ono!

Tentokrát neexistuje způsob, jak byste mohli narazit na nechvalně známý save volat anti-vzor:

try {
    transactionTemplate.execute(
            (TransactionCallback<Void>) transactionStatus -> {
        postRepository.save(
            new Post()
                .setId(1L)
                .setTitle("High-Performance Java Persistence")
                .setSlug("high-performance-java-persistence")
        );
        
        return null;
    });

    fail("Should throw UnsupportedOperationException!");
} catch (UnsupportedOperationException expected) {
    LOGGER.warn("You shouldn't call the JpaRepository save method!");
}

Místo toho můžete použít persist , merge nebo update metoda. Takže, pokud chci zachovat nějaké nové entity, mohu to udělat takto:

postRepository.persist(
    new Post()
        .setId(1L)
        .setTitle("High-Performance Java Persistence")
        .setSlug("high-performance-java-persistence")
);

postRepository.persistAndFlush(
    new Post()
        .setId(2L)
        .setTitle("Hypersistence Optimizer")
        .setSlug("hypersistence-optimizer")
);

postRepository.peristAllAndFlush(
    LongStream.range(3, 1000)
        .mapToObj(i -> new Post()
            .setId(i)
            .setTitle(String.format("Post %d", i))
            .setSlug(String.format("post-%d", i))
        )
        .collect(Collectors.toList())
);

A přenesení změn z některých oddělených entit zpět do databáze se provádí následovně:

List<Post> posts = transactionTemplate.execute(transactionStatus ->
    entityManager.createQuery("""
        select p
        from Post p
        where p.id < 10
        """, Post.class)
    .getResultList()
);

posts.forEach(post -> 
    post.setTitle(post.getTitle() + " rocks!")
);

transactionTemplate.execute(transactionStatus ->
    postRepository.updateAll(posts)
);

A na rozdíl od merge , update nám umožňuje vyhnout se zbytečnému SELECT a existuje pouze jeden UPDATE probíhá:

Query:["
update 
  post 
set 
  slug=?, 
  title=? 
where 
  id=?"
], 
Params:[
  (high-performance-java-persistence, High-Performance Java Persistence rocks!, 1), 
  (hypersistence-optimizer, Hypersistence Optimizer rocks!, 2), 
  (post-3, Post 3 rocks!, 3), 
  (post-4, Post 4 rocks!, 4), 
  (post-5, Post 5 rocks!, 5), 
  (post-6, Post 6 rocks!, 6), 
  (post-7, Post 7 rocks!, 7), 
  (post-8, Post 8 rocks!, 8), 
  (post-9, Post 9 rocks!, 9)
]

Skvělé, že?

Závislost na Maven

HibernateRepository je k dispozici na Maven Central, takže první věc, kterou musíme udělat, je přidat závislost Hibernate Types. Pokud například používáte Maven, musíte do svého projektu přidat následující závislost pom.xml konfigurační soubor:

Pro Hibernate 6:

<dependency>
    <groupId>com.vladmihalcea</groupId>
    <artifactId>hibernate-types-60</artifactId>
    <version>${hibernate-types.version}</version>
</dependency>

Pro Hibernate 5.5 a 5.4:

<dependency>
    <groupId>com.vladmihalcea</groupId>
    <artifactId>hibernate-types-55</artifactId>
    <version>${hibernate-types.version}</version>
</dependency>

A pro Hibernate 5.3 a 5.2:

<dependency>
    <groupId>com.vladmihalcea</groupId>
    <artifactId>hibernate-types-52</artifactId>
    <version>${hibernate-types.version}</version>
</dependency>

Poté musíte zahrnout com.vladmihalcea.spring.repository v @EnableJpaRepositories konfigurace, jako toto:

@Configuration
@EnableJpaRepositories(
    basePackages = {
        "com.vladmihalcea.spring.repository",
        ...
    }
)
public class JpaConfiguration {
    ...
}

A je to!

Vaše jarní datová úložiště nyní mohou rozšířit úžasné HibernateRepository utility, což je mnohem lepší alternativa k výchozímu Spring Data JpaRepository .

Závěr

JPA nemá nic takového jako save metoda. Je to jen hack, který musel být implementován v JpaRepository protože metoda je zděděna z CrudRepository , což je základní rozhraní sdílené téměř projekty Spring Data.

Pomocí HibernateRepository , nejen že můžete lépe zdůvodnit, kterou metodu musíte volat, ale můžete také těžit z update metoda, která poskytuje lepší výkon pro úlohy dávkového zpracování.


Java Tag