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:
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.
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> |
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.
lub
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").
lub
Przekazuję infomacje do aplikacji, że repozytorium JPA ma szukać pod ścieżką "base-package" (czyli u nas: "pl.devpragmatic.lifetimer.repository").
lub
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).
lub
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.
lub
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.
lub
To deklaracja menadżera tranzakcji. Tutaj jest podstawowa konfiguracja w której ustawiany jest tylko EntityManagerFactory.
lub
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 + '}'; | |
} | |
} |
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); | |
} | |
} |
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
Post a Comment