Ejemplo de Spring Data con PostgreSQL y Docker
En esta nueva entrada de refactorizando sobre Ejemplo de Spring Data con PostgreSQL y Docker, vamos a ver como construir una aplicación Spring Boot haciendo uso de la dependencia de Spring Data. Para ello vamos a levantar un PostgreSQL haciendo uso de Docker y conectar nuestro PostgreSQL con nuestra aplicaicón Spring Boot.
Spring Data es uno de lo módulos que se encuentran dentro de Spring. El objetivo básico de este módulo es facilitar el trabajo de configuración y persitencia al desarrollador.
PostgreSQL, la cual también es más conocida como Postgres, es una base de datos gratuita y open-source de tipo SQL.
Arrancar PostgreSQL con Docker
Lo primero que vamos a hacer es crear levantar un PostgreSQL con Docker. El comando a introducir será el siguiente:
Recuerda tener instalado Docker en tu local, para asegurarte que lo tienes puedes ejecutar $docker -v
docker run --network host --name postgres -e POSTGRES_PASSWORD=postgres -d postgres
Una vez arrancado PostgreSQL nos podemos asegurar que está arrancado, por ejemplo, nos podemos conectar con algún gestor de Base de Datos o podemos entrar dentro del contenedor de la siguiente manera:
docker exec -it <id del contenedor> bash psql -h localhost -U postgres
o También se puede utilizar un gestor de bases de datos como DBeaver:
Configuración de Spring Boot con PostgreSQL
Dependencias maven para un proyecto con Spring Data
La mejor manera de crear una aplicación desde 0 con Spring Boot es haciendo uso de su initializr y añadir las dependencias que se requieran.
Una vez hemos generado el proyecto deberíamos ver en nuestro pom.xml algo parecido a esto:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.4.0</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.refactorizando</groupId> <artifactId>spring-data-postgresql-example</artifactId> <version>0.0.1-SNAPSHOT</version> <name>spring-data-postgresql-example</name> <description>Spring Data example with PostgreSql</description> <properties> <java.version>11</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <version>${postgresql.version}</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
En donde las dependencias clave de este proyecto son las siguientes:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <version>${postgresql.version}</version> </dependency>
Las dos dependencias de arriba nos permitirá por un lado establecer la configuración necesaria de Spring Data y por otro lado utilizar el driver de Postgresql para conectar con la base de datos.
Configuración de la Conexión
Una vez tenemos la estructura del proyecto generada, es necesaria establacer la configuración de la conexión con nuestra base de datos, para ello lo haremos a través de application.yml o properties que se encuentra en la ruta: src/main/resources. En esta carpeta veremos un application.yml o application.properties. Vamos a configurarlos para establecer la conexión con nuestra base de datos.
Aunque la manera más rápida y comoda es hacerlo a través de los ficheros de configuración, para hacer uso de la autoconfiguración de Spring, también podemos hacerlo de manera programática de la siguiente manera:
spring: data: jpa.repositories.enabled: true datasource: initialization-mode: always driverClassName: org.postgresql.Driver url: jdbc:postgresql://localhost:5432/postgres username: postgres password: postgres continueOnError: true
Hay que tener en cuenta que si el package del repositorio no es un sub package de la clase principal de Spring hay que activar Spring JPA haciendo uso de la anotación @EnableJpaRepositories y vamos a especificar de nuestro repositorio. Esto es debido a que la autoconfiguración y el scaneo de paquetes de Spring no funciona en estos casos:
@EnableJpaRepositories(basePackages = "com.refactorizando.example.....repository")
Creación de la entidad
Una vez se ha realizado la configuración con la Base de Datos, es necesario crear una entidad de Base de Datos, que será el objeto que representa una tabla. Para ello vamos a hacer uso de @Entity y @Table, las cuales son las anotaciones que nos permiten crear y mapear con la tabla de Base de Datos.
Configuración del repositorio con JPA
Spring Data tiene integrado Java Persistence API (JPA), la cual, es una especificación JAVA la cual nos va a permitir persistir objetos y recuperarlos de nuestra base de datos. Es decir, nos abstrae de una posible implementación para persistir los objetos.
Al utilizar la interfaz de JpaRepository, tendremos un CRUD y algunos métodos definidos e implementados.
Algunas de las posibilidades que nos brinda hacer uso de JpaRepository son las siguientes:
- definir nuevos métodos en la interfaz, con una sintaxis propia, para generar queries.
- generar queries mediante JPQL haciendo uso de @Query
- hacer uso de Specification.
- hacer uso de JPA Named Queries.
Algunos de lo métodos predefinidos, son:
- <S extends T> S save(S entity)
- Optional<T> findById(ID primaryKey)
- Iterable<T> findAll()
- long count(); void delete(T entity)
- boolean existsById(ID primaryKey)
En esta entrada de refactorizando únicamente nos centraremos en crear un CRUD que extiend de JpaRepository, una query mediante @Query y una query automática para ver como poder trabajar con una Base de Datos Relacional.
Para poder comenzar creamos una interfaz que extiende de JpaRepository de la siguiente manera:
Creación de una query con JPQL (@Query)
Como hemos comentado en el punto anterior, podemos crear queries haciendo uso de la anotación @Query, con una sintaxis similar a la que proporciona sql.
Esta sintaxis y el uso de esta anotación nos va a permitir crear las queries que necesitemos cuando los métodos predefinidos, o lo métodos que nos puede proporcionar JPA no sean suficiente.
@Query("SELECT c FROM Car c WHERE c.model = :model") List<Car> getCarsByModel(@Param("model") String model);
Creación de una query automática con Spring Data
Cuando generamos un nuevo repositorio con Spring Data, se crea una implementación de los métodos que se han generado en la interfaz. En la generación de estos métodos, se intenta generar queries de esos métodos que están basados en los campos de los nombres de la clase entidad. Esta manera de definir nuevas queries es una forma bastante sencilla y limpia, ya que te ahorra todo el boilerplate de crear la query.
Vamos a ver esto con más detalle, vamos a crear dos métodos para encontrar los coches por marca y por modelo:
public interface CarRepository extends JpaRepository<Car, Long> { List<Car> findByModel(String model); List<Car> findByBrand(String brand); }
Como se puede ver la creación de nuevas queries es bastante sencillo, tan solo te tienes que asegurar que el campo o campos por los que harás la query existe. Pero a parte hay muchas más opciones, como ordenaciones y filtros, podrás ver más opciones pulsando aquí.
Transacciones con Spring Data
Al hacer uso de JpaRepository ya estamos haciendo uso de manera indirecta de @Transactional, ya que las clases padres están marcadas con @Transactional, para los métodos que puedan modificar la base de datos.
Pero la anotación @Transactional, puede ser usada fuera de repositorios para marcar aquellos métodos que puedan tener varias operaciones de modificación sobre base de datos. Esta anotación debería ir en métodos públicos:
@Service public class MyService{ @Transactional public void save() { repo.save(..); repo.deleteById(..); } }
Ten en cuenta hacer uso de @Transactional para todos aquellos métodos que puedan hacer cambios en tu Base de Datos.
Test de Integración con Testcontainers
Una parte muy importante de nuestras aplicaciones es la parte de testing. En una aplicación con Base de Datos habrá que asegurar que nuestra integración con Base de Datos, así como la lógica de nuestra aplicación. Para realizar los test integrados contra nuestra Base de Datos podemos realizarlo con una Base de Datos en memoria y embebida como H2 o con Testcontainers.
Quizás esta sea la mejor aproximación porque se creará un contenedor de la Base de Datos elegida y será lo más parecido a la realidad. Únicamente será necesario realizar una pequeña configuración y cargar la imágen que queremos usar.
Para ver un ejemplo completo de uso y cómo funciona Testcontainers puedes verlo en este enlace.
Ejemplo de Spring Data con PostgreSQL
Una vez visto como funciona Spring Data vamos a ver un pequeño ejemplo que conectará nuestra aplicación Spring Boot, con nuestra Base de Datos PostgreSQL. Este ejemplo lo puedes encontrar completo en nuestro Github.
Nuestro ejemplo va a consitir en un coche que va a tener un CRUD sobre una base de datos.
Para nuestro ejemplo vamos a partir de una entidad Coche (Car), que será la encarga de realizar el mapeo y de guardar el objeto Car en Base de Datos. Para poder crear la tabla o que la base de datos devuelva una tabla a este objeto, será necesario añadir las anotaciones @Table y @Entity.
@Table @Entity @Getter @Setter public class Car { @Id @GeneratedValue(strategy = GenerationType.AUTO) private UUID id; @Column private String model; @Column private String description; @Column private String color; }
A continuación vamos a crear nuestra capa repositorio, la cual en este caso extenderá de JpaRepository que nos facilita una implementación de JPA con lo que nos proporcionará los métodos básicos de un CRUD así como alguno más como el método flush(), tal y como hemos comentado anteriormente.
public interface CarRepository extends JpaRepository<Car, UUID> { }
Una vez hemos creado nuestra capa repositorio, que es la que se comunica con nuestra base de datos. Crearemos una capa de servicio que se encargará de llamar al repositorio, y podrá añadir lógica de negocio si lo necesita.
@AllArgsConstructor @Service public class CarService { private final CarRepository carRepository; public List<Car> findAll() { return carRepository.findAll(); } public Car findById(UUID uuid) { return carRepository.findById(uuid).orElseThrow(() -> new NoSuchElementException()); } public Car saveCar(Car car) { return carRepository.save(car); } public void deleteCar(UUID uuid) { carRepository.deleteById(uuid); } public Car updateCar(Car car) { return carRepository.save(car); } public boolean existById(UUID uuid) { return carRepository.existsById(uuid); } }
Finalmente hay que crear la capa de nuestra API para poder comunicarse con nuestro servicio, por lo que añadiremos un controller. Este controlador tendrá las llamadas necesarias para hacer un CRUD. Hay que tener en cuenta que hemos añadido la anotación @RequestMapping como path de nuestro servicio, que es /api/cars.
@RestController @RequestMapping("/api/cars") @RequiredArgsConstructor public class CarController { private final CarService carservice; @GetMapping() public ResponseEntity<List<Car>> getAllcars() { return new ResponseEntity<>(carservice.findAll(), HttpStatus.OK); } @GetMapping("/{id}") public ResponseEntity<Car> getCarById(@PathVariable("id") String id) { return new ResponseEntity<>(carservice.findById(UUID.fromString(id)), HttpStatus.OK); } @PostMapping() public ResponseEntity<Car> createCar(@RequestBody Car car) { return new ResponseEntity<>(carservice.saveCar(car), HttpStatus.CREATED); } @PutMapping("/{id}") public ResponseEntity<Car> updateCar(@PathVariable("id") String id, @RequestBody Car car) { if (carservice.existById(UUID.fromString(id))) { return new ResponseEntity<>(carservice.saveCar(car), HttpStatus.ACCEPTED); } throw new IllegalArgumentException("Car with id " + id + "not found"); } @DeleteMapping("/{id}") public ResponseEntity<HttpStatus> deleteCar(@PathVariable("id") String id) { carservice.deleteCar(UUID.fromString(id)); return new ResponseEntity<>(HttpStatus.NO_CONTENT); } }
Y no podemos olvidar en nuestra aplicación la parte de Test integrados que será realizado con Testcontainers, echemos un vistazo al código:
@Testcontainers @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) public class CarRepositoryPropertiesIntegrationTest { @Autowired private CarRepository carRepository; @DynamicPropertySource static void postgresqlProperties(DynamicPropertyRegistry registry) { registry.add("spring.datasource.url", postgreSQLContainer::getJdbcUrl); registry.add("spring.datasource.password", postgreSQLContainer::getPassword); registry.add("spring.datasource.username", postgreSQLContainer::getUsername); } @Container public static PostgreSQLContainer postgreSQLContainer = new PostgreSQLContainer("postgres:11-alpine") .withDatabaseName("integration-tests-db") .withPassword("inmemory") .withUsername("inmemory"); @Test public void save_and_read_car() { Car car = new Car(); car.setDescription("New car"); carRepository.save(car); List<Car> allCars = carRepository.findAll(); assert !allCars.isEmpty(); assert allCars.get(0).getDescription().equalsIgnoreCase("New car"); assert allCars.size() == 1; } }
En la clase anterior, haciendo uso de las anotaciones de Testcontainers, hemos creado un Test Integrado de nuestro repositorio contra nuestra Base de Datos. Si quieres leer más información sobre Testcontainers y su funcionamiento lo puedes hacer aquí.
Conclusión
En esta entrada de Ejemplo de Spring Data con PostgreSQL y Docker, hemos visto como Spring Data nos facilita la vida para realizar las operaciones necesarias con nuestra base de datos de una manera fácil y eficaz.
Puedes encontrar el ejemplo completo en nuestro github.
Si te ha gustado no dudes en poner un comentario así como si tienes alguna duda.
Síguenos en nuestras redes sociales a través de Facebook o Twitter para estar al día de nuestros artículos.
7 pensamientos sobre “Ejemplo de Spring Data con PostgreSQL y Docker”