Las colecciones son una nueva característica de Couchbase 6.5. Te permiten agrupar documentos similares dentro de cada bucket, igual que las tablas en las bases de datos relacionales agrupan registros similares dentro de cada base de datos. Las colecciones estarán totalmente soportadas en Couchbase 7.0, pero puedes probarlas ahora mismo en la versión 6.5 de Couchbase como una característica Developer Preview. La Aplicación Demo ya las utiliza.
¿Qué son las colecciones?
Si vienes del mundo de las bases de datos relacionales, puedes pensar en las colecciones como si fueran tablas. Todos los documentos de una colección couchbase deben ser del mismo tipo, del mismo modo que todos los registros de una tabla relacional son del mismo tipo. Puede haber una tabla "cliente" o una tabla "producto" en un esquema relacional; del mismo modo, puede haber una colección "cliente" en un bucket de Couchbase.
En versiones anteriores de Couchbase, los datos se organizaban así:
- Grupo
- Cubo
- Documento
- Cubo
En Couchbase 6.5, hay dos capas más, así:
- Grupo
- Cubo
- Alcance
- Colección
- Documento
- Colección
- Alcance
- Cubo
¿Qué utilidad tienen las colecciones?
Las colecciones son el nivel más bajo de organización documental y contienen directamente documentos. Son útiles porque permiten agrupar los documentos con mayor precisión que antes. En lugar de volcar todos los tipos de documentos (productos, pedidos, clientes) en un único bucket y distinguirlos por un campo de tipo, puede crear una colección para cada tipo. Y cuando se consulta, se puede consultar la colección, no sólo todo el bucket. También podrá controlar el acceso a nivel de colección.
Los ámbitos son el nivel de organización por encima de las colecciones. Los ámbitos contienen colecciones y las colecciones contienen documentos. Hay diferentes formas de usar los ámbitos, dependiendo de para qué se esté usando el cluster de Couchbase. Si está soportando muchas aplicaciones internas diferentes para una compañía, cada aplicación debería tener su propio ámbito. Si el cluster está siendo usado para servir a muchas organizaciones cliente, cada una ejecutando su propia copia de una aplicación, cada copia debería tener un scope propio. Del mismo modo, si un clúster está siendo utilizado por grupos de desarrollo, tal vez para pruebas, la unidad de asignación debe ser un ámbito. En cada caso, el propietario puede crear las colecciones que desee en el ámbito que se le asigne.
Los ámbitos deben ser únicos dentro de sus cubos, y las colecciones deben ser únicas dentro de sus ámbitos. Así, el bucket "default" podría contener dos ámbitos "dev" y "prod", cada uno con sus propias colecciones "products" y "customers".
Uso de las colecciones
Puedes ver colecciones y ámbitos siendo usados en la última versión de la Aplicación Demo de Couchbase, aquí:
https://github.com/couchbaselabs/try-cb-java/tree/6.5.0-branch
Esta aplicación utiliza un bucket "travel-sample" ya existente para permitir al usuario buscar vuelos y hoteles, pero almacena sus propios datos de usuario y reservas en colecciones. La estructura utilizada es la siguiente:
- Cubo: por defecto
- Alcance: larson-travel
- Colección: usuarios
- Colección: vuelos
- Alcance: larson-travel
Cuando el usuario crea una cuenta, se crea un documento en la colección "usuarios". Cuando reservan vuelos, se crean documentos en la colección "vuelos", y se hace referencia a ellos en el documento del usuario en la colección "usuarios".
Este diseño permite que varias aplicaciones compartan el mismo bucket. Si tuviéramos una segunda instancia de la aplicación de demostración, utilizada por otra agencia de viajes, podríamos crear otro ámbito (con sus propias colecciones "users" y "flights") y dirigir la segunda instancia a este ámbito actualizando su archivo application.properties. Las dos instancias funcionarían una al lado de la otra, sin interferir entre sí.
Código de ejemplo
Para empezar, en el archivo application.properties se nombran el bucket y el scope que contienen la información sobre usuarios y reservas:
|
1 2 |
storage.clientorg.bucket=default storage.clientorg.scope=larson-travel |
Estos valores de configuración se recogen en el archivo Database.java:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
@Configuration public class Database { ... @Value("${storage.clientorg.bucket}") private String clientOrgBucket; @Value("${storage.clientorg.scope}") private String clientOrgScope; ... public Bucket clientOrgBucket() { return loginCluster().bucket(clientOrgBucket); } public @Bean Scope clientOrgScope() { return clientOrgBucket().scope(clientOrgScope); } } |
En User.java, vemos cómo se registra un nuevo vuelo para el usuario. Se pasa el bean scope, creado anteriormente. El nombre de usuario es el id el nombre con el que el usuario ha iniciado sesión.
|
1 2 |
public Result<Map<String, Object>> registerFlightForUser(final Scope scope, final String username, final JsonArray newFlights) { |
El id del documento de usuario es el nombre de usuario del usuario. La aplicación sabe que debe obtenerlo de la colección "users", en la colección utilizada por la aplicación.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
String userId = username; Collection usersCollection = scope.collection(USERS_COLLECTION_NAME); Collection flightsCollection = scope.collection(FLIGHTS_COLLECTION_NAME); Optional<GetResult> userDataFetch = usersCollection.get(userId); if (!userDataFetch.isPresent()) { throw new IllegalStateException(); } JsonObject userData = userDataFetch.get().contentAsObject(); if (newFlights == null) { throw new IllegalArgumentException("No flights in payload"); } JsonArray added = JsonArray.empty(); |
Los vuelos que el usuario ha reservado se almacenan en el documento de usuario, en un array llamado "vuelos".
|
1 2 3 4 |
JsonArray allBookedFlights = userData.getArray("flights"); if(allBookedFlights == null) { allBookedFlights = JsonArray.create(); } |
Añadimos los nuevos vuelos a los ya existentes.
|
1 2 3 4 5 6 7 8 9 10 11 |
for (Object newFlight : newFlights) { checkFlight(newFlight); JsonObject t = ((JsonObject) newFlight); t.put("bookedon", "try-cb-java"); String flightId = UUID.randomUUID().toString(); flightsCollection.insert(flightId, t); allBookedFlights.add(flightId); added.add(t); } userData.put("flights", allBookedFlights); |
A continuación, almacenamos la nueva versión del documento de usuario.
|
1 2 3 4 5 6 7 8 |
usersCollection.upsert(userId, userData); JsonObject responseData = JsonObject.create() .put("added", added); return Result.of(responseData.toMap(), "Booked flight in Couchbase document " + userId); } |
Justo debajo, vemos cómo se recuperan los vuelos de un usuario.
|
1 2 |
public List<Map<String, Object>> getFlightsForUser(final Scope scope, final String username) { |
Obtener el documento de usuario de la colección "usuarios".
|
1 2 3 4 5 6 |
Collection users = scope.collection(USERS_COLLECTION_NAME); Optional<GetResult> doc = users.get(username); if (!doc.isPresent()) { return Collections.emptyList(); } JsonObject data = doc.get().contentAsObject(); |
Obtiene la matriz "flights" del documento de usuario. Contiene una lista de identificadores de vuelo.
|
1 2 3 4 |
JsonArray flights = data.getArray("flights"); if (flights == null) { return Collections.emptyList(); } |
Recuperar cada documento de vuelo de la colección "vuelos" por ID.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// The "flights" array contains flight ids. Convert them to actual objects. Collection flightsCollection = scope.collection(FLIGHTS_COLLECTION_NAME); List<Map<String, Object>> results = new ArrayList<Map<String, Object>>(); for (int i = 0; i < flights.size(); i++) { String flightId = flights.getString(i); Optional<GetResult> res = flightsCollection.get(flightId); if (!res.isPresent()) { throw new RuntimeException("Unable to retrieve flight id " + flightId); } Map<String, Object> flight = res.get().contentAsObject().toMap(); results.add(flight); } return results; } |
Cambios respecto al código anterior
El código para trabajar con usuarios y sus vuelos era bastante diferente en la versión anterior, que no utilizaba colecciones. Allí, los vuelos reservados se almacenaban directamente en el documento de usuario. El documento de usuario se almacenaba directamente en la tabla "travel-sample". Este es el código original de la función registerFlightForUser().
|
1 2 |
public Result<Map<String, Object>> registerFlightForUser(final Bucket bucket, final String username, final JsonArray newFlights) { |
Fíjese en el uso de un prefijo para marcar el tipo de documento. Esto no es necesario una vez que las colecciones están disponibles.
|
1 2 3 4 5 6 7 8 9 10 |
JsonDocument userData = bucket.get("user::" + username); if (userData == null) { throw new IllegalStateException(); } if (newFlights == null) { throw new IllegalArgumentException("No flights in payload"); } JsonArray added = JsonArray.empty(); |
Recuperamos la matriz de vuelos, que ya está en el documento.
|
1 2 3 4 5 6 7 8 9 |
JsonArray allBookedFlights = userData.content().getArray("flights"); if(allBookedFlights == null) { allBookedFlights = JsonArray.create(); } for (Object newFlight : newFlights) { checkFlight(newFlight); JsonObject t = ((JsonObject) newFlight); t.put("bookedon", "try-cb-java"); |
Añadimos los vuelos a la lista de vuelos reservados.
|
1 2 3 4 5 |
allBookedFlights.add(t); added.add(t); } userData.content().put("flights", allBookedFlights); |
Y almacenamos el documento de usuario.
|
1 2 3 4 5 6 7 |
JsonDocument response = bucket.upsert(userData); JsonObject responseData = JsonObject.create() .put("added", added); return Result.of(responseData.toMap(), "Booked flight in Couchbase document " + response.id()); } |
Obviamente, separar los vuelos reservados del usuario no es terriblemente convincente en una aplicación de juguete. Pero en una aplicación de producción, en la que estamos almacenando muchos tipos de información sobre cada usuario, tendría sentido almacenar algunos registros fuera del documento de usuario, en particular los que fueran grandes o numerosos o propensos a cambiar con frecuencia.
Documentación sobre ámbitos y colecciones
Para saber más sobre cómo trabajar directamente con ámbitos y colecciones, consulte esta documentaciónque explica la API RESTful para trabajar con ambos, los comandos CLI pertinentes e información sobre las colecciones disponibles en cbstats.
Resumen
Las colecciones y los ámbitos te permiten organizar documentos dentro de un bucket de Couchbase, al igual que las tablas y los esquemas te permiten organizar filas dentro de bases de datos relacionales. La versión actual de Couchbase 6.5 GA proporciona soporte temprano y limitado para colecciones y ámbitos como una característica Developer Preview. Para empezar con colecciones y ámbitos, puedes empezar a trabajar con la Aplicación Demo Java ahora mismo.
Recursos
Descargar
Descargar Couchbase Server 6.5
Documentación
Documentación de Couchbase Collections 6.5
Notas de la versión de Couchbase Server 6.5
Novedades de Couchbase Server 6.5
Blogs
Presentación de Collections - Developer Preview en Couchbase Server 6.5
Hola Johan, esta es una característica muy buena para tener especialmente para las aplicaciones que están implementando multi tenencia sobre la base de la propiedad discriminador de datos. Así que quería saber una cosa que si es posible ejecutar una consulta en múltiples ámbitos? como para algunas consultas, queríamos obtener datos de múltiples ámbitos y para otras consultas, los datos deben volver de un solo ámbito. ¿Es posible en la versión 6.5 de couchbase? Gracias
Hola, Malik. No es posible escribir consultas N1QL contra colecciones en 6.5. En este momento, las consultas N1QL no pueden trabajar contra colecciones, sólo contra cubos, como lo han hecho en el pasado. N1QL para colecciones llegará en 7.0, probablemente en algún momento de 2020.
Una vez que N1QL funcione para colecciones, será posible escribir consultas que abarquen múltiples ámbitos, del mismo modo que ahora es posible escribir consultas N1QL que abarquen múltiples buckets.
Dado que "Todos los documentos de una colección couchbase deben ser del mismo tipo, al igual que todos los registros de una tabla relacional son del mismo tipo", ¿implica que todos los documentos de una colección deben tener los mismos campos?
Gracias