Spring Data Repositories

Disclaimer:
Postul se adresează programatorilor Java care folosesc JPA și urmăresc minimizarea duplicării codului scris pentru a interacționa cu entitățile JPA.
Informațiile ce urmează sunt prezentate ca idei și nu trebuiesc percepute drept unica variantă de implementare.

Acces la date

Accesul la date persistente este uzual modelat cu ajutorul celor patru funcții CRUD: Create, Read, Update, Delete.
Pentru a simplifica integrarea unei implementări JPA intr-o aplicație Spring putem folosi Spring Data JPA.

Contractul unui repository ar putea avea următoarea structură:

public interface CrudRepository<T, ID> {
    <S extends T> S save(S entity);
    T findOne(ID id);
    boolean exists(ID id);
    Iterable<T> findAll();
    void delete(ID id);
    void delete(T entity);
}

Pentru a decupla implementarea repository-ului de nivelele superioare ale aplicației, vom interacționa cu repository-ul prin intermediul acestei interfețe.
Pe lângă entitatea propriu zisă, Spring Data JPA are nevoie de acest contract pentru a furniza o implementare a repository-ului.
Să vedem și un exemplu:
Entitatea User conține atributele: nume utilizator, parola, data creare, data modificare și un grup.

Structura entitătii JPA (getteri/setteri omiși):

@Entity
@Table(name = "users")
public class User {
    @Id
    private String username;
    private String password;
    @Temporal(TemporalType.TIMESTAMP)
    private Date createdAt;
    @Temporal(TemporalType.TIMESTAMP)
    private Date updatedAt;
}

Definiția repository-ului pentru această entitate ar putea fi:

public interface UserRepository extends CrudRepository<User, String> {
}

Și cam atât. În cadrul procesului de inițializare, atât timp cât avem configurările corecte, Spring Data va instanția un spring bean cu o implementare pentru interfața UserRepository ce devine disponibilă în containerul DI.

Configurare

Pentru a include Spring Data JPA într-un proiect ce utilizează maven, adăugăm următoarea dependința:

<dependency>
  <groupId>org.springframework.data</groupId>
  <artifactId>spring-data-jpa</artifactId>
  <version>${spring-data-jpa.version}</version>
</dependency>

Versiunea curentă Spring Data JPA este 1.2.0.RELEASE
Începând cu Spring 3.0 putem folosi clase adnotate @Configuration pentru a construi spring beans. Spring bean-urile Spring Data (începând cu 1.2.0) pot fi configurate atât în fișiere de configurare XML cât și clase de configurare:

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(basePackages = "org.jug.iasi.springdata.jpa.repositories")
@PropertySource("classpath:database.properties")
@ComponentScan(basePackages = "org.jug.iasi.springdata.jpa.services.")
public class SpringDataConfiguration {
 
    @Autowired
    private Environment environment;
 
    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory() throws PropertyVetoException {
        LocalContainerEntityManagerFactoryBean factoryBean = new LocalContainerEntityManagerFactoryBean();
        factoryBean.setDataSource(dataSource());
        factoryBean.setPersistenceUnitName("jug-demo");
        factoryBean.setJpaDialect(new HibernateJpaDialect());
        factoryBean.setJpaVendorAdapter(jpaVendorAdapter());
        return factoryBean;
    }
 
    private JpaVendorAdapter jpaVendorAdapter() {
        HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
        vendorAdapter.setGenerateDdl(true);
        return vendorAdapter;
    }
 
    @Bean
    public HibernateExceptionTranslator hibernateExceptionTranslator() {
        return new HibernateExceptionTranslator();
    }
 
    @Bean
    public PlatformTransactionManager transactionManager() throws PropertyVetoException {
        return new JpaTransactionManager(entityManagerFactory().getObject());
    }
 
    @Bean
    public DataSource dataSource() throws PropertyVetoException {
        final ComboPooledDataSource dataSource = new ComboPooledDataSource();
        dataSource.setDriverClass(environment.getProperty("database.driverClassName"));
        dataSource.setJdbcUrl(environment.getProperty("database.url"));
        dataSource.setUser(environment.getProperty("database.username"));
        dataSource.setPassword(environment.getProperty("database.password"));
        dataSource.setInitialPoolSize(5);
        dataSource.setMaxPoolSize(20);
        return dataSource;
    }
 
}

iar într-un serviciu putem injecta dependința către UserRepository:

@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;
 
    @Transactional
    public void someBusinessMethod(String username) {
        User user = userRepository.findOne(username);
        ...
    }
 
}

Query-ing

Unul din beneficiile folosirii Spring Data JPA este dat de capabilitățile de a construi interogări.
Cea mai simplă variantă de interogare este construită în funcție de numele metodei. Spre exemplu: pentru a extrage o înregistrare User folosindu-ne de grupul din care acesta face parte putem adăuga în UserRepository următoarea metodă:

public interface UserRepository extends CrudRepository<User, Integer> {
    List<User> findByGroup(String group);
 
    List<User> findByGroup(String group, Sort sort);
 
    @Query("SELECT u FROM User u WHERE u.group=?1")
    Page<User> findByGroup(String group, Pageable pageRequest);
}

Prima metodă întoarce toți utilizatorii existenți pentru grupul dat ca parametru. A doua metodă oferă posibilitatea ordonării rezultatelor, iar a treia metodă oferă o variantă convenabilă pentru ordonarea și paginarea rezultatelor (Pageable conține și Sort).
Daca metoda nu este adnotată specific (primele două metode spre exemplu, Spring Data va deduce interogarea și filtrul din numele metodei. Capabilitățile acestui sistem de detecție sunt limitate și e recomandat a fi folosite pentru filtrari simple.
În cazul in care interogarea este una complexa, putem folosi o interogare specifică transmisă prin adnotarea @Query. Această interogare va fi evaluată în momentul instanțierii repository-ului, în caz că este invalidă va rezulta în oprirea containerului Spring.

QueryDSL

O variantă fluentă de a construi interogări este QueryDSL. Pentru aceasta Spring Data ofera interfata QueryDslPredicateExecutor

Cum funcționează QueryDSL? Folosind APT (Annotation Processing Tool – disponibil încă din Java 1.5), genereaza o clasă pentru fiecare entitate JPA existentă (urmărind adnotarea @Entity). În clasa generată vor fi adăugate tipuri specifice pentru fiecare atribut al entității sursă. Aceste tipuri oferă posibilitatea compunerii într-o maniera fluentă a filtrelor ce returnează în final un predicat. Interfata QueryDslPredicateExecutor acceptă spre execuție un astfel de predicat.

Pentru a introduce QueryDSL într-un proiect ce folosește maven, pe lângă dependințe trebuie să listăm și plugin-ul care genereaza aceste clase:

<plugin>
    <groupId>com.mysema.maven</groupId>
    <artifactId>maven-apt-plugin</artifactId>
    <version>1.0.4</version>
    <dependencies>
        <dependency>
            <groupId>com.mysema.querydsl</groupId>
            <artifactId>querydsl-apt</artifactId>
            <version>${querydsl.version}</version>
        </dependency>
    </dependencies>
    <executions>
        <execution>
            <phase>generate-sources</phase>
            <goals>
                <goal>process</goal>
            </goals>
            <configuration>
                <outputDirectory>target/generated-sources</outputDirectory>
                <processor>com.mysema.query.apt.jpa.JPAAnnotationProcessor</processor>
            </configuration>
        </execution>
    </executions>
</plugin>

rulând plugin-ul vom obține o clasă de forma:

@Generated("com.mysema.query.codegen.EntitySerializer")
public class QUser extends EntityPathBase<User> {
 
    private static final long serialVersionUID = -300523121;
 
    public static final QUser user = new QUser("user");
 
    public final DateTimePath<Date> createdAt = createDateTime("createdAt", Date.class);
 
    public final StringPath username = createString("username");
 
    public final StringPath group = createString("group");}

ce o vom putea folosi pentru a declara predicate și ulterior a le executa folosind repository-ul pus la dispoziție de Spring Data:

public class UserRepositoryQueryDSLTest {
    public static final QUser $ = QUser.user;
    @Autowired
    private UserRepository userRepository;
 
    @Test
    public void testQueryDSL() throws Exception {
        Date monthStart = DateTime.now().withDayOfMonth(1).toDate();
        BooleanExpression createdThisMonth = $.createdAt.after(monthStart);
        BooleanExpression updated = $.updatedAt.isNotNull();
 
        List<User> userList = userRepository.findAll(allOf(updated, createdThisMonth));
        Assert.assertNotNull(userList);
 
        // OR
 
        List <User> users = userRepository.findAll(
                $.createdAt.after(monthStart)
                        .and($.updatedAt.isNotNull()));
        Assert.assertNotNull(users);
    }

Pe lângă JPA, Spring Data oferă acces spre alte tehnologii: MongoDB, Neo4J, Hadoop, Solr, Riak, Cassandra și nu numai.
Aceste module sunt disponibile opensource pe SpringSource GitHub repository

Share Button

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.