czwartek, 6 maja 2010

Spring Framework 3.0 Tutorial – cz 2 – baza danych, walidacja,wiadomości, encje, hibernate

W tej części tutorialu skupimy się na skonfigurowaniu połączenia z bazą danych, podłączeniu frameworka hibernate do naszej aplikacji oraz zobaczymy jak tworzyć encje i jak sprawdzać poprawność danych przed ich zapisem (walidacja).

Przygotowanie do pracy


W poprzedniej części przygotowaliśmy w ramach części klienckiej małą aplikację "witaj świecie", ponieważ projekt nie zawierał elementów stricte przyporządkowanych części dla klienta możemy skopiować go (głównie pliki konfiguracyjne) i użyć w panelu administracyjnym (prawie jak copypasteryzm).

Aby przejść przez tą część tutoriala będziemy musieli zainstalować bazę danych PostgreSQL oraz pobrać kilka dodatkowy bibliotek (zrobimy to wykorzystując mavena).
Jak wspomniałem w pierwszym odcinku tutoriala, wspólne klasy będą trzymane w osobnym projekcie "bookstore-lib", tam przechowywane będą klasy Encji, Dao oraz Serwisy, a także wspólne klasy które będą wykonywać specyficzne zadania (powstaną wkrótce), eclipse zadba, by dołączyć je automatycznie podczas kompilacji. Projekt "bookstore-lib" również korzysta z mavena do obsługi bibliotek wykorzystanych w projekcie, nie posiada on natomiast zintegrowanego Spring frameworka (co jest logiczne). Nad jego konfiguracją rozpisywać się nie będę, stworzyłem czysty projekt, dodałem obsługę mavena i zacząłem pisać klasy (w tym przypadku na pierwszy ogień poszły Encje), gdy eclipse wykrył, że nie mam jakiejś klasy zależnej dodałem ją do projektu ustawiając odpowiedni zakres (scope - jeśli klasa wykorzystywana jest w panelu, zakres ustawiałem na runtime, jeśli była używana stricte w bookstore-lib, zakres ustawiany był na compile).

Projekt "bookstore-admin" prócz bibliotek potrzebnych do uruchomienia Springframework-a potrzebuje również bibliotek odpowiedzialnych za obsługę bazy danych oraz walidację formularzy, do pliku pom.xml dodałem następujące biblioteki (tu również skorzystałem z zakresów - scope, dzięki czemu aplikacje nie posiadały zdublowanych bibliotek).

<dependency>
<groupId>postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>8.4-701.jdbc4</version>
<type>jar</type>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>3.0.2.RELEASE</version>
<type>jar</type>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>3.3.2.GA</version>
<type>jar</type>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
<version>3.4.0.GA</version>
<type>jar</type>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.5.8</version>
<type>jar</type>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>4.0.2.GA</version>
</dependency>

Konfiguracja bazy danych


Konfiguracja bazy danych jak i wszystkich elementów związanych z obsługą zapisu do bazy danych oraz odczytu z niej znajduje się w pliku web-data.xml (trzeba być porządnym programistą i nie tworzyć śmietnika konfiguracyjnego).
Zanim skonfigurujemy połączenie z bazą danych musimy stworzyć użytkownika bazy danych oraz dodać bazę z którą będzie się łączyć, jeśli ktoś nie wie polecam google.com lub program pgAdmin. Połączenie z bazą danych możemy skonfigurować na 2 sposoby (tyle znam), pierwszy (standardowy), czyli dostęp do bazy poprzez adres serwera+port oraz dane uwierzytelniające, drugi to dostęp do bazy poprzez nazwę JNDI (czytaj więcej o Java Naming and Directory Interface API), ważną zaletą (moim zdaniem) połączenia JNDI jest niezależność ustawień autoryzacyjnych, ponieważ łączymy się z bazą nie poprzez login i hasło, a ustaloną wcześniej przez serwer nazwę, możliwe jest bezproblemowe (bez potrzeby ponownego grzebania w konfiguracji) zmienianie danych autoryzacyjnych, czy też adresu bazy danych dlatego też oprę naszą aplikację o ten typ połączenia.
Ponieważ połączenia JNDI opierają się na skonfigurowanej nazwie musimy taką konfigurację wprowadzić. W naszym przypadku (tomcat 6.0) robimy to w pliku TOMCAT_HOME/conf/context.xml1 poprzez wpisanie między tagi <context> zawartości:

<Resource auth="Container" driverClassName="org.postgresql.Driver" name="bookstore" password="bookstore" type="javax.sql.DataSource" url="jdbc:postgresql://localhost:5432/bookstore" username="bookstore"/>

Co w skrócie oznacza "przypisz nazwie bookstore połączenie do bazy o nazwie bookstore znajdującej się pod adresem localhost, na porcie 5432 wykorzystując nazwę użytkownika bookstore oraz hasło bookstore".

Nasza aplikacja korzystać będzie z frameworka hibernate, dlatego potrzebujemy skonfigurować jego działanie. Konfiguracja taka polega na utworzeniu pliku  hibernate.cfg.xmlclasspath (np. src/META-INF; może ktoś zna polski odpowiednik classpath) i wpisujemy zawartość:

<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
<session-factory>
<property name="hibernate.dialect">org.hibernate.dialect.PostgreSQLDialect</property>
<property name="hibernate.hbm2ddl.auto">update</property>
<property name="hibernate.show_sql">true</property>
<property name="hibernate.format_sql">true</property>
<property name="hibernate.use_sql_comments">true</property>
<mapping
class="com.darekzon.bookstore.domain.Account" />
<mapping
class="com.darekzon.bookstore.domain.AccountRole" />
<mapping
class="com.darekzon.bookstore.domain.Administrator" />
</session-factory>
</hibernate-configuration>


Pliik ustawia kolejno:
  1. hibernate.dialect -  jaki język zapytań będzie wykorzystywany (w tym wypadku rozszerzenia dodane przez postgresql)
  2. hibernate.hbm2ddl.auto - automatycznie aktualizuj schemat bazy danych na podstawie class encji (super sprawa)
  3. hibernate.show_sql - pokazuje zapytania sql jakie wykonuje hibernate (dobre przy debugowaniu, czasem wprowadza chaos do konsoli)
  4. hibernate.format_sql - formatuje w/w zapytania, przydaje się
  5. hibernate.user_sql_comments - dodatkowe komentarze, pomagają zorientować się jakie zapytanie aktualnie jest wykonywane
  6. mapping class  - wskazanie jakie klasy traktowane są jako klasy encji (muszą być oznaczone adnotacją @Entity), wykorzystywane przy aktualizacji schematów

Drugim ważnym plikiem jest persistence.xml który konfiguruje nam PersistenceUnit czyli jednostę utrwalającą. Plik ten ma zawartość:

<persistence xmlns="http://java.sun.com/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"
version="2.0">
<persistence-unit name="bookstorePU" transaction-type="RESOURCE_LOCAL">
<provider>org.hibernate.ejb.HibernatePersistence</provider>
<jta-data-source>bookstore</jta-data-source>
<class>com.darekzon.bookstore.domain.Administrator</class>
<class>com.darekzon.bookstore.domain.Account</class>
<class>com.darekzon.bookstore.domain.AccountRole</class>
</persistence-unit>
</persistence>

Określamy w nim nazwę jednostki utrwalającej, typ transakcji, dostawę jednostki oraz adres naszego źródła danych, musimy również wprowadzić informacje, które klasy będą używane podczas utrwalania (więc pewne dane się zdublują z plikiem hibernate.cfg.xml). Powyższy plik możemy umieścić w tym samym miejscu co plik hibernate.cfg.xml, tak by był dostępny dla JVM podczas ładowania aplikacji.

Gdy mamy już skonfigurowanego hibernate-a oraz jednostę utrwalającą, pora zintegrować je z naszą aplikacja, polegać to będzie na utworzeniu szeregu beanów odpowiedzialnych za obsługę żądań do bazy danych. Konfiguracja ta została zapisana w pliku web-data.xml i ma zawartość:

<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:mvc="http://www.springframework.org/schema/mvc"
xmlns:jee="http://www.springframework.org/schema/jee"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd">

<jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/bookstore"
cache="true" resource-ref="true" lookup-on-startup="false"
proxy-interface="javax.sql.DataSource" />

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

<bean id="sessionFactory"
class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="configurationClass" value="org.hibernate.cfg.AnnotationConfiguration"/>
<property name="configLocation" value="classpath:META-INF/hibernate.cfg.xml" />
</bean>

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

<bean id="templateManager"
class="org.springframework.orm.hibernate3.HibernateTemplate">
<property name="sessionFactory" ref="sessionFactory" />
</bean>

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

</beans>


Konfiguracja zawiera:

dataSource:Wyszukuje nasze połączenie (skonfigurowane w serwerze) i binduje je do nazwy dataSource

entityManager: manager encji

sessionFactory: można powiedzieć, że sesja jest tożsama (oznacza to samo) co połączenie z bazą danych. Dzięki podaniu klasy AnnotationConfiguration do parametru configurationClass mogę używać adnotacji do konfigurowania encji zamiast pisać pliki *.hba.xml. Parametr configLocation wskazuje miejsce gdzie znajduje się plik konfiguracyjny hibernate-a.

transactionManager: manager transakcji (raczej jasne)

templateManager: posiada szereg metod upraszczających obsługę ORM (wyszukiwanie, zapisywanie, aktualizacja, kasowanie etc.), do działania wymaga sesji podawanej w parametrze sessionFactory.

tx:annotation-driven: dzięki temu zapisowi możemy używać dodatkowych adnotacji (np. @Transactional)

Należy pamiętać by dopisać ścieżkę do konfiguracji  (/WEB-INF/web-data.xml) w pliku web.xml (w miejscu konfiguracji kontekstu).

Tak skonfigurowany spring, będzie lączył się przy starcie ze zdefiniowanym serwerem bazy danych, udostępnia również szereg obiektów pomagających w dostępie do bazy. Jeśli połączenie działa poprawnie (czyt. przy starcie nie wyskakuje żadne wyjątek) bierzemy się do dalszej pracy.

Obsługa wiadomości / tłumaczenia


Jestem zwolennikiem odcinania treści od formy, nie lubię gdy w kodzie strony widzę treści takie jak, nazwy pól, komunikaty błędów, czy też nagłówki. Takie podejście sprawia problem, gdy będziemy musieli zmienić tekst znajdujący się w wielu elementach aplikacji czy też co ważniejsze udostępnić aplikację w wielu językach.  Na szczęście istnieje proste rozwiązanie jakim jest mechanizm wiadomości. Mechanizm ten składa się z 3 elementów: konfiguracji wskazującej gdzie znajdują się teksty, pliku z tekstami oraz odpowiednich wstawek w kodzie aplikacji które wskazują jaka treść ma zostać umieszczona w danym miejscu.

Konfiguracja mechanizmu jest niezwykle prosta, polega na zdefiniowaniu nowego obiektu i przekazaniu mu listy plików w której znajdują się nasze wiadomości (poniżej):

<bean id="messageSource"
class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basenames">
<list>
<value>/WEB-INF/messages/main</value>
</list>
</property>
</bean>


Pewną nieścisłością jest fakt, że obiekt ten będzie przeszukiwał katalog /WEB-INF/messages/ w poszukiwaniu pliku main.properties, a nie jak można by sadzić pliku main, o czym trzeba pamiętać.

Nasz plik z wiadomościami ma prostą budowę, składa się z zestawu   klucz = treść, gdzie klucz jest unikalnym identyfikatorem nadawanym naszej treści, będzie on również używany w kodzie strony, gdzie należy wstawiać tagi w postaci <tag:message code="IDENTYFIKATOR" /> który zostanie zastąpiony odpowiednią treścią. Proste w wygodne.

Dodawanie administratorów


Pierwszym zadaniem z jakim się zmierzymy jest dodawanie administratorów (w następnej części tutoriala zabezpieczymy nasz system, więc potrzebujemy konta by móc się zalogować), gdzie konta powinny spełniać wymagania:
  1. hasło musi mieć przynajmniej 8 znaków, ale nie więcej jak 20 znaków
  2. hasła zaszyfrowane zostaną algorytmem SHA-2 (odmiana sha256)
  3. wykorzystana zostanie sól w postaci identyfikatora użytkownika (identyfikator nie będzie wystawiany na widok publiczny)
  4. w bazie danych przechowywani będą użytkownicy o różnych poziomach dostępu, do panelu administracyjnego dostęp będą mieli tylko i wyłącznie Ci użytkownicy, którzy posiadają poziom ROLE_ADMIN

Z powyższych krótkich wymagań można łatwo wywnioskować, że potrzebujemy obiektu który będzie szyfrować ciągi znaków, oraz jednego do tworzenia soli. Obiekty te skonfigurowane zostaną w kolejnym pliku xml zapisanym pod nazwą security.xml, z racji, że są to obiekty wykorzystywane przy funkcjach bezpieczeństwa.
Plik ten aktualnie prezentuje się jak poniżej:

<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:mvc="http://www.springframework.org/schema/mvc"
xmlns:jee="http://www.springframework.org/schema/jee"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/jee
http://www.springframework.org/schema/jee/spring-jee-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd">

<bean id="passwordEncoder" class="org.springframework.security.authentication.encoding.ShaPasswordEncoder">
<constructor-arg value="256"/>
</bean>

<bean id="saltSource" class="org.springframework.security.authentication.dao.ReflectionSaltSource">
<property name="userPropertyToUse" value="id"/>
</bean>

</beans>


Diagram klas struktury przechowującej dane nt. administratorów wygląda następująco:

Klasa administratora (na razie pusta) rozszerza klasę użytkownika która posiada login oraz hasło jako dane uwierzytelniające, dodatkowo użytkownik agreguje poziom dostępu użytkownika (z racji, że w tej samej tabeli przechowywani będą administratorzy, operatorzy oraz zwykli użytkownicy) który wydelegowany jest do oddzielnej klasy.

Tworzymy Encje


Powyższy diagram musimy przepisać na klasy encji, najprościej mówiąc encja to obiekt reprezentujący jeden rekord jednej tabeli, tak więc Encja Account będzie odzwierciedleniem strukturę tabeli account.

Encje jakie utworzyliśmy podczas projektu powinny wyglądać tak:

Administrator (pełna zawartość pliku w repozytorium - zobacz koniec wpisu):

@Table(name = "administrator")
@Entity
public class Administrator extends Account { }


Account (pełna zawartość pliku w repozytorium - zobacz koniec wpisu):

@Entity
@Table(name = "account")
@Inheritance(strategy = InheritanceType.JOINED)
public class Account {

@Id
@GeneratedValue(generator = "account_id", strategy = GenerationType.SEQUENCE)
@SequenceGenerator(name = "account_id", sequenceName = "account_id_seq")
Integer id;

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

@Basic
@NotNull
@Size(min = 5, max = 20)
String username;

public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

@Basic
@Size(min = 8, max = 20)
String password;

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}

@Transient
public String repeatedPassword;

public String getRepeatedPassword() {
return repeatedPassword;
}

public void setRepeatedPassword(String repeatedPassword) {
this.repeatedPassword = repeatedPassword;
}

@Basic
@DateTimeFormat(iso = ISO.DATE_TIME)
Date addDate = new Date();

public Date getAddDate() {
return addDate;
}

public void setAddDate(Date addDate) {
this.addDate = addDate;
}

@OneToMany(cascade = CascadeType.ALL, mappedBy = "account", fetch = FetchType.EAGER, targetEntity = AccountRole.class)
List<AccountRole> accountRole = new ArrayList<AccountRole>();

public List<AccountRole> getAccountRole() {
return accountRole;
}

public void setAccountRole(List<AccountRole> accountRole) {
for (AccountRole ar : accountRole) {
this.addAccountRole(ar);
}
}

public void addAccountRole(AccountRole accountRole) {
if (!this.accountRole.contains(accountRole)) {
accountRole.setAccount(this);
this.accountRole.add(accountRole);
}
}
}


AccountRole (pełna zawartość pliku w repozytorium - zobacz koniec wpisu):

@Entity
@Table(name = "account_role")
public class AccountRole {

public AccountRole() {
this.setRole("ROLE_ANONYMOUS");
}

public AccountRole(String role) {
this.setRole(role);
}

@Id
@GeneratedValue(generator = "role_id", strategy = GenerationType.SEQUENCE)
@SequenceGenerator(name = "role_id", sequenceName = "account_role_id_seq", initialValue = 1)
Integer roleId;

public Integer getRoleId() {
return roleId;
}

public void setRoleId(Integer roleId) {
this.roleId = roleId;
}

@Basic
String role;

public String getRole() {
return role;
}

public void setRole(String role) {
this.role = role;
}

@ManyToOne(targetEntity = Account.class, cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@JoinColumn(name = "accountId", nullable = true)
Account account;

public Account getAccount() {
return account;
}

public void setAccount(Account account) {
this.account = account;
}

}


Warto zwrócić uwagę na adnotacje:
  1. @Entity - oznacza, że dana klasa jest klasą encyjną
  2. @Table - mówi jaka tabela w bazie odpowiada naszej encji (jeśli ustawimy, hibernate automatycznie wyeksportuje naszą encje do danej tabeli)
  3. @Inheritance - mówi hibernate-owi, że ewentualne zależności (poprzez dziedziczenie) powinny być zapisywane w osobnych tabelach (UWAGA! - domyślnie, hibernate łączy wszelkie zależności w jednej tabeli), mechanizm sam zadba o odpowiednie klucze obce.
  4. @Id - oznacza wybrane pole jako identyfikator
  5. @GeneratedValue - konfiguruje metodę generowania wartości dla pola, wykorzystywany w identyfikatorach (autoincrement)
  6. @SequenceGenerator - konfiguruje generator sekwencji dla wybranego pola
  7. @Basic - zwykłe pole
  8. @Transistent - mówi aplikacji, że dane pole nie jest odzwierciedlone w bazie danych
  9. @DateTimeFormat - ustala format daty, dla pola które datę będzie przechowywać
  10. @ManyToOne - tworzy relacje wiele-do-jednego
  11. @OneToMany - tworzy relacje jeden-do-wielu
  12. @JoinColumn - ustawia kolumnę łączącą dwie tabele

Ostatnie adnotacje posłużyły do skonfigurowania relacji Konto->Poziom dostępu, dzięki temu przy pobieraniu informacji o koncie, automatycznie dostaniemy listę poziomów dostępu jakie posiada użytkownik.

Drugą ważną rzeczą w powyższych klasach encji są adnotacje służące walidowaniu Dzięki nim nie musimy zaśmiecać kontrolerów dodatkowymi regułami sprawdzającymi, czy też konfigurować walidacji przez XML, reguły walidacji to kolejno:
  1. @NotNull - pole nie może być nullem
  2. @Size - wielkość pola musi mieścić się w granicach min do max znaków

Oczywiście, to nie jedyne walidatory jakie są dostępne, pełną listę podstawowych walidatorów znajdziecie w dokumentacji.

Dodawanie administratorów będzie zapisane w kontrolerze AdministratorController, na początek zaimplementujemy metodę która będzie wyświetlać formularz rejestracyjny.

AdministratorController (pełna zawartość pliku w repozytorium - zobacz koniec wpisu):

@Controller
public class AdministratorController {

@Autowired
AccountService accountService;

@RequestMapping(value = "/administrator/add", method = RequestMethod.GET)
public ModelAndView add() {
ModelAndView mav = new ModelAndView("/administrator/add");
mav.addObject("administrator", new Administrator());
return mav;
}

}


Co warto zauważyć?
  1. @Controler - każdy kontroler oznaczony musi być adnotacją @Controler, dzięki czemu spring podczas skanowania pakietu dodaje go do listy
  2. @Autowired - mówi Spring-owi aby poszukał obiektu o zadanym typie i automatycznie podpiął go do pola
  3. @RequestMapping - mówi springowi jakie żądanie ma być skierowane do danej metody, opcja RequestType wskazuje dodatkowo jaki typ żądania ma być tam skierowane,w tym przypadku zależy nam na żądaniu GET

Powyższa metoda zwraca obiekt ModelAndView (standardowy obiekt zwracany przez większość metod) który przechowuje informacje na temat pliku widok oraz obiekt klasy Administrator, obiekt ten zbindujemy w widoku dzięki czemu wartości pól w formularzu zostaną przepisane do odpowiadających im wartości pól w obiekcie.
Widok formularza prezentuje się następująco:

<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<%@ taglib prefix="tag" uri="http://www.springframework.org/tags" %>
<div>
<form:form commandName="administrator">
<ol>
<li>
<form:label path="username"><tag:message code="administrator.username" /></form:label>
<form:input path="username" />
<form:errors path="username" />
</li>
<li>
<form:label path="password"><tag:message code="administrator.password" /></form:label>
<form:password path="password" />
<form:errors path="password" />
</li>
<li>
<form:label path="repeatedPassword"><tag:message code="administrator.repeatedPassword" /></form:label>
<form:password path="repeatedPassword" />
<form:errors path="repeatedPassword" />
</li>
<li>
<input type="submit" />
</li>
</ol>
</form:form>
</div>


Gdzie:
  • taglib - pozwala stosować rozszerzenia w JSP; form-generowanie formularzy, obsługa błedów, tag-wyświetlanie wiadomości o których wspomniałem wcześniej
  • commandName - to nazwa nadana naszemu obiektowi podczas przekazywania go z kontrolera do widoku,
  • input path - to pole w naszym obiekcie
  • errors - lista błędów dla danego pola (pojawia się po walidacji)

Powyższy formularz prezentuje się następująco:



Po wciśnięciu wyślij, dane z formularza już pod postacią obiektu klasy Administrator przekazywane są do kolejnej metody która sprawdza poprawność danych, i jeśli wszystko jest jak trzeba zapisuje je. Metoda ta wygląda następująco (pełna zawartość pliku w repozytorium - zobacz koniec posta):

@RequestMapping(value = "/administrator/add", method = RequestMethod.POST)
public ModelAndView add(@Valid Administrator admin, BindingResult result) {
ModelAndView mav = new ModelAndView("administrator/add");
if (!admin.getPassword().equals(admin.getRepeatedPassword())) {
result.addError(new FieldError("administrator", "repeatedPassword",
"notEqual"));
}
if (result.hasErrors()) {
mav.addObject("administrator", admin);
return mav;
}
List<AccountRole> ar = new ArrayList<AccountRole>();
ar.add(new AccountRole("ROLE_ADMIN"));
try {
accountService.registerAccount(admin, ar);
} catch (UserExistsException e) {
result.reject("user.exists");
mav.addObject("administrator", admin);
return mav;
}
mav.setViewName("redirect:/administrator.html");
return mav;
}


Powyższa metoda przyjmuje dwa argumenty, pierwszym jest nasz obiekt Administrator który zawiera dane z formularza, oraz obiekt BindingResult który przechowuje informacje na temat ewentualnych błędów które wystąpiły podczas sprawdzania poprawności (zgodnie z adnotacjami w klasach encyjnych). Aby Spring wiedział, że dany obiekt wymaga sprawdzenia poprawności danych należy oznaczyć go adnotacją @Valid, dzięki temu, spring automatycznie sprawdzi obiekt, a błędy zapisze w obiekcie BindingResult.

Ponieważ nie istnieje adnotacja walidacyjna (przynajmniej w podstawowym zestawie) która sprawdza czy dwa pola są identyczne musiałem zaprogramować dodatkowe sprawdzanie już w kontrolerze. Gdy akcja wykryje, że hasło oraz jego powtórzona wartość nie są identyczne, doda do obiektu BindingResult błąd dla pola repeatedPassword przez co formularz nie zostanie zapisany w bazie, a zamiast tego zostanie ponownie wyświetlony z zaznaczonym błędem, jak przykład poniżej:


Jeśli natomiast wszystkie nasze pola zostaną wypełnione poprawnie nastąpi próba zapisania formularza accountService.registerAccount, a po poprawnym zapisaniu zostaniemy przekierowani (dyrektywa "redirect:/administrator") do strony /administrator.html. Akcja rejestracji nowego administratora składa się z 2 plików (nie wliczając interfejsów które te pliki implementują.

Pierwszy plik to AccountServiceImpl który jest w tym wypadku głównym plikiem (pełna zawartość pliku w repozytorium - zobacz koniec posta):

@Transactional
@Repository
public class AccountServiceImpl implements AccountService {
@Autowired
AccountDao accountDao;

@Autowired
PasswordEncoder passwordEncoder;

@Autowired
SaltSource saltSource;

@PersistenceContext
EntityManager entityManager;

@Override
public void registerAccount(Account account,List<AccountRole> ar) throws UserExistsException {
try{
accountDao.findUsername(account.getUsername());
throw new UserExistsException();
}catch(UserNotFoundException une){
entityManager.persist(account);
account.setPassword(passwordEncoder.encodePassword(account.getPassword(), saltSource));
for(AccountRole arole : ar){
account.addAccountRole(arole);
}
entityManager.flush();
}

}

}


Nasza klasa oznaczona została dwoma adnotacjami, @Transactional, która mówi że, wszystkie metody w niej zaimplementowane objęte są transakcją, oraz @Repository które pozwala nam na pobranie managera encji poprzez adnotację @PersistenceContext.

Aby nasza metoda działała poprawnie musimy dostarczyć jej obiekt accountDao który posłuży do sprawdzenia czy dany użytkownik już istnieje, manager encji który zapisze nasz obiekt w bacie, a także obiekty passwordEncoder oraz saltSource służące szyfrowaniu hasła.

Po wywołaniu metody obiekt accountDao znajduje użytkownika o podanej nazwie użytkownika i jeśli go nie znajdzie wyrzuca wyjątek UserNotFoundException i gdy ten wyjątek wystąpi możemy zapisywać użytkownika do bazy. Nasz administrator zapisywany jest metodą persist z obiektu entityManager. Dzięki tej metodzie nasza encja zostanie tymczasowo związana z rekordem w bazie danych co oznacza, że wszelkie modyfikacje administratora będą automatyczne odzwierciedlane w bazie danych (więcej o cyklu życia encji w dokumentacji). Gdy nasz użytkownik zostanie zapisany musimy zaktualizować mu hasło, dzieje się tak, gdyż hasło szyfrowane algorytmem sha256 z dodatkową solą w postaci identyfikatora użytkownika, który nadawany jest dopiero po zapisaniu go do bazy danych.

Po aktualizacji hasła przypisujemy naszemu użytkownikowi odpowiednie poziomy dostępu (może mieć ich więcej jak jeden) poprzez dodanie obiektów AccountRole do obiektu Administrator (dane zostają zapisane dzięki wcześniejszemu skonfigurowaniu powiązań @ManyToOne i @OneToMany).

Ciekawie wygląda również plik AccountDaoImpl który wyszukuje użytkowników po zadanym loginie (pełna zawartość pliku w repozytorium - zobacz koniec posta):

public class AccountDaoImpl implements AccountDao {

@Autowired
HibernateTemplate template;

@Autowired
SessionFactory session;

public Account findUsername(String username) throws  UserNotFoundException {
DetachedCriteria dc = DetachedCriteria.forClass(Account.class);
dc.add(Property.forName("username").eq(username));
List users = template.findByCriteria(dc, 0, 1);

if (users.size() > 0) {
return (Account) users.get(0);
}

throw new UserNotFoundException();
}

}


W ciele metody findUsername zastosowane zostało wyszukiwanie po kryteriach, CriteriaApi daje ogromne możliwości w dynamicznym budowaniu zapytań do bazy, w naszym przypadku wskazaliśmy, że dane zapytanie dotyczyć będzie klasy Account (a tym samym tabeli odzwierciedlającej tą klasę), oraz dodaliśmy właściwość pola username ustalając że pole to powinno być równe wartości podanej w argumencie.

Wyświetlamy użytkowników


Jeśli nasi administratorzy zostali zapisani do bazy, warto by ich wyświetlić w liście, w tym celu stworzymy metodę obsługującą żądanie /administrator.html. Metoda ta znajduje się w pliku AdministratorController i wygląda następująco:

@RequestMapping(value = "/administrator")
public ModelAndView index() {
ModelAndView mav = new ModelAndView("/administrator/index");
List<String> roles = new ArrayList<String>();
roles.add("ROLE_ADMIN");
List<Account> acc = accountService.listAccounts(roles);
mav.addObject("accounts", acc);
return mav;
}


Metoda listująca pobiera z bazy wszystkich użytkowników o zadanym poziomie dostępu (AccountRole) po czym przekazujemy listę do widoku. Metoda wyszukująca użytkowników zaimplementowana jest w dwóch plikach, pierwszy AccountServiceImpl poniżej:

@Override
@Transactional(readOnly=true)
public List<Account> listAccounts(List<String> roles) {
return accountDao.listAccounts(roles);
}


Ma jedynie za zadanie wywołać metodę wyszukującą z obiektu AccountDaoImpl, warto wyjaśnić adnotację @Transactional, ponieważ wszystkie metody w klasie AccountServiceImpl objęte są transakcją warto przy metodach które nie zapisują danych zaznaczyć, że transakcja ma być traktowana jako tylko do odczytu, dzięki temu zyskujemy na wydajności.

Metoda wywołana przez powyższą metodę ma postać:

@Override
public List listAccounts(List roles) {
Criteria criteria = session.openSession().createCriteria(Account.class);
criteria.addOrder(Order.asc("username"));
criteria.createCriteria("accountRole").add(Restrictions.in("role",roles));
return criteria.list();
}


Również tutaj zastosowanie znalazły kryteria, choć w troszkę inny sposób. W tym przypadku wyszukujemy danych dla klasy Account sortując je (criteria.addOrder) według nazwy użytkownika, przy czym wszystkie zwracane rekordy powinny posiadać powiązany rekord z tabeli reprezentowanej przez obiekt accountRole który pole role ma zadaną wartość (tu ROLE_ADMIN). Ponieważ do wyszukania poziomów dostępu musimy złączyć dwie tabele robimy to poprzez stworzenie nowych cryteriów "na obiekcie" już aktywnych kryteriów.

UPDATE 1

W komentarzach zauważyliście, że w aplikacji mieszam JPA z hibernatem, aby to poprawić stworzyłem nową metodę z wykorzystaniem JPA QL:

@Override
public Collection<Account> listAccounts(Collection<AccountRole> roles){
List<String> c = new ArrayList<String>(roles.size());
for(AccountRole in : roles){
c.add(in.getRole());
}
List<Account> accounts = entityManager.createQuery("SELECT a FROM Account a JOIN a.accountRole r WHERE r.role IN(?1)",Account.class).setParameter(1,c).getResultList();
return accounts;
}


Powyższa metoda przyjmuje jako argument kolekcję ról do jakich muszą należeć konta które pobieramy. Kolekcję obiektów musimy przekrztałcić na kolekcję prymitywów (w naszym wypadku ciągów znaków - STRING).
Następnie przy pomocy managera encji (entityManager) tworzymy zapytanie (createQuery) które przyjmuje tylko jeden parametr przekazywany poprzez setParameter.

Gdy znajdziemy wszystkich użytkowników przekazujemy ich listę do widoku i generujemy:

<%@ taglib prefix="c" uri="http://java.sun.com/jstl/core_rt" %>

<c:choose>
<c:when test="${not empty accounts}">
<c:forEach items="${accounts}" var="account" varStatus="status">
${status.index} ${account.username}
</c:forEach>
</c:when>
<c:otherwise>
<tag:message code="list.no-entry" />
</c:otherwise>
</c:choose>

Co dalej


W kolejnej części tutoriala postaramy się zabezpieczyć nasz panel administracyjny poprzez zastosowanie SpringSecurity, ponieważ w tej chwili panel administracyjny wygląda biednie, wykorzystamy sitemesh, jsp oraz html by poprawić jego wygląd. A jeśli zostanie trochę miejsca i czasu zajmiemy się.... (może niespodzianka)

PS. Zauważyłem, że ta część tutoriala zajęła mi dużo czasu, postanowiłem rozbić go na mniejsze ale częściej występujące wpisy.

Jak pobrać projekt


Projekt został umieszczony na serwerach GitHub.com. Adres bezpośredni do projektu: https://github.com/darek/bookstore.
Dodatkowo zostało uruchomione repozytorium GIT dla wszystkich chętnych i dostępny jest pod adresem: git://github.com/darek/bookstore.git





  1. Gdzie TOMCAT_HOME jest katalogiem głównym tomcata

28 komentarzy:

  1. Piękny artykuł, na pewno wielu osobą się przyda.
    Co do classpath, ja spotkałem się ze zwrotem ścieżka klas.

    OdpowiedzUsuń
  2. [...] This post was mentioned on Twitter by Developers World. Developers World said: Polecamy w Blogsferze: Spring Framework 3.0 Tutorial – cz 2 – baza danych, walidacja, wiadomości, encje, hibernate http://rdir.pl/aiiwz #... [...]

    OdpowiedzUsuń
  3. Bardzo pomocne i porządne posty! Ten i poprzedni podsunęły mi "ładne" rozwiązania na które jako początkujący z hibernate/spring nie wiem czy bym wpadł :-) A dogłębne zrozumienie sitemesha pomoże z pewnością przy Grailsach... Czekam z niecierpliwością na kolejne części serii :-)

    OdpowiedzUsuń
  4. Hej, dzięki za fajny artykuł. Mam pytanie uzupełniające: naprzemiennie używasz czysto Hibernatowych HibernateTemplate/SessionFactory/Session oraz obiektów JPA (EntityManager etc), skąd takie podejście? Czy chodzi tu tylko o wygodę konstruowania zapytań w Hibernate (użycie Criteria itp.) a samo dodawanie do bazy wolisz robić przez JPA (a jeżeli tak to dlaczego - transakcyjność?). Zauważyłem że w AccountDao dodałeś metodę save() z użyciem hibernate template, ale nie używasz jej, tylko w AccountService używasz em.persist().
    Dzięki za wyjaśnienie.

    OdpowiedzUsuń
  5. @Lukasz

    1. wygoda,
    2. metoda save jest nieużywana, w tym miejscu używam metody persist aby "złączyć" obiekt z bazą, bo po zapisaniu użytkownika do bazy musze mu zaktualizować hasło (sól opiera się o ID użytkownika) oraz przypisać poziomy dostępu.

    OdpowiedzUsuń
  6. Hej Darek, dzięki za odpowiedzi, pozwól że jeszcze trochę podrążę temat (nie żeby się czepiać, jestem dosyć nowy w temacie). Generalnie to co mnie interesuje to czy jest sens łączyć natywne API hibernate z JPA? Z tego co się zorientowałem to obiekt Session Hiberanate także ma metodę persist() i wydaje się, że działa ona podobnie jak z ta z JPA (cytując z API hibernata "Make a transient instance persistent"). Czy jest tu jednak jakaś różnica pomiędzy nimi, która zadecydowała, że wybrałeś JPA do tego celu? Bo jeżeli nie to czy jest wg ciebie sens używania dwóch różnych API w jednej aplikacji? Wydaje się że natywne API hiberanate jest bogatsze od JPA (chociażby od kryteria i filtry) tak więc by wystarczyło.
    I drugie moje pytanie to czy w AccountServiceImpl.registerAccount(..) nie powinno być jakiegoś locka na całość operacji? Istnieje chyba przecież możliwość, że pomiędzy sprawdzeniem dostępności nazwy przez findUserName() i zflushowaniem dodania nowego konta, z innego wątku zostanie dodane konto o tej samej nazwie? Czy może czegoś nie dostrzegam?
    dzięki, Łukasz

    OdpowiedzUsuń
  7. Lukasz:

    1. Jeśli prześledzisz dobrze konfigurację zauważysz w pliku persistence.xml wpis "" który wskazuje co obsługuje mechanizm persystencji. JPA to zbiór zasad (API) które implementuje hibernate. Im więcej korzystasz z JPA które potem łączysz z hibernate tym łatwiej zmienić dostawcę z Hibernate na np. Toplink, czy też

    2. Użyta została adnotacja @Transactional na całej klasie, co oznacza, że wszystkie metody w niej zawarte są objęte transakcją

    OdpowiedzUsuń
  8. Darek,

    Ad1. ok tutaj co do przenośności wszystko jasne, zresztą ją gwarantuje sam Hibernate, tylko mi chodziło o to że naprzemiennie używasz API JPA oaz natywnego API Hibernate + jescze dodatkowo Springowego HibernateTemplate. Ale rozumiem, że to kwestia wygody. Nawiasem mówiąc może zainteresuje Cię wpis który znalazłem na sieci apropo używania (a raczej że nie potrzeba już używać) HimernateTemplate w Springu: http://blog.springsource.com/2007/06/26/so-should-you-still-use-springs-hibernatetemplate-andor-jpatemplate/

    Ad2. Czy tranzakcyjność nie odnosi się do operacji insert / update? W tym przypadku najpierw sprawdzany jest fakt *nieistnienia* konkretnego rekordu a potem dopiero następuje jego stworzenie. Tak więc, wydaje mi się, że jest możliwa sytuacja opisana przeze mnie, i wtedy, jeżeli pole username oznaczone jest jako unique, to poleci wyjątek przy commicie operacji do bazy.

    już nie nadużywam twojej cierpliwości, dzięki za wyjaśnienia ;)

    OdpowiedzUsuń
  9. [...] Pierwszy z nich zawiera już konfigurację obiektu szyfrującego hasła oraz mechanizm soli (z wcześniejszego tutorialu), tym razem musimy dodać obsługę poziomów dostępu oraz mechanizmu uwierzytelniania opartego [...]

    OdpowiedzUsuń
  10. Dobry post... krótko, zwięźle i na temat.

    Ale się pogubiłem...

    Co się dzieje gdy stawiasz 2 konteksty persystencji? Jeden przez "czyste" JPA a drugi przez API Hibernate?
    Co wówczas cache 1 i 2 poziomu? Dlaczego to dublować i nie korzystać we wszystkich Use Case z tego samego?
    Transaction manager jest tylko dla jednego kontekstu - co z drugim? Szczególnie, że servis używa obu - raz przez DAO (session factory) a raz bez hermetyzacji przez EntityManagera.

    Czy jest to zabieg, który ma po prostu ilustrować różne możliwości i podejścia czy może stoi za tym coś głębszego? Jeżeli tak, to trzeba by to wyjaśnić zanim ludzie zaczną powielać metodą Kopiego-Pasty;P

    OdpowiedzUsuń
  11. @Lukasz:
    AD1. jakoś tak przywykłem, bo w poprzednim projekcie korzystałem głównie z hibernatewoych narzędzi, w tym postanowiłem jak najwięcej używać JPA,
    AD2. z tego co wiem transakcja jest nakładana na tabelę, więc obejmuje wszystko (poprawcie jeśli się mylę)

    @Sławek
    Jak wspomniałem przednikowi, używałem głównie narzędzi hibernate-a teraz przechodzę na JPA, w sumie nie wiem czemu nie dołączyłem biblioteki JPA2 (co prawda beta, ale mi to nie przeszkadza), właśnie przepisałem metodę wyszukiwania użytkownika na taką która używa CriteriaBuildera i entityManagera.

    Pytanie: Zaktualizować wpis? czy pozostawić tylko w źródłach?

    OdpowiedzUsuń
  12. Hej Darek,

    @AD1
    Nie wymądrzając się, tylko Ci tutaj zwrócę uwagę na fakt że używając HibernateTemplate wiążesz swój kod DAO dodatkowo ze Springiem co zmniejsza jego przenośność, w sytuacji kiedy dla Hibernate 3.2+ nie jest już to po prostu potrzebne. Osobiście nie jestem wielkim fanatykiem przenaszalności, ale skoro można to warto.

    @AD2
    Wydaje mi się że w sytuacji o której mówię tutaj zastosowanie bądź niezastosowanie tranzakcyjności nie ma żadnego znaczenia. Zwróć uwagę na logikę: najpierw sprawdzasz czy użytkownik o danej nazwie istnieje a potem dodajesz takowego. W żaden sposób nie gwarantuje to, że pomiędzy tymi operacjami nie zostanie dodany (z dowolnego źródła - także z ew. innych aplikacji korzystających z tej bazy danych) użytkownik o tej samej nazwie. Skąd baza ma wiedzieć, że sprawdzasz istnienie użytkownika po to żeby go dodać? Chyba że jest tu jakaś magia o której ja nie wiem...
    W takiej sytuacji, ponieważ pole username jest unique, zostanie rzucony runtime exception z sqlowym błędem. I tranzakcyjność nic do tego nie ma, bo czy jest czy jej nie ma rekord i tak nie zostanie stworzony (co nie znaczy że nie jest potrzebna - jest potrzebna bo potem robisz updaty wpisując skrót hasła);.

    I teraz są dwie możliwości:
    1. jeżeli świadomie założysz, że tylko ta aplikacja będzie korzystać z bazy, to możesz zrobić jakiś lock na poziomie aplikacyjnym którym wymusisz eliminację duplikatów na etapie jeszcze przed tranzakcją z bazą
    2. zrezygnujesz ze sprawdzania nazwy w bazie przed persist() (no bo po co skoro zaraz po tym ktoś to może dodać?)i po prostu obsłużysz RuntimeException.

    Popieram to co napisał Sławek - zmodyfikuj ten fragment kodu tak żeby korzystał tylko z JPA - będzie wtedy super przykład.
    Co do obecnej postaci to nie jestem tutaj ekspertem i nie wchodziłem w source code, ale przypuszczam że hibernate implementuje JPA przy użyciu swojego natywnego API, i dlatego mieszanie obydwu API ma szanse działać prawidłowo.
    pozdro ŁZ.

    OdpowiedzUsuń
  13. witam, do kawałka kodu:



    dostaję błąd:
    "Invalid property 'sessionFactory' of bean class [org.springframework.orm.jpa.JpaTransactionManager]: Bean property 'sessionFactory' is not writable or has an invalid setter method. Does the parameter type of the setter match the return type of the getter?"
    Pogoglowałem trochę i zauważyłem, że wstrzykuje się co najwyżej entityManagera w settera transactionManagera. W doku też nie widzę settera do tej referencji. Czy to u mnie jest coś nie ok, że występuje ten błąd? Przyznam że może trochę inną konfigurację mam w zależnościach mavena - brałem najnowsze wersje bibliotek.

    Pozdrawiam,
    Łukasz

    OdpowiedzUsuń
  14. Kawałek kodu który ucięło to rejestracja "transactionManagera" w "web-data.xml" i wrzucanie do jego propertiesu "sessionFactory" - referencji "sessionFactory". Wolałem opisać słownie, żeby znowu nie ucięło.

    OdpowiedzUsuń
  15. Bardzo ciekawy i wyczerpujący kick-start do Springa. Malutka literówka: hibernate.user_sql_comments -> hibernate.use_sql_comments.

    OdpowiedzUsuń
  16. @Łukasz: rozumiem, że to u Ciebie ucięło, u mnie aplikacja działa poprawnie, proponuję ew. pobrać ją z repozytorium

    @Tomek N.: Dzięki, poprawione

    OdpowiedzUsuń
  17. [...] http://darekzon.com/2010/05/spring-framework-3-0-tutorial-%E2%80%93-cz-2-%E2%80%93-baza-danych-walid... [...]

    OdpowiedzUsuń
  18. A jak wygląda skrypt tworzący bazę danych?

    OdpowiedzUsuń
  19. Jestem w tym temacie Springa początkujący ale wydaje mi się że nie powinno zakładać się ograniczenia 20 znaków na hasło które potem jest ustawiane jako sha256 (64 znaki). U mnie prezentowany kod nie działał z tego powodu.

    Rozwiązałem to tak :
    w Account.java

    @Basic
    @Column(name="password", length=64)

    String passwordSha;

    public String getPasswordSha() {
    return passwordSha;
    }

    public void setPasswordSha(String passwordSha) {
    this.passwordSha = passwordSha;
    }

    @Transient
    @Size(min = 8, max = 20)
    String password;

    w AccountServiceImpl.java
    account.setPasswordSha(passwordEncoder.encodePassword(account
    .getPassword(), saltSource));

    Pozdrawiam

    OdpowiedzUsuń
  20. Racja, ale w tym przypadku @Size oznacza, że hasło wpisywane przez użytkownika powinno mieścić się w zakresie 8 do 20 znaków, ale pole w bazie danych może mieć odpowiednio większy rozmiar jak to jest w adnotacji dla atrybutu "passwordSha" (adnotacja @Column, atrybut length)

    OdpowiedzUsuń
  21. Witam,
    na zrzucie z błędami formularza są pola z polskim tekstem i błędy walidacji w języku angielskie. Jakie są sposoby żeby walidacje zrobić w języku polskim?

    OdpowiedzUsuń
  22. Zauważ, że w pliku application-context.xml (konfiguracji spring-a) dodałem bean "messageSource" który odpowiada za trzymanie tłumaczeń.
    Jeśli umieścisz w nim odpowiednie wpisy formularze powinny się poprawnie tłumaczyć. Więcej informacji o tłumaczeniach znajdziesz pod adresem:

    http://static.springsource.org/spring/docs/3.1.x/spring-framework-reference/html/validation.html#validation-conversion

    OdpowiedzUsuń
  23. Nie ma przypadkiem jakiś domyślnym definicji klas Admin i Role które są używane przez spring security ?

    OdpowiedzUsuń
  24. Jeśli dobrze pamiętam to nie ma takich klas, a to pewnie dlatego, że Spring nie jest uzależniony od konkretnej bazy lub ORM-a

    OdpowiedzUsuń
  25. Czy mógł byś wyjaśnić problem o którym napisał Bartek w komentarzu z Luty 12, 2012 at 21:33. Chodzi o to, że gdy @Size posiada wartość max=20 nie można zapisać do bazy shaszowanego hasła ponieważ @Valid sprawdza je jeszcze raz przed zapisaniem do bazy i wyrzuca błąd:

    http://pastebin.com/raw.php?i=MssKbfrC

    W bazie oczywiście ustawiłem aby pole password miało wielkość 64. Można to obejść stosując poradę Bartka ale jest ciekawy jak sprawiłeś, że u Ciebie to działa.

    OdpowiedzUsuń
  26. Racja, może się tak zdarzyć gdyż Hibernate wywołuje drugi raz walidację tuż przed wykonaniem zapisu do bazy (aczkolwiek nie wyświetlana jest tedy wiadomość błędu w formularzach), najlepiej zwiększyć rozmiar pola w którym jest przechowywane hasło

    OdpowiedzUsuń
  27. Witam,
    Postępuję dokładnie jak piszesz, tylko przy stawaniu apacha dostaję takiego errora:

    Error creating bean with name 'transactionManager' defined in ServletContext resource [/WEB-INF/web-data.xml]: Error setting property values; nested exception is org.springframework.beans.NotWritablePropertyException: Invalid property 'sessionFactory' of bean class [org.springframework.orm.jpa.JpaTransactionManager]: Bean property 'sessionFactory' is not writable or has an invalid setter method. Does the parameter type of the setter match the return type of the getter?

    Orientujecie się jak to zwalczyć ?

    OdpowiedzUsuń
  28. Witam. Wydaje mi się, że znalazłem - cieżki to wykrycia - bład. Nie wiem czy wynika to z jakiś zmian wersji spirnga ale :

    w AccountServiceImpl, podczas tworzenia użytkownika mamy :

    account.setPassword(passwordEncoder.encodePassword(account.getPassword(), saltSource));

    Tu jest błąd - do funkcji encodePassword w PasswordEncoder ma trafić już gotowa sól (w sesnie string jakiś z usera - jesgo id lub login), a nie obiekt solący :

    encodePassword(String rawPass, Object salt)

    Niestety sól jest ogólnego typu Object - zatem nie ma błędu.
    Niestety w bazie danych hasło zakładanego konta nie jest solone.
    W następnym tutorialu (cześć 3cia) hasło jest już sprawdzane z prawidłową solą (poprzez spring.security). Wówczas za cholere nie chcę się zalogować - hasła się różnią :)

    Trudno to wykryć - dopiero po analizie wpisów w bazie i faktu, że dla takich samych haseł hashe są takie same.

    OdpowiedzUsuń