Podaci iz prethodne lekcije nestaju kada se server ugasi. Ovde se čuvaju u bazi podataka. Spring Data JPA pretvara Java klase u tabele i sam piše SQL upite – dovoljno je napisati interfejs. Na kraju je objašnjeno kako se aplikacija pakuje i postavlja na internet.
Pre nego što počneš: potrebno predznanje – REST API, isti pojmovi u Django-u (model, migracije). Kako da učiš: radi u svom projektu korak po korak i posle svakog koraka pokreni server i proveri rezultat u pregledaču. Primeri su za Spring Boot 3 ili 4 i Javu 17+; svaki deo koda upiši u fajl naveden u komentaru na vrhu primera.
Dodavanje biblioteka
Na start.spring.io (ili u IntelliJ-u: desni klik na pom.xml → Add Dependency) dodaju se još dve zavisnosti:
Zavisnost
Šta daje
Spring Data JPA
rad sa bazom preko Java klasa (Hibernate)
H2 Database
mala baza napisana u Javi; čuva se u fajlu u fascikli projekta – ne instalira se ništa
Kasnije se H2 lako zamenjuje bazom PostgreSQL ili MySQL – menjaju se samo zavisnost i podešavanja, kod ostaje isti.
# src/main/resources/application.properties
spring.application.name=zadaci
# baza u fajlu ./data/zadaci.mv.db (pravi se sama)
spring.datasource.url=jdbc:h2:file:./data/zadaci
spring.datasource.username=sa
spring.datasource.password=
# Hibernate pravi i menja tabele prema klasama
spring.jpa.hibernate.ddl-auto=update
# u konzoli se ispisuju SQL upiti – korisno za učenje
spring.jpa.show-sql=true
Entitet: klasa koja postaje tabela
Klasa Zadatak dobija anotaciju @Entity. Polje sa @Id je primarni ključ, a @GeneratedValue znači da baza sama dodeljuje brojeve. Geteri, seteri i prazan konstruktor ostaju kao u prethodnoj lekciji:
// src/main/java/rs/svetprogramiranja/zadaci/Zadatak.java
package rs.svetprogramiranja.zadaci;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import java.time.LocalDate;
@Entity // tabela ZADATAK
public class Zadatak {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 200)
private String naslov;
private String predmet;
private LocalDate rok;
private boolean uradjen;
public Zadatak() { }
public Zadatak(String naslov, String predmet, LocalDate rok) {
this.naslov = naslov;
this.predmet = predmet;
this.rok = rok;
}
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getNaslov() { return naslov; }
public void setNaslov(String naslov) { this.naslov = naslov; }
public String getPredmet() { return predmet; }
public void setPredmet(String predmet) { this.predmet = predmet; }
public LocalDate getRok() { return rok; }
public void setRok(LocalDate rok) { this.rok = rok; }
public boolean isUradjen() { return uradjen; }
public void setUradjen(boolean uradjen) { this.uradjen = uradjen; }
}
Pri pokretanju Hibernate pravi tabelu (u konzoli se vidi create table zadatak (...)). Svaki objekat je jedan red tabele, a svako polje jedna kolona.
Repozitorijum: interfejs bez koda
Za pristup tabeli dovoljno je napisati interfejs koji nasleđuje JpaRepository<Zadatak, Long> (tip entiteta i tip ključa). Spring pri pokretanju sam pravi klasu koja ga ostvaruje. Dobijaju se gotove metode findAll, findById, save, deleteById, count…, a nove se dodaju samo imenom metode:
// src/main/java/rs/svetprogramiranja/zadaci/ZadatakRepository.java
package rs.svetprogramiranja.zadaci;
import java.time.LocalDate;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ZadatakRepository extends JpaRepository<Zadatak, Long> {
// Spring iz imena metode pravi upit:
// SELECT * FROM zadatak WHERE uradjen = false ORDER BY rok
List<Zadatak> findByUradjenFalseOrderByRokAsc();
// WHERE upper(predmet) = upper(?)
List<Zadatak> findByPredmetIgnoreCase(String predmet);
// WHERE upper(naslov) LIKE upper('%' || ? || '%')
List<Zadatak> findByNaslovContainingIgnoreCase(String deo);
// WHERE rok < ? AND uradjen = false
long countByRokBeforeAndUradjenFalse(LocalDate datum);
}
Deo imena
Značenje u upitu
findBy… / countBy…
SELECT / SELECT COUNT
…And…, …Or…
više uslova
Containing, StartingWith
LIKE '%x%', LIKE 'x%'
Before, After, GreaterThan
<, >
IgnoreCase
bez obzira na velika i mala slova
OrderBy…Asc/Desc
ORDER BY
Servis koji koristi bazu
Servis sada umesto mape koristi repozitorijum. Kontroler iz prethodne lekcije ostaje potpuno isti – to je korist od podele na slojeve.
// src/main/java/rs/svetprogramiranja/zadaci/ZadatakService.java
package rs.svetprogramiranja.zadaci;
import java.util.List;
import java.util.Optional;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
@Service
public class ZadatakService {
private final ZadatakRepository repo;
public ZadatakService(ZadatakRepository repo) {
this.repo = repo;
}
public List<Zadatak> svi() {
return repo.findAll(Sort.by("rok")); // SELECT ... ORDER BY rok
}
public Optional<Zadatak> nadji(long id) {
return repo.findById(id);
}
public Zadatak dodaj(Zadatak z) {
z.setId(null); // id dodeljuje baza
return repo.save(z); // INSERT
}
public Optional<Zadatak> izmeni(long id, Zadatak novi) {
return repo.findById(id).map(z -> {
z.setNaslov(novi.getNaslov());
z.setPredmet(novi.getPredmet());
z.setRok(novi.getRok());
z.setUradjen(novi.isUradjen());
return repo.save(z); // UPDATE
});
}
public boolean obrisi(long id) {
if (!repo.existsById(id)) return false;
repo.deleteById(id); // DELETE
return true;
}
public List<Zadatak> pretraga(String deo) {
return repo.findByNaslovContainingIgnoreCase(deo);
}
}
Pokreni server, dodaj nekoliko zadataka, zaustavi server i pokreni ga ponovo – zadaci su i dalje tu. U fascikli projekta pojavio se fajl data/zadaci.mv.db.
Početni podaci i pretraga
Klasa koja ostvaruje CommandLineRunner i označena je sa @Component izvršava se jednom, posle pokretanja – zgodno za ubacivanje primera u praznu bazu:
// src/main/java/rs/svetprogramiranja/zadaci/PocetniPodaci.java
package rs.svetprogramiranja.zadaci;
import java.time.LocalDate;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
@Component
public class PocetniPodaci implements CommandLineRunner {
private final ZadatakRepository repo;
public PocetniPodaci(ZadatakRepository repo) {
this.repo = repo;
}
@Override
public void run(String... args) {
if (repo.count() == 0) { // samo kada je baza prazna
repo.save(new Zadatak("Uraditi zadatke 1-5", "Matematika", LocalDate.now().plusDays(2)));
repo.save(new Zadatak("Procitati lektiru", "Srpski", LocalDate.now().plusDays(7)));
repo.save(new Zadatak("Nauciti Njutnove zakone", "Fizika", LocalDate.now().plusDays(4)));
}
}
}
Pretraga kroz API: u kontroler se dodaje metoda koja poziva servis.pretraga:
// dodatak u klasi ZadatakController
// import org.springframework.web.bind.annotation.RequestParam;
// http://localhost:8080/api/zadaci/pretraga?q=lekt
@GetMapping("/pretraga")
public List<Zadatak> pretraga(@RequestParam String q) {
return servis.pretraga(q);
}
Pakovanje i postavljanje na internet
Cela aplikacija pakuje se u jedan .jar fajl, koji se pokreće na bilo kom računaru sa Javom:
Za postavljanje na internet koriste se servisi koji pokreću Java aplikacije ili Docker kontejnere (npr. Render, Railway, Fly.io). Najčešće je dovoljan ovakav Dockerfile u korenu projekta:
# Dockerfile – prvo se aplikacija prevodi, pa se samo .jar prenosi u manju sliku
FROM eclipse-temurin:21-jdk AS build
WORKDIR /app
COPY . .
RUN chmod +x mvnw && ./mvnw -q clean package -DskipTests
FROM eclipse-temurin:21-jre
WORKDIR /app
COPY --from=build /app/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
Na takvim servisima disk se obično briše pri svakom novom postavljanju, pa H2 baza u fajlu nije dobra za pravu upotrebu – tada se koristi PostgreSQL baza koju servis nudi, a podešavanja se zadaju promenljivim okruženja (SPRING_DATASOURCE_URL, SPRING_DATASOURCE_USERNAME, SPRING_DATASOURCE_PASSWORD). Isti postupak sa Python-om i Django-om opisan je u lekciji Postavljanje na internet.
Zadaci za vežbu
Za svaki zadatak postoji primer, uputstvo i rešenje.
ZADATAK 01Težina: teže
Predmeti i zadaci
Dodaj entitet Predmet (polja: id, naziv, nastavnik) sa repozitorijumom, i povezivanje: svaki zadatak pripada jednom predmetu (veza više-prema-jedan). Dodaj rutu GET /api/predmeti/{id}/zadaci koja vraća zadatke tog predmeta, sortirane po roku.
Uputstvo
U klasi Zadatak polje private Predmet predmetObj; sa anotacijom @ManyToOne. U ZadatakRepository dodaj metodu findByPredmetObjIdOrderByRokAsc(Long predmetId) – Spring razume i putanju kroz vezu (PredmetObj → Id).
Rešenje (Java)
// src/main/java/rs/svetprogramiranja/zadaci/Predmet.java
package rs.svetprogramiranja.zadaci;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
@Entity
public class Predmet {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String naziv;
private String nastavnik;
public Predmet() { }
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getNaziv() { return naziv; }
public void setNaziv(String naziv) { this.naziv = naziv; }
public String getNastavnik() { return nastavnik; }
public void setNastavnik(String nastavnik) { this.nastavnik = nastavnik; }
}
// src/main/java/rs/svetprogramiranja/zadaci/PredmetRepository.java
package rs.svetprogramiranja.zadaci;
import org.springframework.data.jpa.repository.JpaRepository;
public interface PredmetRepository extends JpaRepository<Predmet, Long> { }
// dodatak u klasi Zadatak (+ getter i setter za predmetObj)
// import jakarta.persistence.ManyToOne;
@ManyToOne // više zadataka – jedan predmet (kolona predmet_obj_id)
private Predmet predmetObj;
// dodatak u ZadatakRepository
List<Zadatak> findByPredmetObjIdOrderByRokAsc(Long predmetId);
// src/main/java/rs/svetprogramiranja/zadaci/PredmetController.java
package rs.svetprogramiranja.zadaci;
import java.util.List;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/predmeti")
public class PredmetController {
private final PredmetRepository predmeti;
private final ZadatakRepository zadaci;
public PredmetController(PredmetRepository predmeti, ZadatakRepository zadaci) {
this.predmeti = predmeti;
this.zadaci = zadaci;
}
@GetMapping
public List<Predmet> svi() {
return predmeti.findAll();
}
@GetMapping("/{id}/zadaci")
public ResponseEntity<List<Zadatak>> zadaciPredmeta(@PathVariable long id) {
if (!predmeti.existsById(id))
return ResponseEntity.notFound().build();
return ResponseEntity.ok(zadaci.findByPredmetObjIdOrderByRokAsc(id));
}
}
Česte greške i saveti
Ako se promeni klasa entiteta, ddl-auto=update dodaje nove kolone, ali ne briše stare; za čist početak obriši fajl baze u fascikli data.
Ime metode u repozitorijumu mora tačno odgovarati imenima polja (findByNaslov… za polje naslov), inače aplikacija neće ni da se pokrene.
Lozinke za bazu ne upisuj u kod koji objavljuješ (GitHub) – zadaj ih kroz promenljive okruženja.
Za prave aplikacije sa nalozima korisnika koristi se Spring Security – ne pravi sopstveno čuvanje lozinki.
Proveri znanje
Otvori kviz
Spring Data JPA
1. Šta označava anotacija @Entity?
2. Ko piše klasu koja ostvaruje interfejs ZadatakRepository?
3. Koju operaciju izvršava repo.save(z) za nov objekat?
4. Zašto se kontroler nije menjao kada je dodata baza?
Podešavanja kolačića
Koristimo kolačiće da bismo vam pružili najbolje moguće iskustvo. Takođe nam omogućavaju da analiziramo ponašanje korisnika kako bismo stalno poboljšavali veb lokaciju za vas.