Índice de contenidos
- 1. Introducción
- 2. Entorno
- 3. Preparación de la base de datos
- 4. Creación de la aplicación
- 5. Conclusiones
- 6. Referencias
Introducción
Couchbase es una base de datos no SQL, distribuida y orientada a documentos usada por aplicaciones como Amadeus, Ebay, Betfair, LinkedIn, etc.
Couchbase está diseñada para responder a millones de operaciones de forma rápida, con alta disponibilidad y escalado, realizadas por muchos usuarios concurrentes.
Podemos leer un tutorial introductivo en https://adictosaltrabajo.com/2018/12/11/primeros-pasos-con-couchbase-server/.
Como siempre Spring nos ofrece una abstracción para facilitarnos el trabajo con Couchbase y reducir significativamente la cantidad de código necesario para configurar y trabajar con esta base de datos. Dentro del proyecto Spring Data nos encontramos con Spring Data Couchbase que nos proporciona integración con la base de datos Couchbase Server con un modelo que nos permite interactuar con los Buckets y escribir fácilmente la capa de acceso a datos.
En este tutorial veremos cómo hacer una aplicación que consuma e inserte datos en un bucket de Couchbase.
Entorno
Este tutorial está escrito usando el siguiente entorno:
- Hardware: MacBook Pro 15’ (2,5 GHz Intel Core i7, 16GB DDR3)
- Sistema operativo: macOS Mojave 10.14.1
- Versiones del software:
- Docker: 18.09.0
- Couchbase Server: 5.0.1
- Java: 8
- Spring Boot: 2.1.1.RELEASE
Preparación de la base de datos
Arrancamos Couchbase:
docker run -t --name db -p 8091-8094:8091-8094 -p 11210:11210 couchbase/server-sandbox:5.0.1
Dentro del servidor de Couchbase nos encontramos con el bucket de ejemplo «travel-sample» con casi 32000 items donde dentro de ellos podemos encontrar aeropuertos, aerolíneas, rutas etc.
Vistas
Las vistas en Couchbase nos permiten extraer campos e información y crear índices sobre éstos. Las vistas creadas nos permitirán iterar, seleccionar y consultar información sobre ellas.
Debido a la naturaleza del clúster de Couchbase y al tamaño de los conjuntos de datos que puede almacenar, se debe controlar el impacto que puede tener el desarrollo de una vista. Crear una vista implica la creación del índice que puede ralentizar el rendimiento del servidor mientras éste se genera. Debido a este posible impacto, para admitir tanto la creación y pruebas de vistas, como el despliegue de éstas en producción, Couchbase Server soporta dos tipos de vistas: las vistas de desarrollo y las vistas de producción. Los dos tipos de vista funcionan de manera idéntica, pero tienen diferentes propósitos y restricciones en sus operaciones.
Ahora vamos a crear una vista con los aeropuertos. Para ello vamos a la sección Indexes y en la columna vistas pinchamos sobre «ADD VIEW» y creamos las vista «all» con nombre de documento «airport«. Estos son los valores con los que funcionará por defecto el CrudRepository de Spring para saber hacer ciertas operaciones.
En el Map de la vista añadimos el siguiente código Javascript:
function (doc, meta) {
if(doc.type == "airport") {
emit(meta.id, null);
}
}
Tras crear y probar la vista la publicamos para que pase a producción y tengamos acceso a ella desde la aplicación que vamos a desarrollar.
Seguridad
Para que la aplicación tenga acceso al bucket vamos a crearle un usuario. En la consola de Couchbase, en la sección Security creamos el usuario «travel-sample» con contraseña «travel-sample1234» y en los roles le damos Bucket Full Access sobre el bucket «travel-sample».
Creación de la aplicación
Creamos una aplicación Spring Boot con maven:
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.autentia</groupId>
<artifactId>democouchbase</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>democouchbase</name>
<description>Demo project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.1.RELEASE</version>
<relativePath/>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-couchbase</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Lo primero que vamos a hacer es modelar la entidad que respalde la representación de aeropuertos que tenemos en el bucket «travel-sample» de nuestro servidor Couchbase.
@Document
public class Airport {
@Id
private String documentId;
@Field("airportname")
private String airportName;
@Field
private String city;
@Field
private String country;
@Field
private String icao;
@Field
private Long id;
@Field
private String tz;
@Field
private String type;
public Airport(String airportName, String city, String country, String icao, Long id, String tz, String type) {
this.airportName = airportName;
this.city = city;
this.country = country;
this.icao = icao;
this.id = id;
this.tz = tz;
this.type = type;
}
// Getters...
Vemos como al no estar en camel case el nombre del aeropuerto, le tenemos que indicar el nombre en base de datos dentro de la anotación @Field.
Una vez tenemos modelada la entidad pasamos a configurar los repositorios. Simplemente tenemos que crear una clase de configuración que extienda a AbstractCouchbaseConfiguration, donde activemos los repositorios con @EnableCouchbaseRepositories y donde especifiquemos:
- La lista de nodos Couchbase para iniciar (no hace falta puerto, sólo IP o nombre del host).
- El nombre del bucket.
- La contraseña que le hemos dado al usuario de la aplicación que anteriormente hemos creado en Couchbase con el mismo nombre del bucket.
@Configuration
@EnableCouchbaseRepositories(basePackages = {"com.autentia.democouchbase.dao"})
public class CouchbaseConfig extends AbstractCouchbaseConfiguration {
@Override
protected List getBootstrapHosts() {
return Collections.singletonList("127.0.0.1");
}
@Override
protected String getBucketName() {
return "travel-sample";
}
@Override
protected String getBucketPassword() {
return "travel-sample1234";
}
}
Con esta configuración ya podemos probar a arrancar la aplicación y comprobar que se conecta correctamente a la base de datos. Si todo va bien, podemos pasar a crear el repositorio.
Consultas basadas en vistas
A partir de la versión 2 de Spring Couchbase las consultas con vista de respaldo han evolucionado y se ha introducido soporte para N1QL. Las consultas con vistas de respaldo son mucho más limitadas debido a que cada método personalizado de consulta necesitaría su propia vista que debe prepararse de antemano en el clúster. Como regla general los siguientes métodos todavía requieren una vista de respaldo:
- Iterable findAll()
- long count()
- void deleteAll()
Como nosotros ya hemos creado la vista ya podemos crear el repositorio para listar todos los aeropuertos.
public interface AirportRepository extends CouchbasePagingAndSortingRepository {
}
Creamos el servicio:
@Service
public class AirportService {
private final AirportRepository airportRepository;
public AirportService(AirportRepository airportRepository) {
this.airportRepository = airportRepository;
}
public List list() {
Iterable airportIterable = airportRepository.findAll();
List airports = new ArrayList();
airportIterable.forEach(airports::add);
return airports;
}
}
Si creamos ahora un controlador ya podemos listar todos los aeropuertos llamando a http://localhost:8080/airport/ . Si no tuvieramos la vista airport/all Spring lanzaría un error indicando que no la encuentra.
@RestController
public class AirportController {
private final AirportService airportService;
public AirportController(AirportService airportService) {
this.airportService = airportService;
}
@GetMapping("/airport")
public List list() {
return airportService.list();
}
}
Podemos realizar más consultas basadas en vistas con la anotación @View en cada método que definamos en el repositorio.
Consultas N1QL
A partir de la versión 4.0, Couchbase Server tiene un nuevo lenguaje de consultas llamado N1QL. En «Spring-Data-Couchbase 2.0″ es la forma preteterminada de hacer consultas y de derivar éstas a partir del nombre de los métodos.
Ahora vamos a añadir una consulta para listar todos los aeropuertoes de un país.
public interface AirportRepository extends PagingAndSortingRepository {
@Query("#{#n1ql.selectEntity} WHERE type = 'airport' AND country = $country")
List list(String country);
}
En la consulta hemos usado «#n1ql.selectEntity» uno de los valores que SpEL (Spring Expression Language) tiene para dar soporte a N1QL. Con esto nos aseguramos que se seleccionen todos los campos del bucket actual. La consulta que hemos puesto sería equivalente a:
SELECT #{#n1ql.fields} FROM #{#n1ql.bucket} WHERE type = 'airport' and country = $country
Podemos ver en el log las consultas generadas estableciendo en application.properties:
logging.level.org.springframework.data=DEBUG
Otro SpEL que es recomendable usar es «#n1ql.filter» para que nos añada el discriminador del documento sobre el que estamos seleccionando:
#{#n1ql.selectEntity} WHERE #{#n1ql.filter} AND country = $country
En este caso la consulta generada sería del tipo:
#{#n1ql.selectEntity} WHERE _class = 'com.autentia.democouchbase.entity.Airport' AND country = $country
Nosotros para discriminar tenemos la propiedad «type = ‘airport‘» y como vemos Spring usa la propiedad «_class» y como valor el nombre de la clase. El nombre de la propiedad se puede cambiar sobrescribiendo el método «typeKey()» en la configuración de AbstractCouchbaseConfiguration. El problema es que Spring no permite todavía cambiar el valor, y siempre nos pone el nombre de la clase. Existen dos soluciones:
- No usar «#{#n1ql.filter}» y poner siempre «type = ‘airport‘».
- Añadir la columna en base de datos.
Como «#{#n1ql.filter}» también nos añade filtros de paginación, vamos a optar por la segunda opción. Vamos ahora al Couch Server y ejecutamos la consulta:
UPDATE `travel-sample` SET _class = 'com.autentia.democouchbase.entity.Airport'
where type = 'airport';
Ahora ya podemos quitar el filtrado manual de tipos y añadir paginado:
public interface AirportRepository extends PagingAndSortingRepository {
@Query("#{#n1ql.selectEntity} WHERE #{#n1ql.filter} AND country = $country")
Page list(String country, Pageable pageable);
}
El servicio nos quedaría:
public List list(String country, int pageNumber, int pageSize) {
PageRequest pageRequest = PageRequest.of(pageNumber, pageSize);
Page page = airportRepository.list(country, pageRequest);
return page.getContent();
}
Y el controlador:
@GetMapping("/airport/{country}")
public List list(@PathVariable String country, @RequestParam int pageNumber, @RequestParam int pageSize) {
return airportService.list(country, pageNumber, pageSize);
}
Si queremos, en vez de escribir nosotros mismos la consulta N1QL en el repositorio, podemos hacer que Spring nos la genere a través del nombre del método, por lo que el repositorio quedaría:
public interface AirportRepository extends PagingAndSortingRepository {
Page findByCountry(String country, Pageable pageable);
}
Haciendo desde el navegador consultas a http://localhost:8080/airport/United%20States?pageNumber=0&pageSize=10 o a http://localhost:8080/airport/France?pageNumber=0&pageSize=10 comprobamos como se listan los aeropuertos filtrados por país. También podemos observar que las consultas tardan un poco en ejecutarse y que en el log de la aplicación sale constantemente el aviso:
ThresholdLogReporter : Operations over threshold
Así que vamos a optimizar la consulta creando un índice sobre el campo «country».
CREATE INDEX country_idx ON `travel-sample`(country);
Ahora sí que vemos que la consulta se ejecuta mucho más rápidamente y que deja de salir el aviso en el log.
Inserción de registros
Como podemos observar, el identificador de las entidades de la base de datos es la concatenación del campo type con el campo id, por lo que para insertar registros en base de datos vamos a añadir a nuestra entidad una clave autogenerada.
@Id @GeneratedValue(strategy = GenerationStrategy.USE_ATTRIBUTES, delimiter = "_")
private String documentId;
@Field @IdPrefix(order=0)
private String type;
@Field @IdSuffix(order=0)
private Long id;
Ya podemos escribir el comando de creación y el servicio.
public class CreateAirportCommand {
private String airportName;
private String city;
private String country;
private String icao;
private Long id;
private String tz;
// getters y setters
}
public Airport create(CreateAirportCommand command) {
String airportName = command.getAirportName();
String city = command.getCity();
String country = command.getCountry();
String icao = command.getIcao();
Long id = command.getId();
String type = Airport.TYPE;
String tz = command.getTz();
Airport airport = new Airport(airportName, city, country, icao, id, tz, type);
airportRepository.save(airport);
return airport;
}
Por último creamos el controlador:
@PostMapping("/airport")
public Airport create(@RequestBody CreateAirportCommand command) {
return airportService.create(command);
}
Haciendo POST a la url con un JSON en el cuerpo de la petición deberíamos comprobar que el identificador se crea correctamente:
Por último vamos a añadir validaciones en nuestra entidad. Como es lógico las validaciones JSR a nivel de controlador seguirán funcionando en Spring MVC, ahora lo que vamos a probar es el soporte de la librería Spring Data Couchbase con las validaciones JSR 303 a nivel de entidad.
Para que las validaciones funcionen tenemos que añadir a nuestro proyecto la librería JSR-303 y alguna de sus implementaciones, por ejemplo la de Hibernate.
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.13.Final</version>
</dependency>
También debemos añadir dos Beans a nuestra clase de configuración CouchbaseConfig.
@Bean
public LocalValidatorFactoryBean validator() {
return new LocalValidatorFactoryBean();
}
@Bean
public ValidatingCouchbaseEventListener validationEventListener() {
return new ValidatingCouchbaseEventListener(validator());
}
En nuestro caso no necesitamos definir el Bean ValidatingCouchbaseEventListener debido al starter spring-boot-starter-data-couchbase que tenemos que ya nos lo define.
Ahora ya podemos anotar la entidad Airport. Si la validación fallara en el save() se lanza la excepción ConstraintViolationException.
@Document
public class Airport {
public static final String TYPE = "airport";
@Id @GeneratedValue(strategy = GenerationStrategy.USE_ATTRIBUTES, delimiter = "_")
private String documentId;
@Size(min = 5)
@NotNull
@Field("airportname")
private String airportName;
@Size(min = 5)
@NotNull
@Field
private String city;
@Size(min = 5)
@NotNull
@Field
private String country;
@Size(min = 4)
@NotNull
@Field
private String icao;
@NotNull
@Field @IdSuffix(order=0)
private Long id;
@Size(min = 5)
@NotNull
@Field
private String tz;
@Size(min = 5)
@NotNull
@Field @IdPrefix(order=0)
private String type;
// getters & setters
}
Se puede descargar el código completo desde:
git clone https://github.com/diyipol/couch_base_example.git
Conclusiones
Como nos tiene acostumbrados, Spring nos da soporte para poder trabajar de forma transparente con bases de datos Couchbase, por lo que podemos configurar la base de datos rápidamente y de forma sencilla en nuestra aplicación.
Además, gracias al nuevo soporte de consultas N1QL nos quita la gran dependencia que tenía de tener que crear vistas en base de datos para luego crear las operaciones del repositorio de nuestra aplicación sobre ellas.