Spring Boot API
Spring Boot web services using JPA
https://github.com/gerardfp/demo
Database server
Màquina virtual
Descarrega aquesta imatge de disc dur virtual: debian-11-nocloud-amd64.qcow2
Crea una nova màquina virtual amb la imatge descarregada.
Inicia la màquina virtual i esbrina la seva IP.
Descarrega aquesta clau privada: id_rsa
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
-----BEGIN OPENSSH PRIVATE KEY-----
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
60XApYsxosgayPAAAACmdmYWxjb0BkMDY=
-----END OPENSSH PRIVATE KEY-----
Estableix permisos 600
al fitxer de la clau:
1
chmod 600 /path/to/id_rsa
Utilitza aquesta clau per a accedir a la màquina virtual per ssh:
1
ssh -i /path/to/id_rsa root@192.168.122.1
Docker Postgresql
Instal·la Docker a la màquina virtual:
1
curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh
Inicia un contenidor Docker amb la base de dades PostreSQL:
1
docker run -d --name myapi-postgres -e POSTGRES\_PASSWORD=mysecretpassword -e POSTGRES\_USER=myapidbadmin -e POSTGRES\_DB=myapidb -p 5432:5432 -v /root/db\_data:/var/lib/postgresql/data postgres
Habilita l’inici automàtic del contenidor:
/etc/systemd/system/myapi-postgres.service
1
2
3
4
5
6
7
8
9
10
11
12
[Unit]
Description=MyApi container
Requires=docker.service
After=docker.service
[Service]
Restart=always
ExecStart=/usr/bin/docker start -a myapi-postgres
ExecStop=/usr/bin/docker stop -t 2 myapi-postgres
[Install]
WantedBy=default.target
1
systemctl enable myapi-postgres.service
El següent pas és accedir al contenidor per a crear l’esquema de la base de dades.
Esbrina el CONTAINER ID
amb la comanda:
1
docker ps
Inicia un shell (bash) al contenidor amb la comanda:
1
docker exec -it 6516a4b4e3c2 bash
Accedeix al shell psql amb la comanda:
1
psql -U myapidbadmin myapidb
Copia i enganxa aquest script SQL al shell:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
DROP TABLE IF EXISTS movie, actor, genre, movie_actor, movie_genre CASCADE;
CREATE TABLE IF NOT EXISTS movie (
movieid uuid NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
title text,
imageurl text);
CREATE TABLE IF NOT EXISTS actor (
actorid uuid NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
name text,
imageurl text);
CREATE TABLE IF NOT EXISTS genre (
genreid uuid NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
label text);
CREATE TABLE IF NOT EXISTS movie_actor (
movieid uuid REFERENCES movie(movieid) ON DELETE CASCADE,
actorid uuid REFERENCES actor(actorid) ON DELETE CASCADE);
CREATE TABLE IF NOT EXISTS movie_genre (
movieid uuid REFERENCES movie(movieid) ON DELETE CASCADE,
genreid uuid REFERENCES genre(genreid) ON DELETE CASCADE);
INSERT INTO movie(title, imageurl) VALUES
('Movie One','movie1.jpg'),
('Movie Two','movie2.jpg'),
('Movie Three','movie3.jpg'),
('Movie Four','movie4.jpg');
INSERT INTO actor(name, imageurl) VALUES
('Actor One','actor1.jpg'),
('Actor Two','actor2.jpg'),
('Actor Three','actor3.jpg'),
('Actor Four','actor4.jpg'),
('Actor Five','actor5.jpg');
INSERT INTO genre(label) VALUES
('Genre One'),
('Genre Two'),
('Genre Three');
INSERT INTO movie_actor VALUES
((SELECT movieid FROM movie WHERE title='Movie One'),(SELECT actorid FROM actor WHERE name='Actor One')),
((SELECT movieid FROM movie WHERE title='Movie One'),(SELECT actorid FROM actor WHERE name='Actor Two')),
((SELECT movieid FROM movie WHERE title='Movie Two'),(SELECT actorid FROM actor WHERE name='Actor Three')),
((SELECT movieid FROM movie WHERE title='Movie Two'),(SELECT actorid FROM actor WHERE name='Actor Four')),
((SELECT movieid FROM movie WHERE title='Movie Three'),(SELECT actorid FROM actor WHERE name='Actor Four')),
((SELECT movieid FROM movie WHERE title='Movie Three'),(SELECT actorid FROM actor WHERE name='Actor Five')),
((SELECT movieid FROM movie WHERE title='Movie Four'),(SELECT actorid FROM actor WHERE name='Actor One')),
((SELECT movieid FROM movie WHERE title='Movie Four'),(SELECT actorid FROM actor WHERE name='Actor Four'));
INSERT INTO movie_genre VALUES
((SELECT movieid FROM movie WHERE title='Movie One'),(SELECT genreid FROM genre WHERE label='Genre One')),
((SELECT movieid FROM movie WHERE title='Movie One'),(SELECT genreid FROM genre WHERE label='Genre Two')),
((SELECT movieid FROM movie WHERE title='Movie Two'),(SELECT genreid FROM genre WHERE label='Genre One')),
((SELECT movieid FROM movie WHERE title='Movie Three'),(SELECT genreid FROM genre WHERE label='Genre One')),
((SELECT movieid FROM movie WHERE title='Movie Three'),(SELECT genreid FROM genre WHERE label='Genre Two')),
((SELECT movieid FROM movie WHERE title='Movie Three'),(SELECT genreid FROM genre WHERE label='Genre Three'));
Spring boot
Accedeix a spring initializr per a generar un projecte Spring Boot
Selecciona Gradle Project
Afegeix les dependències:
- Spring Web
- Spring Data JPA
- PostgreSQL Driver
Genera el projecte i descomprimeix-lo. Obre’l amb IntelliJ
Configura l’accés a la base de dades:
src/main/resources/application.properties
1
2
3
4
5
spring.datasource.url= jdbc:postgresql://192.168.122.99:5432/myapidb
spring.datasource.username= myapidbadmin
spring.datasource.password= mysecretpassword
spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation= true
spring.jpa.properties.hibernate.dialect= org.hibernate.dialect.PostgreSQLDialect
L’arquitectura bàsica de la nostra ApiHttp amb Spring Boot serà aquesta:
Model
Les classes Model serveixen per a crear objectes amb les dades i així poder transportar-les d’un component a un altre.
Començarem creant la classe Movie
que ens servirà per a transportar les dades d’una pel·lícula:
src/main/java/com/example/demo/domain/model/Movie.java
1
2
3
4
5
6
7
8
9
10
11
12
13
import javax.persistence.*;
import java.util.UUID;
@Entity
@Table(name = "movie")
public class Movie {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
public UUID movieid;
public String title;
public String imageurl;
}
Repository
src/main/java/com/example/demo/repository/MovieRepository.java
1
2
3
4
5
6
7
8
import com.example.demo.domain.model.Movie;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.UUID;
public interface MovieRepository extends JpaRepository<Movie, UUID> {
}
Controller
src/main/java/com/example/demo/controller/MovieController.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import com.example.demo.domain.model.Movie;
import com.example.demo.repository.MovieRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/movies")
public class MovieController {
@Autowired
private MovieRepository movieRepository;
@GetMapping("/")
public List<Movie> findAllMovies() {
return movieRepository.findAll();
}
@PostMapping("/")
public Movie createMovie(@RequestBody Movie movie) {
return movieRepository.save(movie);
}
}
Database migrations
La llibreria Flyway permet gestionar els canvis en l’esquema de la base de dades.
Agegeix Flyway al projecte:
build.gradle
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
plugins {
...
id 'org.flywaydb.flyway' version '8.0.2'
}
flyway {
configFiles = ['src/main/resources/application.properties']
}
dependencies {
...
implementation 'org.flywaydb:flyway-core:8.0.1'
}
Configura els paràmetres d’accés a la base de dades:
src/main/resources/application.properties
1
2
3
4
5
flyway.url=jdbc:postgresql://192.168.122.99:5432/myapidb
flyway.schemas=public
flyway.user=myapidbadmin
flyway.password=mysecretpassword
spring.flyway.baseline_on_migrate=true
Crea una primera versió de l’esquema de la base de dades. Les migracions de l’esquema de la base de dades es defineixen creant arxius al directori resources/db/migration/
. El nom d’aquests arxius ha de seguir una nomenclatura específica (veure: migrations#naming)
Crea l’arxiu resources/db/migration/V1__createdatabase.sql
:
resources/db/migration/V1__createdatabase.sql
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
CREATE TABLE IF NOT EXISTS movie (
movieid uuid NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
title text,
synopsis text,
imageurl text);
CREATE TABLE IF NOT EXISTS actor (
actorid uuid NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
name text,
imageurl text);
CREATE TABLE IF NOT EXISTS genre (
genreid uuid NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
label text);
CREATE TABLE IF NOT EXISTS movie_actor (
movieid uuid REFERENCES movie(movieid) ON DELETE CASCADE,
actorid uuid REFERENCES actor(actorid) ON DELETE CASCADE,
PRIMARY KEY (movieid, actorid));
CREATE TABLE IF NOT EXISTS movie_genre (
movieid uuid REFERENCES movie(movieid) ON DELETE CASCADE,
genreid uuid REFERENCES genre(genreid) ON DELETE CASCADE,
PRIMARY KEY (movieid, genreid));
;
INSERT INTO movie(title, synopsis, imageurl) VALUES
('Movie One','This is the One Movie','movie1.jpg'),
('Movie Two','The Two Movie is the next','movie2.jpg'),
('Movie Three','The Trilogy','movie3.jpg'),
('Movie Four','Four movies is too much','movie4.jpg');
INSERT INTO actor(name, imageurl) VALUES
('Actor One','actor1.jpg'),
('Actor Two','actor2.jpg'),
('Actor Three','actor3.jpg'),
('Actor Four','actor4.jpg'),
('Actor Five','actor5.jpg');
INSERT INTO genre(label) VALUES
('Genre One'),
('Genre Two'),
('Genre Three');
INSERT INTO movie_actor VALUES
((SELECT movieid FROM movie WHERE title='Movie One'),(SELECT actorid FROM actor WHERE name='Actor One')),
((SELECT movieid FROM movie WHERE title='Movie One'),(SELECT actorid FROM actor WHERE name='Actor Two')),
((SELECT movieid FROM movie WHERE title='Movie Two'),(SELECT actorid FROM actor WHERE name='Actor Three')),
((SELECT movieid FROM movie WHERE title='Movie Two'),(SELECT actorid FROM actor WHERE name='Actor Four')),
((SELECT movieid FROM movie WHERE title='Movie Three'),(SELECT actorid FROM actor WHERE name='Actor Four')),
((SELECT movieid FROM movie WHERE title='Movie Three'),(SELECT actorid FROM actor WHERE name='Actor Five')),
((SELECT movieid FROM movie WHERE title='Movie Four'),(SELECT actorid FROM actor WHERE name='Actor One')),
((SELECT movieid FROM movie WHERE title='Movie Four'),(SELECT actorid FROM actor WHERE name='Actor Four'));
INSERT INTO movie_genre VALUES
((SELECT movieid FROM movie WHERE title='Movie One'),(SELECT genreid FROM genre WHERE label='Genre One')),
((SELECT movieid FROM movie WHERE title='Movie One'),(SELECT genreid FROM genre WHERE label='Genre Two')),
((SELECT movieid FROM movie WHERE title='Movie Two'),(SELECT genreid FROM genre WHERE label='Genre One')),
((SELECT movieid FROM movie WHERE title='Movie Three'),(SELECT genreid FROM genre WHERE label='Genre One')),
((SELECT movieid FROM movie WHERE title='Movie Three'),(SELECT genreid FROM genre WHERE label='Genre Two')),
((SELECT movieid FROM movie WHERE title='Movie Three'),(SELECT genreid FROM genre WHERE label='Genre Three'));
Heroku deploy
Afegeix aquest arxiu de configuració del projecte:
src/main/resources/application-production.properties
1 2 3 4 5
spring.datasource.url=${JDBC_DATABASE_URL:} spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect server.port=${PORT:}
Les variables ${JDBC_DATABASE_URL:} i ${PORT:} són variables d’entorn proporcionades per Heroku.
Afegeix aquest arxiu al directori arrel del projecte. És la comanda que executarà Heroku per a iniciar l’aplicació:
Procfile
1
web: java -Dspring.profiles.active=production -jar build/libs/myapi-0.0.1-SNAPSHOT.jar
Amb l’opció -Dspring.profiles.active=production li diem que agafi l’arxiu de configuració application-production.properties
myapi-0.0.1-SNAPSHOT: El nom del projecte el trobaràs a l’arxiu settings.gradle, i la versió a l’arxiu build.gradle.
Publica el projecte a GitHub.
Crea un compte a Heroku
Crea una nova app a Heroku:
File uploads
Afegirem una migració de la base de dades per a crear una taula que emmagatzemi els arxius que carregin (upload)
Crea aquest arxiu de migració:
src/main/resources/db/migration/V2__filetable.sql
1
2
3
4
CREATE TABLE file (
fileid UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
contenttype TEXT,
data bytea);
Creem el model:
src/main/java/com/example/demo/domain/model/File.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import org.hibernate.annotations.Type;
import javax.persistence.*;
import java.util.UUID;
@Entity
@Table(name = "file")
public class File {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
public UUID fileid;
public String contenttype;
@Lob
@Type(type="org.hibernate.type.BinaryType")
public byte[] data;
}
Creem el repository:
src/main/java/com/example/demo/repository/FileRepository.java
1
2
3
4
5
6
7
8
import com.example.demo.domain.model.File;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.UUID;
public interface FileRepository extends JpaRepository<File, UUID> {
}
I per últim el controlador:
src/main/java/com/example/demo/controller/FileController.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import com.example.demo.domain.model.File;
import com.example.demo.repository.FileRepository;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
import java.util.UUID;
@RestController
@RequestMapping("/files")
public class FileController {
private final FileRepository fileRepository;
FileController(FileRepository fileRepository){
this.fileRepository = fileRepository;
}
@PostMapping
public String upload(@RequestParam("file") MultipartFile uploadedFile) {
try {
File file = new File();
file.contenttype = uploadedFile.getContentType();
file.data = uploadedFile.getBytes();
return fileRepository.save(file).fileid.toString();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
@GetMapping("/{id}")
public ResponseEntity<byte[]> getFile(@PathVariable UUID id) {
File file = fileRepository.findById(id).orElse(null);
if (file == null) return ResponseEntity.notFound().build();
return ResponseEntity.ok()
.contentType(MediaType.valueOf(file.contenttype))
.contentLength(file.data.length)
.body(file.data);
}
}
Autenticació i autorització
Crea aquest arxiu de migració:
src/main/resources/db/migration/V3__usertable.sql
1
2
3
4
5
6
7
8
9
10
11
12
13
CREATE TABLE usser (
userid uuid NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
username varchar(24) NOT NULL UNIQUE,
password varchar(255) NOT NULL,
role varchar(10),
enabled boolean DEFAULT true
);
-- afegim un usuari de prova
CREATE EXTENSION IF NOT EXISTS pgcrypto;
INSERT INTO usser (username, password) VALUES ('user', crypt('pass', gen_salt('bf')));
Afegeix la llibreria spring-boot-starter-security
:
build.gradle
1
2
3
4
5
6
dependencies {
...
implementation 'org.springframework.boot:spring-boot-starter-security'
}
Creem el model User
:
src/main/java/com/example/demo/domain/model/User.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import javax.persistence.*;
import java.util.UUID;
@Entity
@Table(name="usser")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
public UUID userid;
public String username;
public String password;
public String role;
public boolean enabled;
}
Creem el repository:
src/main/java/com/example/demo/repository/UserRepository.java
1
2
3
4
5
6
7
8
9
import com.example.demo.domain.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.UUID;
public interface UserRepository extends JpaRepository {
User findByUsername(String username);
}
Afegim la configuració de seguretat:
src/main/java/com/example/demo/SecurityConfig.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import javax.sql.DataSource;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired private DataSource dataSource;
@Bean
public BCryptPasswordEncoder getPasswordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().csrf().disable()
.authorizeRequests()
//.mvcMatchers("/users/register/")
// .permitAll()
.anyRequest()
.authenticated()
.and()
.httpBasic();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.jdbcAuthentication()
.dataSource(dataSource)
.usersByUsernameQuery("select username, password, enabled from usser where username = ?")
.authoritiesByUsernameQuery("select username, role from usser where username = ?")
.passwordEncoder(getPasswordEncoder());
}
}
Registre d’usuaris
Per al registre d’usuaris necessitarem una classe (DTO) que per guardar les dades que ens envia l’usuari (username i password):
src/main/java/com/example/demo/domain/dto/UserRegisterRequest.java
1
2
3
4
public class UserRegisterRequest {
public String username;
public String password;
}
També necessitarem un controlador per atendre les peticions de registre:
src/main/java/com/example/demo/controller/UserController.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import com.example.demo.domain.dto.UserRegisterRequest;
import com.example.demo.domain.model.User;
import com.example.demo.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/users")
public class UserController {
@Autowired private UserRepository userRepository;
@Autowired private BCryptPasswordEncoder passwordEncoder;
@PostMapping("/register")
public String register(@RequestBody UserRegisterRequest userRegisterRequest) {
if (userRepository.findByUsername(userRegisterRequest.username) == null) {
User user = new User();
user.username = userRegisterRequest.username;
user.password = passwordEncoder.encode(userRegisterRequest.password);
user.enabled = true;
userRepository.save(user);
return "OK"; // TODO
}
return "ERROR"; // TODO
}
}
Per últim caldrà permetre l’accés a usuaris no autenticats a l’endpoint de registre (/users/register):
src/main/java/com/example/demo/SecurityConfig.java .mvcMatchers(“/users/register/”) .permitAll()
ResponseEntity
Per a que els mètodes REST retornin respostes de forma adequada podem utilitzar la classe ResponseEntity
.
Aquesta classe té uns mètodes builder que ens permeten establir el HttpStatus, les capçaleres HTTP i el cos de la resposta.
Haurem d’implementar els mètdoes Mapping dels controlladors de forma que retornin un objecte de classe ResponseEntity<?>
Per exemple, en el següent codi retornem l’status 200 (OK) i afegim al cos de la resposta l’objecte movie
que tot just s’ha creat.
1
2
3
4
5
@PostMapping
public ResponseEntity<?> createMovie(@RequestBody Movie movie, Authentication authentication) {
Movie movie = movieRepository.save(movie);
return ResponseEntity.ok().body(movie);
}
L’objecte movie
que hem posat al body()
es serialitzarà a dades JSON així:
1
2
3
4
5
{
"movieid": "c4806e2b-2e19-4e32-a7cd-8ead8b32350e",
"title": "Movie Title",
"imageurl": "/url/to/image"
}
Projections
Una projecció és quan al “select” d’una consulta posem només un subconjunt de camps.
Per a fer-ho amb un JpaRepository primer definirem en un interface
quins són els camps que volem seleccionar:
1
2
3
4
5
6
public interface ProjectionMovie {
UUID getMovieid();
String getTitle();
Set<ProjectionActor> getActors();
}
Veiem que en lloc de definir els camps, hem de definir getters seguint l’estàndard JavaBeans.
Després al Repository podem fer que les consultes retornin objectes conforme a aquests interfaces:
1
2
3
4
public interface MovieRepository extends JpaRepository<Movie, UUID> {
List<ProjectionMovie> findBy();
}
Açò funcionarà per a les consultes que JPA derivades del nom: Query Methods
Relacions
@ManyToMany
En una relació ManyToMany entre dues entitats hem d’escollir primer una de les dos entitats com la “propietària” de la relació i l’altra com a “no-propietària”.
A l’entitat “pripietària” definirem les anotacions @ManyToMany
i @JoinTable
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Entity
@Table(name = "movie")
public class Movie {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
public UUID movieid;
public String title;
public String imageurl;
@ManyToMany
@JoinTable(name = "movie_actor", joinColumns = @JoinColumn(name = "movieid"), inverseJoinColumns = @JoinColumn(name = "actorid"))
public Set<Actor> actors;
}
A l’entitat “no-propietària” definirem l’anotació @ManyToMany
fent referència al camp de l’entitat “propietària” que defineix la relació:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Entity
@Table(name = "actor")
public class Actor {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
public UUID actorid;
public String name;
public String imageurl;
@ManyToMany(mappedBy = "actors")
public Set<Movie> movies;
}
Com la relació és bidireccional, quan la llibreria Jackson faci la serialització a JSON, es produeix una dependència circular (recursió infinita). Podem tallar aquesta recursió amb l’anotació @JsonIgnoreProperties
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Entity
@Table(name = "movie")
public class Movie {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
public UUID movieid;
public String title;
public String imageurl;
@ManyToMany
@JoinTable(name = "movie_actor", joinColumns = @JoinColumn(name = "movieid"), inverseJoinColumns = @JoinColumn(name = "actorid"))
@JsonIgnoreProperties("movies")
public Set<Actor> actors;
}
@Entity
@Table(name = "actor")
public class Actor {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
public UUID actorid;
public String name;
public String imageurl;
@ManyToMany(mappedBy = "actors")
@JsonIgnoreProperties("actors")
public Set<Movie> movies;
}
Webgrafía: