Interacțiunea cu baza de date
Pe lângă logica aplicației, trebuie să existe persistența datelor asupra cărora se efectuează logica efectivă. În acest sens, majoritatea aplicațiilor folosesc baze de date. Pentru a simplifica interacțiunea programelor cu baza de date, au fost implementate ORM-uri (Object-Relational Mapping). Acestea sunt framework-uri care realizează o corespondență între tabelele și tipurile de date din baza de date cu obiectele, numite entități, și tipurile declarate în codul aplicației.
ORM-urile expun în general o interfață generică care poate fi folosită pentru mai multe baze de date (cum ar fi PostgreSQL, MariaDB sau MySQL), utilizând același cod, chiar dacă pot exista particularizări pentru fiecare. În Spring Boot, ORM-ul utilizat se numește Hibernate și este integrat cu Spring Data JPA. Interfața generică este expusă printr-un context de bază de date gestionat de Hibernate. Contextul nu este altceva decât un client pentru baza de date, care serializează/deserializează cereri și obiecte în comunicarea cu baza de date și servește drept cache pentru entități.
Vă recomandăm să utilizați baze de date SQL; majoritatea aplicațiilor nu vor avea nevoie de baze de date NoSQL, iar bazele de date tradiționale vor satisface cel mai probabil nevoile voastre.
Inițializarea bazei de date
Pentru acest exemplu vom avea o abordare code-first, adică vom crea codul necesar pentru a inițializa baza de date.
Mai întâi, trebuie să adăugăm următoarele dependințe în pom.xml
(dacă folosim Maven):
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>
De asemenea, trebuie configurat fișierul application.properties
:
spring.datasource.url=jdbc:postgresql://localhost:5432/mydatabase
spring.datasource.username=myuser
spring.datasource.password=mypassword
spring.jpa.hibernate.ddl-auto=update
Definirea schemei bazei de date
Majoritatea logicii aplicației va fi dictată de schema de date. Pentru a începe dezvoltarea unei aplicații, prima etapă constă în definirea schemei bazei de date și a obiectivelor pe care doriți să le realizați cu ea. Dacă aceste aspecte sunt bine definite, implementarea logicii peste date va fi mult mai ușoară și va necesita mai puține modificări asupra aplicației.
Mai jos aveți un exemplu de corespondență a unor entități cu tabele din baza de date. Observați aici că entitățile reprezentate prin clase normale pot moșteni clase abstracte pentru a evita codul duplicat. Fiecare entitate va reprezenta o tabelă în baza de date, iar legăturile între entități, reprezentate prin proprietăți ce conțin tipul altor entități, se numesc proprietăți de navigare (navigation properties). Prin intermediul acestor proprietăți se vor realiza legăturile de cheie străină (foreign key).
Schema bazei de date este definită prin entități Java, adnotate cu @Entity
și mapate în Hibernate. Mai jos este un exemplu de entități și relațiile dintre ele:
import jakarta.persistence.*;
import java.time.LocalDateTime;
import java.util.Set;
@MappedSuperclass
public abstract class BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private String id;
private LocalDateTime createdAt = LocalDateTime.now();
private LocalDateTime updatedAt = LocalDateTime.now();
public void updateTime() {
this.updatedAt = LocalDateTime.now();
}
}
@Entity
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class User extends BaseEntity {
private String name;
private String email;
private String password;
@Enumerated(EnumType.STRING)
private UserRoleEnum role;
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
private Set<UserFile> userFiles;
}
@Entity
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class UserFile extends BaseEntity {
private String path;
private String name;
private String description;
@ManyToOne
@JoinColumn(name = "user_id", nullable = false)
private User user;
}

Repository și Service
Spring Data JPA oferă o interfață simplificată pentru interacțiunea cu baza de date prin JpaRepository
.
- Furnizează metode CRUD (Create, Read, Update, Delete) fără a fi nevoie să le implementăm manual.
- Permite definirea de metode personalizate bazate pe convenții de denumire.
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository<User, String> {}
public interface UserFileRepository extends JpaRepository<UserFile, String> {}
Un Service în Spring gestionează logica de business și face legătura dintre controler și repository. Acesta conține metodele care manipulează datele și aplică reguli specifice domeniului.
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public List<User> getAllUsers() {
return userRepository.findAll();
}
}
Explicație:
UserRepository
șiUserFileRepository
sunt interfețe care extindJpaRepository<T, ID>
, unde:T
este tipul entității (ex:User
).ID
este tipul cheii primare (ex:String
).
@Service
: Marchează această clasă ca un serviciu Spring, astfel fiind detectata si gestionata ca un bean.userRepository
: Este injectat în clasaUserService
folosind Dependency Injection (DI) prin constructor.getAllUsers()
: ApeluluserRepository.findAll()
returnează o listă cu toți utilizatorii din baza de date.
Nu avem nevoie să implementăm manual metodele precum findAll()
, save()
, deleteById()
, deoarece sunt oferite automat de JpaRepository
.
Intr-un context real al unei aplicatii, vom evita folosirea metodei findAll()
. De ce?
Migrări cu Flyway
Când implementați o schemă de bază de date, aceasta poate suferi diverse modificări în timpul dezvoltării și maturizării aplicației. Din acest motiv, modificările la schema de bază de date ar trebui să fie efectuate incremental, adică orice schimbare se aplică peste modificările anterioare. De aceea, există conceptul de migrare. O migrare este o transformare, adesea reversibilă, a schemei bazei de date care să reflecte schimbările din cod.
Pentru a menține schema bazei de date, folosim Flyway, adaugand in pom.xml
dependinta:
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
Creăm fișierul SQL src/main/resources/db/migration/V1__init.sql
:
CREATE TABLE users (
id UUID PRIMARY KEY,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
role VARCHAR(255) NOT NULL,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL
);
Întotdeauna creați migrări în dezvoltarea proiectelor și nu neglijați importanța lor. Acest lucru vă ajută din două puncte de vedere: automatizați procesul de modificare a bazei de date și puteți urmări modificările de-a lungul istoricului aplicației pentru a detecta eventuale erori care pot apărea datorită unei modificări.
Aplicarea unei migrări poate eșua dacă constrângerile pe coloane sunt încălcate. De exemplu, dacă este pusă condiția de not null pe o coloană existentă și în baza de date există înregistrări cu NULL pe acea coloană, migrarea va eșua.
Trebuie să fiți conștienți că anumite modificări asupra bazei de date sunt ireversibile, cum ar fi ștergerea unor tabele sau coloane. Înainte de a aplica o migrare, faceți un backup la baza de date.
Manipulara datelor cu JPA
În Spring Data JPA, putem citi date prin:
1. Derived Query Method
Nu trebuie scris cod SQL de mana si este simplu de utilizat dar este si limitat pentru interogari complexe:
public interface UserRepository extends JpaRepository<User, Long> {
User findByEmail(String email);
}
2. JPQL
Flexibil, permite relatii intre entitati si este recomandat pentru interogari complexe dar este generalizat pentru ANSI SQL, ceea ce inseamna ca nu poate sa foloseasca functii specifice DBMS-ului folosit:
public interface UserRepository extends JpaRepository<User, Long> {
@Query("SELECT u FROM User u WHERE u.email = :email")
User getUserByEmail(@Param("email") String email);
}
3. SQL Nativ
Ofera control total dar este dependent de baza de date:
@Query(value = "SELECT * FROM users WHERE email = :email", nativeQuery = true)
User getUserByEmailNative(@Param("email") String email);
Adăugarea unui utilizator:
public User addUser(User user) {
return userRepository.save(user);
}
Ștergerea unui utilizator:
public void deleteUser(String userId) {
userRepository.deleteById(userId);
}