Spring Data JPA - kontynuacja aplikacji lifetimer

Kontynuujmy aplikacje z poprzedniego wpisu licznik życia.

Dodamy do niego połączenie z bazą danych. Wykorzystamy do tego bazę h2, HikariDataSource, Hibernate i tytułowy JPA Spring Data. Pokażę i wytłumaczę jak to działa w praktyce z uwzględnieniem teorii na ten temat. Proponuję zajrzeć do mojego poprzedniego wpisu w którym przeczytacie jak działają warstwy połączenia z bazą danych aplikacji Java.

W celu prezentacji stworzę osobny projekt, który będzie zaczątkiem do aplikacji samodzielnej. Dołączę do niego poprzedni projekt, który będzie służył za logikę aplikacji. Efekt ten zyskuje dodając do pom'a dane o projekcie. Grupa oraz wersja jest identyczna jak w nowym projekcie. Różnią się jedynie nazwa projektu (artifactId).

Wykonuję to poniższym fragmentem pliku:

<dependency> <groupId>${project.groupId}</groupId> <artifactId>lifetimer</artifactId> <version>${project.version}</version> </dependency>

Zajmijmy się teraz samym połączeniem do bazy danych. Potrzebujemy do tego sterownika bazy danych. Znajduję sobie najnowszą wersję sterownika do bazy h2 i dodaje go do pom'a.

<dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <version>1.4.192</version> </dependency>

Moglibyśmy już zacząć się bawić obsługą bazy danych, ale dodamy kilka warstw, które pozwolą nam na łatwą i obiektową obsługę repozytorium. Wykorzystamy do tego wspomniane na początku biblioteki HikariDataSource, Hibernate i JPA Spring Data.

<!-- DataSource -->
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>2.5.1</version>
</dependency>
<!-- JPA Provider (Hibernate) -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
<version>5.1.0.Final</version>
</dependency>
<!-- Spring Data JPA -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
<version>1.10.4.RELEASE</version>
</dependency>
view raw pom.xml hosted with ❤ by GitHub

Dobrze! Mamy wszystkie biblioteki, które potrzebujemy do samego połączenia z bazą.

Trzeba teraz je odpowiednio skonfigurować. Wykorzystując spring context mamy dwie opcje skonfigurowania aplikacji. Tworząc konfigurację poprzez adnotacje lub plik .xml. Przedstawię wam obydwa rozwiązania:

Adnotacje:

package pl.devpragmatic.livetimerdesktop.config;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import java.util.Properties;
import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.env.Environment;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.annotation.EnableTransactionManagement;
/**
*
* @author devpragmatic
*/
@Configuration
@EnableTransactionManagement
@ComponentScan(basePackages = "pl.devpragmatic.lifetimer.service")
@EnableJpaRepositories(basePackages = {
"pl.devpragmatic.lifetimer.repository"
})
@PropertySource("classpath:application.properties")
public class PersistenceContext {
@Bean
JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
JpaTransactionManager transactionManager = new JpaTransactionManager();
transactionManager.setEntityManagerFactory(entityManagerFactory);
return transactionManager;
}
@Bean(destroyMethod = "close")
DataSource dataSource(Environment env) {
HikariConfig dataSourceConfig = new HikariConfig();
dataSourceConfig.setDriverClassName(env.getRequiredProperty("db.driver"));
dataSourceConfig.setJdbcUrl(env.getRequiredProperty("db.url"));
dataSourceConfig.setUsername(env.getRequiredProperty("db.username"));
dataSourceConfig.setPassword(env.getRequiredProperty("db.password"));
return new HikariDataSource(dataSourceConfig);
}
@Bean
LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource,
Environment env) {
LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean();
entityManagerFactoryBean.setDataSource(dataSource);
entityManagerFactoryBean.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
entityManagerFactoryBean.setPackagesToScan("pl.devpragmatic.lifetimer.domain");
Properties jpaProperties = new Properties();
jpaProperties.put("hibernate.dialect", env.getRequiredProperty("hibernate.dialect"));
jpaProperties.put("hibernate.hbm2ddl.auto",
env.getRequiredProperty("hibernate.hbm2ddl.auto")
);
jpaProperties.put("hibernate.ejb.naming_strategy",
env.getRequiredProperty("hibernate.ejb.naming_strategy")
);
jpaProperties.put("hibernate.show_sql",
env.getRequiredProperty("hibernate.show_sql")
);
jpaProperties.put("hibernate.format_sql",
env.getRequiredProperty("hibernate.format_sql")
);
entityManagerFactoryBean.setJpaProperties(jpaProperties);
return entityManagerFactoryBean;
}
}

Plik xml:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:jpa="http://www.springframework.org/schema/data/jpa"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
">
<context:component-scan base-package="pl.devpragmatic.lifetimer.service"/>
<jpa:repositories base-package="pl.devpragmatic.lifetimer.repository" />
<bean id="propertyPlaceholder" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="systemPropertiesModeName" value="SYSTEM_PROPERTIES_MODE_OVERRIDE"/>
<property name="ignoreResourceNotFound" value="true"/>
<property name="locations">
<list>
<value>classpath:application.properties</value>
</list>
</property>
</bean>
<bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource" destroy-method="close">
<property name="driverClassName" value="${db.driver}" />
<property name="jdbcUrl" value="${db.url}" />
<property name="username" value="${db.username}" />
<property name="password" value="${db.password}" />
</bean>
<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="packagesToScan" value="pl.devpragmatic.lifetimer.domain" />
<property name="jpaVendorAdapter">
<bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"/>
</property>
<property name="jpaProperties">
<props>
<prop key="hibernate.dialect">${hibernate.dialect}</prop>
<prop key="hibernate.hbm2ddl.auto">${hibernate.hbm2ddl.auto}</prop>
<prop key="hibernate.ejb.naming_strategy">${hibernate.ejb.naming_strategy}</prop>
<prop key="hibernate.show_sql">${hibernate.show_sql}</prop>
<prop key="hibernate.format_sql">${hibernate.format_sql}</prop>
</props>
</property>
</bean>
<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="entityManagerFactory"/>
</bean>
<tx:annotation-driven transaction-manager="transactionManager"/>
</beans>

Wytłumaczę teraz za co odpowiadają poszczególne części konfiguracji.

<context:component-scan base-package="pl.devpragmatic.lifetimer.service"/>

lub

@ComponentScan(basePackages = "pl.devpragmatic.lifetimer.service")

Powyższy kod to są polecenia dla springa, które przekazują mu informacje, żeby automatycznie wyszukiwał komponenty pod ścieżką "base-package" (czyli u nas: "pl.devpragmatic.lifetimer.service").

<jpa:repositories base-package="pl.devpragmatic.lifetimer.repository" />

lub

@EnableJpaRepositories(basePackages = {"pl.devpragmatic.lifetimer.repository"})

Przekazuję infomacje do aplikacji, że repozytorium JPA ma szukać pod ścieżką "base-package" (czyli u nas: "pl.devpragmatic.lifetimer.repository").

<bean id="propertyPlaceholder" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"> <property name="systemPropertiesModeName" value="SYSTEM_PROPERTIES_MODE_OVERRIDE"/> <property name="ignoreResourceNotFound" value="true"/> <property name="locations"> <list> <value>classpath:application.properties</value> </list> </property> </bean>

lub

@PropertySource("classpath:application.properties")

Powyższa konfiguracja pozwala na używanie zmiennych środowiskowych, które są umieszczone w pliku application.properties. Celowo wyciągnąłem zmienne takie jak konfiguracja bazy danych i hibernate. Przydaje się to w celu szybkiej zmiany na działającym już środowisku bez kompilowania kodu (jeżeli używamy adnotacji) lub wyszukiwania i podmieniania tych wartości (jeżeli używamy plików .xml).

<bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource" destroy-method="close"> <property name="driverClassName" value="${db.driver}" /> <property name="jdbcUrl" value="${db.url}" /> <property name="username" value="${db.username}" /> <property name="password" value="${db.password}" /> </bean>

lub

@Bean(destroyMethod = "close") DataSource dataSource(Environment env) { HikariConfig dataSourceConfig = new HikariConfig(); dataSourceConfig.setDriverClassName(env.getRequiredProperty("db.driver")); dataSourceConfig.setJdbcUrl(env.getRequiredProperty("db.url")); dataSourceConfig.setUsername(env.getRequiredProperty("db.username")); dataSourceConfig.setPassword(env.getRequiredProperty("db.password")); return new HikariDataSource(dataSourceConfig); }

Tworzone jest tutaj źródło danych. Przekazujemy do niego parametry takie jak sterownik do bazy danych, adres bazy, użytkownika i hasło do niego.

@Bean LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource, Environment env) { LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean(); entityManagerFactoryBean.setDataSource(dataSource); entityManagerFactoryBean.setJpaVendorAdapter(new HibernateJpaVendorAdapter()); entityManagerFactoryBean.setPackagesToScan("pl.devpragmatic.lifetimer.domain"); Properties jpaProperties = new Properties(); jpaProperties.put("hibernate.dialect", env.getRequiredProperty("hibernate.dialect")); jpaProperties.put("hibernate.hbm2ddl.auto", env.getRequiredProperty("hibernate.hbm2ddl.auto") ); jpaProperties.put("hibernate.ejb.naming_strategy", env.getRequiredProperty("hibernate.ejb.naming_strategy") ); jpaProperties.put("hibernate.show_sql", env.getRequiredProperty("hibernate.show_sql") ); jpaProperties.put("hibernate.format_sql", env.getRequiredProperty("hibernate.format_sql") ); entityManagerFactoryBean.setJpaProperties(jpaProperties); return entityManagerFactoryBean; }

lub

<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="packagesToScan" value="pl.devpragmatic.lifetimer.domain" /> <property name="jpaVendorAdapter"> <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"/> </property> <property name="jpaProperties"> <props> <prop key="hibernate.dialect">${hibernate.dialect}</prop> <prop key="hibernate.hbm2ddl.auto">${hibernate.hbm2ddl.auto}</prop> <prop key="hibernate.ejb.naming_strategy">${hibernate.ejb.naming_strategy}</prop> <prop key="hibernate.show_sql">${hibernate.show_sql}</prop> <prop key="hibernate.format_sql">${hibernate.format_sql}</prop> </props> </property> </bean>

Tutaj posiadamy już konfiguracje JPA. Musimy podać mu źródło danych, ścieżke do encji(gdy używamy adnotacji do deklaracji encji), dostawce (u nas Hibernate) oraz parametry dla dostawcy.

Opis parametrów tutaj umieszczonych:

  • hibernate.dialect - czyli sterownik komunikacji (u nas to będzie org.hibernate.dialect.H2Dialect)
  • hibernate.hbm2ddl.auto - automatyczna walidacja lub eksportowanie schematu DDL (języka definicji danych) do bazy danych. Definiujemy tutaj właśnie strategie jaką ma przyjąć przy otwarciu sesji z bazą danych. Możliwe są:
    • validate - weryfikacja poprawności schematu, nie robi żadnych zmian na bazie,
    • update - modyfikuje schemat bazy danych,
    • create - tworzy tabele na nowo w schemacie bazy i usuwa poprzednie dane,
    • create-drop - wykonuje czynność operacji create i na końcu sesji usuwa schemat bazy danych,
  • hibernate.ejb.naming_strategy - czyli strategia tworzenia nazw tabel,
  • hibernate.show_sql - flaga czy powinny być logowane zapytania sql,
  • hibernate.format_sql - flaga czy powinny zapytania być formatowane.

Parametrów konfiguracyjnych jest o wiele, wiele więcej. Opisałem tutaj tylko te, które zostały wykorzystane w kodzie naszego programu.

<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager"> <property name="entityManagerFactory" ref="entityManagerFactory"/> </bean>

lub

@Bean JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) { JpaTransactionManager transactionManager = new JpaTransactionManager(); transactionManager.setEntityManagerFactory(entityManagerFactory); return transactionManager; }

To deklaracja menadżera tranzakcji. Tutaj jest podstawowa konfiguracja w której ustawiany jest tylko EntityManagerFactory.

<tx:annotation-driven transaction-manager="transactionManager"/>

lub

@EnableTransactionManagement

Tutaj zaznaczamy Springowi, że zezwalamy na obsługę tranzakcji poprzez adnotacje.

Ok, starczy tej konfiguracji. Zróbmy CrudRepository dla klasy Time. W konfiguracji zaznaczyliśmy, że JPA Repository ma wyszukiwać w ścieżce "pl.devpragmatic.lifetimer.repository". Posiadamy pod tą ścieżką naszą klasę TimeRepository. Wystarczą małe modyfikacje i dostosujemy ją do nowego interfejsu.

package pl.devpragmatic.lifetimer.repository;
import java.util.List;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
import pl.devpragmatic.lifetimer.domain.Time;
/**
* Repository interface for time
* @author devpragmatic
*
*/
@Repository
public interface TimeRepository extends CrudRepository<Time, Long> {
List<Time> findAll();
}

Oznaczmy także klase Time adnotacjami, aby hibernate wiedział jak ma się zachować z tą klasą.

package pl.devpragmatic.lifetimer.domain;
import java.io.Serializable;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
/**
*
* @author devpragmatic
*/
@Entity
public class Time implements Serializable {
private static final int HOURS_IN_DAY = 24;
private static final int MINUTES_IN_HOUR = 60;
private static final int SECONDS_IN_MINUTE = 60;
@Id
@GeneratedValue
private Long id;
private int days;
private int hours;
private int minutes;
private int seconds;
private Long parentId;
public void setTime(int days, int hours, int minutes, int seconds) {
minutes = seconds/SECONDS_IN_MINUTE + minutes;
hours = minutes/MINUTES_IN_HOUR + hours;
days = hours/HOURS_IN_DAY + days;
this.days = days;
this.hours = hours%HOURS_IN_DAY;
this.minutes = minutes%MINUTES_IN_HOUR;
this.seconds = seconds%SECONDS_IN_MINUTE;
if(this.seconds < 0){
this.seconds += SECONDS_IN_MINUTE;
this.minutes--;
}
if(this.minutes < 0){
this.minutes += MINUTES_IN_HOUR;
this.hours--;
}
if(this.hours < 0){
this.hours += HOURS_IN_DAY;
this.days--;
}
if(this.days < 0){
reset();
}
}
public Long getId() {
return id;
}
public int getDays() {
return days;
}
public int getHours() {
return hours;
}
public int getMinutes() {
return minutes;
}
public int getSeconds() {
return seconds;
}
public Long getParentId() {
return parentId;
}
public boolean hasParent() {
return parentId != null;
}
public void setParentId(Long parentId) {
this.parentId = parentId;
}
public void addTime(Time time) {
if(time != null){
this.days += time.days;
this.hours += time.hours;
this.minutes += time.minutes;
this.seconds += time.seconds;
this.setTime(days, hours, minutes, seconds);
}
}
public void subTime(Time time) {
if(time != null){
this.days -= time.days;
this.hours -= time.hours;
this.minutes -= time.minutes;
this.seconds -= time.seconds;
this.setTime(days, hours, minutes, seconds);
}
}
private void reset(){
this.days = 0;
this.hours = 0;
this.minutes = 0;
this.seconds = 0;
}
@Override
public String toString() {
return "Time{" + "id=" + id + ", days=" + days + ", hours=" + hours + ", minutes=" + minutes + ", seconds=" + seconds + ", parentId=" + parentId + '}';
}
}
view raw Time.java hosted with ❤ by GitHub

Właściwie już koniec implementacji naszego połączenia z bazą. Możemy teraz napisać program obsługujący to. Zróbmy prosty program, który będzie tworzył i pobierał obiekty z bazy.

package pl.devpragmatic.livetimerdesktop;
import java.util.List;
import pl.devpragmatic.lifetimer.domain.Time;
import pl.devpragmatic.lifetimer.service.TimeService;
/**
*
* @author devpragmatic
*/
public class TimeCircle {
public void run(TimeService timeService){
Time time = new Time();
timeService.add(time);
System.out.println(time);
Time timeSecond = new Time();
timeSecond.setParentId(time.getId());
timeService.add(timeSecond);
List<Time> times = timeService.getAll();
System.out.println(times);
}
}
view raw TimeCircle.java hosted with ❤ by GitHub

Wywołanie programu z użyciem konfiguracji w klasie Javowej.

package pl.devpragmatic.livetimerdesktop;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import pl.devpragmatic.lifetimer.service.TimeService;
import pl.devpragmatic.livetimerdesktop.config.PersistenceContext;
/**
*
* @author devpragmatic
*/
public class Application {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(PersistenceContext.class);
TimeService timeService = ctx.getBean(TimeService.class);
TimeCircle circle = new TimeCircle();
circle.run(timeService);
}
}

Wywołanie programu z użyciem konfiguracji w pliku .xml.

package pl.devpragmatic.livetimerdesktop;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import pl.devpragmatic.lifetimer.service.TimeService;
/**
*
* @author devpragmatic
*/
public class ApplicationXmlConfig {
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationconfig.xml");
TimeService timeService = ctx.getBean(TimeService.class);
TimeCircle circle = new TimeCircle();
circle.run(timeService);
}
}

Przedstawiłem Wam w tym wpisie jak prosto zrobić aplikację, która korzysta z bazy danych. Zapraszam do przeczytania mojego poprzedniego wpisu, który wytłumaczy Wam jak działają poszczególne warstwy baz danych. Projekty są umieszczone na repozytorium github LifeTimerDesktop i LifeTimer.

Comments

Popular posts from this blog

My mistakes in working with legacy code

Don't change project, change yourself.

Spring Framework - skąd jego popularność?