FetchType, SessionFactory

Postanowiłem zmniejszyć obciążenie bazy danych. Za każdym razem jak szło zapytanie do bazy (np chciałem pobrać wszystkie choroby – Disease.class) to JPA puszczał też zapytanie dotyczące innych typów nieprostych – np listy leków List, które są przypisane do tej choroby. Działo się tak bo w deklaracji encji ustawione było


@ManyToMany(fetch = FetchType.EAGER)
private List<Medicament> medicaments;

ale dzięki temu jak przekazałem obiekt do widoku do jsp to mogłem w łatwy sposób dostać się do leków przypisanych do choroby


<c:forEach items="${disease.medicaments}" var="medicament">
	${medicament.name}
</c:forEach>

W momencie jak chcę wyświetlić choroby i nic więcej to nie ma potrzeby puszczać tylu zapytać do bazy. Więc zmieniłem FetchType na LAZY. Spowodowało to, że JPA puszcza zapytanie dotyczące obiektu który jest w zapytaniu.

JPA niestety uniemożliwia zmianę FetchType w trakcie działania, np encja ma ustawione LAZY ale puszczam zapytanie z EAGER – przynajmnije nie mogłem znaleźć informacji na ten temat.

Stwierdziłem więc, że w JPA pobiorę dwa obiekty i wrzucę je do tablicy, a tę tablicę do listy


@Query("select d, m from Disease d LEFT OUTER JOIN d.medicaments m")
List<Object[]> findWithMedicaments();

Poszło jedno zapytanie do bazy! Teraz mogę wyciągnąć z tablicy obiekty Dosease i Medicament


List<Object[]> diseasesObjects = diseaseRepository.findWithMedicaments();
for (Object objects[] : diseasesObjects) {
	Disease disease = (Disease) objects[0];
	if(objects[1] != null)
	{
		Medicament medicament = (Medicament) objects[1];
		disease.addMedicament(medicament);
	}
}

ale niestety rzuca błedem:

Request processing failed; nested exception is org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: pl.tomo.entity.Disease.medicaments, could not initialize proxy - no Session

Pomyślałem, że utworzę drugą listę w Disease

@ManyToMany(fetch = FetchType.LAZY)
private List<Medicament> medicaments;	//pierwsza lista
@Transient
private List<Medicament> medicaments2;	//druga lista

i zmienię metodę addMedicament żeby wrzucała Disease do listy która jest Transient(nie jest w bazie danych). I to działa!

Następnie pomyślałem, że na pewno musi istnieć jakiś sposób żeby puścić zapytanie i dostać powiązane obiekty, mimo ustawienia FetchType.LAZY. Z pomocą przyszedł stackoverflow

Dodałem beana:

	<bean id="sessionFactory"
		class="org.springframework.orm.hibernate4.LocalSessionFactoryBean">
		<property name="dataSource" ref="dataSource"></property>
		<property name="hibernateProperties">
			<props>
				<prop key="hibernate.dialect">org.hibernate.dialect.MySQL5Dialect</prop>
				<prop key="hibernate.show_sql">true</prop>
			</props>
		</property>
		<property name="packagesToScan" value="pl.tomo.entity" />
	</bean>

Wstrzyknąłem go do serwisu

@Service
public class DiseaseService {
	@Autowired
	private SessionFactory sessionFactory;
	public void test(){
		Session openSession = sessionFactory.openSession();
		Query query = openSession.createQuery("Select d from Disease d");
		List<Disease> list = query.list();
		for (Disease disease : list) {
			Hibernate.initialize(disease.getMedicaments());
		}
		openSession.close();
}

List list = query.list() puszcza Hibernate: select disease…
Hibernate.initialize(disease.getMedicaments()) puszcza dla każdej pozycji z listy Hibernate: select medicament…

Refaktoring dodawania leków

Za pierwszym razem przy dodawania leku chciałem użyć jakiegoś autocompleta jquery do wyszukiwania leków. Problem polegał na tym, iż za każdym razem jak user coś wpisywał to json musiałbyć pobierany na nowo. Jak wolno wpisywał to długo program odpowiadał – coś było nie tak. Korzystałem z jakiegoś gotowca.

Postanowiłem to poprawić i stworzyłem na nowo całego forma do dodawania leku.

<div id="page-content-wrapper">
<form:form method="POST" modelAttribute="medicament" action="add.html" id="form">
<form:label path="name">Wpisz nazwę leku i kliknij w lupkę (min 3 znaki)</form:label>
<div class="form-group" id="idMedicamentSearchFormGroup">
<div class="input-group">
<span class="input-group-addon" id="searchButt">
<span class="glyphicon glyphicon-search"></span>
</span>
<form:input path="name" id="search" type="search" name="search" placeholder="Wpisz nazwę leku (min 3 znaki)" class="form-control"/>
<form:errors path="name" cssClass="form-error" />
</div>
<form:hidden path="idMedicamentDb" id="hiddenId" />
<form:errors path="idMedicamentDb" cssClass="form-error text-danger" />
</div>
<table class="table table-striped" id="table" hidden="true"></table>
<form:label path="dateStringExpiration">Wybierz datę ważności klikająć w kalendarz</form:label>
<div class="form-group">
<div class="input-group">
<label for="expirationDateValue" class="input-group-addon btn"><span class="glyphicon glyphicon-calendar"></span></label>
<form:input path="dateStringExpiration" cssClass="datepicker form-control" placeholder="Kliknij kalendarz" id="expirationDateValue"/>
</div>
<form:errors path="dateStringExpiration" cssClass="form-error text-danger" />
</div>
<h5 hidden="true">Wybrany lek</h5>
<table class="table table-bordered text-left" id=table-choosed-medicament></table>
<input type="submit" value="Dodaj lek" Class="btn btn-default" />
</form:form>
</div>

Bez tytułu

Teraz user musi wpisać min 3 znaki (jak mniej to dostanie czerwone info). Jak nic nie znajdzie to dostanie czerwone info. Jak będą błędne dane wprowadzone to czerwone info.Bez tytułuŻeby nie obciążać bazy pierwsza walidacja jest w javascript.

Wyniki wyszukiwania pokazane są w tabelce. User zaznaczając pozycję w input:hidden wrzuca id leku. Przyjazne dla usera pola wrzucane są w tabelkę nad przyciskiem dodajBez tytułu

W kontrolerze nic nie było zmieniane – zmiany tylko w html i duuuuże zmiany w javascript.

Funkcja do zaznaczania pozycji na liście wyników:

var expirationDate = '';
$(document).ready(function() {
$('#table').on('click', '.tableRow', function(){
$('.to-remove').remove();
$('#hiddenId').val($(this).children('.medicament-list-id').html());
$(this).addClass('info');
$(this).siblings().removeClass('info');
$('h5').show();
$('#table-choosed-medicament').append("
<tr class=\"to-remove\">
<td class=\"col-md-2\">Nazwa</td>
<td>" + $(this).children('.medicament-list-name').html() + "</td>
</tr>
" +
"
<tr class=\"to-remove\">
<td class=\"col-md-2\">Producent</td>
<td>" + $(this).children('.medicament-list-producent').html() + "</td>
</tr>
" +
"
<tr class=\"to-remove\">
<td class=\"col-md-2\">Rodzaj</td>
<td>" + $(this).children('.medicament-list-kind').html() + "</td>
</tr>
" +
"
<tr class=\"to-remove\">
<td class=\"col-md-2\">Cena</td>
<td>" + $(this).children('.medicament-list-price').html() + "</td>
</tr>
" +
"
<tr class=\"to-remove\">
<td class=\"col-md-2\">Data ważności</td>
<td id=\"expirationDateId\">" + expirationDate + "</td>
</tr>
");
});
});

i funkcja do szukania leków wraz z walidacją

$(function() {
$('#searchButt').click(function() {
$('.to-remove').remove();
$('.tableRow').remove();
$('.table-row-header').remove();
$("[for=hiddenId]").remove();
$('#hiddenId').val('');
$('h5').hide();
var addedTableTitle = false;
if($('#search').val().length >= 3)
{
$.getJSON("medicaments-db.json", function(result)
{
$.each(result, function(i, medicament)
{
if(medicament.name.toLowerCase().indexOf($('#search').val().toLowerCase()) >= 0)
{
if(i >= 0 & addedTableTitle == false)
{
addedTableTitle = true;
$('#table').append("
<tr class=\"table-row-header\">
<th>Nazwa</th>
<th>Producent</th>
<th>Opakowanie</th>
<th>Cena</th>
</tr>
");
}
$('#idMedicamentSearchFormGroup').removeClass('has-error');
$('#table').append("
<tr class=\"tableRow\">
<td class=\"medicament-list-name\">" + medicament.name +
"</td>
<td class=\"medicament-list-id\" hidden=\"true\">" + medicament.id +
"</td>
<td class=\"medicament-list-producent\">" + medicament.producent +
"</td>
<td class=\"medicament-list-kind\">" + medicament.kind +
"</td>
<td class=\"medicament-list-price\">" + medicament.price + "</td>
</tr>
");
return;
}
});
if(addedTableTitle == false)
{
$('#idMedicamentSearchFormGroup').append("<span class=\"help-block tableRow\">Nic nie znaleziono</span>")
$('#idMedicamentSearchFormGroup').addClass('has-error');
}
});
}
else
{
$('#idMedicamentSearchFormGroup').append("<span class=\"help-block tableRow\">Wpisz minimun 3 znaki</span>")
$('#idMedicamentSearchFormGroup').addClass('has-error');
}
$('#table').show();
});
})

 

Rejestracja usera z confirmem na mejla

Poprawiłem rejestrację usera – dodałem konieczność potwierdzenia emaila.

User po rejestracji otrzymuje na mejla link aktywacyjny.

Dodałem do encji User


private String uniqueID;

 private boolean active;

do metody która rejestruje usera dodałem

user.setUniqueID(UUID.randomUUID().toString());
emailService.sendEmail(user.getEmail(), user.getUniqueID());

EmailService

@Service
public class EmailService{

 @Autowired
 private Email email;

 public void sendEmail(String sendTo, String uniqueID) {

 Session session = Session.getInstance(email.getProps(),
 new javax.mail.Authenticator() {
 protected PasswordAuthentication getPasswordAuthentication() {
 return new PasswordAuthentication(email.getUser(), email.getPassword());
 }
 });
 try {

 Message message = new MimeMessage(session);
 message.setFrom(new InternetAddress("tomasz.molenda.autoguard.yt@gmail.com"));
 message.setRecipients(Message.RecipientType.TO,
 InternetAddress.parse(sendTo));
 message.setSubject("Rejestracja w serwisie Mediciline");
 message.setText("Witaj!,"
 + "http://localhost:8080/confirm/" + uniqueID + ".html");

 Transport.send(message);

 System.out.println("Done");

 } catch (MessagingException e) {
 throw new RuntimeException(e);
 }

 }

Email

public class Email {

 private String user;

 private String password;

 private String sendTo;

 private String sendUniqueID;

 private Properties props;

 public Email(String user, String password) {
 this.user = user;
 this.password = password;
 props = new Properties();
 props.put("mail.smtp.auth", "true");
 props.put("mail.smtp.starttls.enable", "true");
 props.put("mail.smtp.host", "smtp.gmail.com");
 props.put("mail.smtp.port", "587");
 }
}

oraz dodałem beana

 &lt;bean id="mail" class="pl.tomo.provider.Email"&gt;
 &lt;constructor-arg name="user" value="${mail.user}"/&gt;
 &lt;constructor-arg name="password" value="${mail.password}"/&gt;
 &lt;/bean&gt;</pre>
<pre>

Logika wysyłająca użyta z mkyong


W końcu po paru dniach udało mi się pokonać przeszkodę – dodawanie leków do choroby.

Wymyśliłem taką funkcjonalność, że użytkownik będzie mógł do choroby przypisać konkretne leki. Po naciśnięciu buttona dodaj pojawia się lista wszystkich leków z checkboxami, user zaznacza te które chce dodać i daje submit.

Cały czas coś się wykrzaczało ale w końcu z pomocą Shalom forum.4programmers.net udało się rozwiązać problem.

 

MedicamentForm


public class MedicamentForm {

private List<Medicament> medicaments;

private List<Integer> ids;

private int diseaseId;
}

Medicament

@Entity
public class Medicament {

@Id
@GeneratedValue
private int id;

private int liczba;

private String name;

@ManyToOne
@JoinColumn(name = "user_id")
private User user;

@Transient
private String dateStringOpen;
@Transient
private String dateStringExpiration;
@Transient
private String dateStringEnd;

@Temporal(TemporalType.DATE)
private Date dateOpen;

@Temporal(TemporalType.DATE)
private Date dateExpiration;

@Temporal(TemporalType.DATE)
private Date dateEnd;

@ManyToOne
private MedicamentDb medicamentDb;

@ManyToMany(mappedBy="medicaments", fetch = FetchType.EAGER)
private List<Disease> disease;

MedicamentDb

@Entity
public class MedicamentDb {
@Id
@GeneratedValue
private int id;
private String name = "";
private String producent = "";
private double cena = 0;
private String kind = "";

@Transient
private String description = "";

@JsonIgnore
@OneToMany(mappedBy = "medicamentDb")
private List<Medicament> listLek;
}

controller


 @RequestMapping(value = "/addmedicaments/{id}")
 public ModelAndView addMedicaments(@PathVariable int id, Principal principal)
 {
 ModelAndView mav = new ModelAndView("diseaseAddMedicaments");
 String name = principal.getName();
 User user = userService.findByName(name);
 MedicamentForm medicamentForm = new MedicamentForm();
 List&lt;Medicament&gt; list = medicamentService.findByUser(user);
 medicamentForm.setMedicaments(list);
 medicamentForm.setDiseaseId(id);
 mav.addObject("medicamentForm", medicamentForm);
 return mav;

 }

 @RequestMapping(value = "/addmedicaments/do")
 public ModelAndView addMedicamentsSubmit(@ModelAttribute("medicamentForm") MedicamentForm medicamentForm, Principal principal)
 {
 List&lt;Integer&gt; ids = medicamentForm.getIds();
 int diseaseId = medicamentForm.getDiseaseId();
 Disease disease = diseaseService.findById(diseaseId);
 List&lt;Medicament&gt; medicaments = medicamentService.findByDisease(disease);
 for (Integer id : ids) {
 Medicament medicament = medicamentService.findById(id);
 medicaments.add(medicament);
 }
 disease.setMedicaments(medicaments);
 diseaseService.save(disease);
 ModelAndView mav = new ModelAndView("redirect:/disease/list.html");
 return mav;
 }

jsp


<form:form method="POST" modelAttribute="medicamentForm" action="do.html">
<div class="table-responsive">
<table id="myTable"
class="table table-bordered table-hover table-striped">
<thead>
<tr>
<td>dodaj</td>
<td>nazwa leku</td>
<td>opakowanie</td>
<td>data waznosci</td>
<td>Producent</td>
</tr>
</thead>
<tbody>
<c:forEach items="${medicamentForm.medicaments}" var="medicament" varStatus="status">
<tr>
<td><form:checkbox path="ids" value="${medicament.id}"/><input name="diseaseId" type="hidden" value="${medicamentForm.diseaseId}"/></td>
<td>${medicament.medicamentDb.name}</td>
<td>${medicament.medicamentDb.kind}</td>
<td>${medicament.dateExpiration}</td>
<td>${medicament.medicamentDb.producent}</td>
</tr>
</c:forEach></tbody>
</table>
</div>
<input type="submit" value="Dodaj leki" Class="btn btn-default" />
</form:form>

Refactoring i git

Wrzuciłem projekt na githuba – wcześniej wrzuciłem z danymi do bazy (sic!)!!

Teraz ogarnąłem database.properties w taki sposób, że dodałem beana


<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="locations" value="classpath:/properties/database.properties"/>
</bean>

plik wrzuciłem src/main/resources/properties

src/main/resources jest moim classpathem

Zawartość pliku:


database.driver=com.mysql.jdbc.Driver
database.url=jdbc:mysql://domain.pl:3306/db
database.username=username
database.password=password

i bean dataSoure wygląda tak:


<bean class="org.apache.commons.dbcp.BasicDataSource" id="dataSource">
<property name="driverClassName" value="${database.driver}" />
<property name="url" value="${database.url}" />
<property name="username" value="${database.username}" />
<property name="password" value="${database.password}" />
<property name="validationQuery" value="SELECT 1" />
</bean>

Dodatkowo zrobiłem refactoring kontrolera MedicamentController – poczyściłem z syfu. Powoli zaczynam rozumieć RequestMappinp, RequestMethod, zwracanie z jsp do metody…

 

Pierwszy post

Bloga założyłem ze względu na konkurs daj się poznać. Pewnie i tak tego nikt nie czyta poza osobą związaną z konkursem 🙂

Idąc dalej – na blogu będę zamieszczał informacje z postępów z moją pierwszą aplikacją: mediciline – aplikacja do zarządzania lekami w domu. Skąd pomysł? Wielokrotnie z żoną jak byliśmy u lekarza z dzieckiem pytani byliśmy czy mamy to i to w domu – a my – ehhh – kiedyś było. Wpadł mi pomysł, żeby zrobić aplikację, która pomogłaby w zarządzaniu lekami.