As coleções são um novo recurso do Couchbase 6.5. Elas permitem agrupar documentos semelhantes dentro de cada bucket, assim como as tabelas em bancos de dados relacionais coletam registros semelhantes dentro de cada banco de dados. As coleções terão suporte total no Couchbase 7.0, mas você pode experimentá-las agora mesmo na versão 6.5 do Couchbase como um recurso Developer Preview. O aplicativo de demonstração já as utiliza.
O que são coleções?
Se você vem do mundo dos bancos de dados relacionais, pode pensar nas coleções como tabelas. Todos os documentos de uma coleção do couchbase devem ser do mesmo tipo, assim como todos os registros de uma tabela relacional são do mesmo tipo. Pode haver uma tabela "cliente" ou uma tabela "produto" em um esquema relacional; da mesma forma, pode haver uma coleção "cliente" em um bucket do Couchbase.
Nas versões mais antigas do Couchbase, os dados eram organizados da seguinte forma:
- Aglomerado
- Balde
- Documento
- Balde
No Couchbase 6.5, há mais duas camadas, como esta:
- Aglomerado
- Balde
- Escopo
- Coleção
- Documento
- Coleção
- Escopo
- Balde
Como as coleções são úteis?
As coleções são o nível mais baixo de organização de documentos e contêm diretamente os documentos. Elas são úteis porque permitem que você agrupe documentos com mais precisão do que era possível antes. Em vez de despejar todos os diferentes tipos de documentos (produtos, pedidos, clientes) em um único compartimento e distingui-los por um campo de tipo, você pode criar uma coleção para cada tipo. E, quando fizer uma consulta, você poderá consultar a coleção, e não apenas o bucket inteiro. Eventualmente, você também poderá controlar o acesso no nível da coleção.
Os escopos são o nível de organização acima das coleções. Os escopos contêm coleções e as coleções contêm documentos. Há diferentes maneiras de usar escopos, dependendo da finalidade para a qual o cluster do Couchbase está sendo usado. Se ele estiver oferecendo suporte a muitos aplicativos internos diferentes de uma empresa, cada aplicativo deverá ter um escopo próprio. Se o cluster estiver sendo usado para atender a muitas organizações clientes, cada uma executando sua própria cópia de um aplicativo, cada cópia deverá ter um escopo próprio. Da mesma forma, se um cluster estiver sendo usado por grupos de desenvolvimento, talvez para testes, a unidade de alocação deverá ser um escopo. Em cada caso, o proprietário pode criar as coleções que quiser no escopo que lhe foi atribuído.
Os escopos devem ser exclusivos dentro de seus compartimentos e as coleções devem ser exclusivas dentro de seus escopos. Dessa forma, o bucket "default" poderia conter dois escopos "dev" e "prod", cada um com suas próprias coleções "products" e "customers".
Uso de coleções
Você pode ver as coleções e os escopos sendo usados na versão mais recente do aplicativo de demonstração do Couchbase, aqui:
https://github.com/couchbaselabs/try-cb-java/tree/6.5.0-branch
Esse aplicativo usa um bucket "travel-sample" existente para permitir que o usuário pesquise voos e hotéis, mas armazena seus próprios dados de usuário e reservas em coleções. A estrutura que está sendo usada é a seguinte:
- Balde: padrão
- Escopo: larson-travel
- Coleção: usuários
- Coleção: voos
- Escopo: larson-travel
Quando o usuário cria uma conta, um documento é criado na coleção "users". Quando eles reservam voos, os documentos são criados na coleção "flights" e referenciados no documento do usuário na coleção "users".
Esse design permite que vários aplicativos compartilhem o mesmo bucket. Se tivéssemos uma segunda instância do aplicativo de demonstração, usada por outra agência de viagens, poderíamos simplesmente criar outro escopo (com suas próprias coleções "users" e "flights") e apontar a segunda instância para esse escopo, atualizando seu arquivo application.properties. As duas instâncias operariam lado a lado, sem interferir uma na outra.
Exemplo de código
Para começar, o bucket e o escopo que contêm informações de usuário e reservas são nomeados no arquivo application.properties:
|
1 2 |
storage.clientorg.bucket=default storage.clientorg.scope=larson-travel |
Esses valores de configuração são coletados no arquivo 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); } } |
Em User.java, vemos como um novo voo é registrado para o usuário. O bean de escopo, criado acima, é passado. O nome de usuário é o id, o nome com o qual o usuário fez login.
|
1 2 |
public Result<Map<String, Object>> registerFlightForUser(final Scope scope, final String username, final JsonArray newFlights) { |
O id do documento do usuário é o nome de usuário do usuário. O aplicativo sabe como obtê-lo na coleção "users", na coleção usada pelo aplicativo.
|
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(); |
Os voos que o usuário reservou são armazenados no documento do usuário, em uma matriz denominada "flights".
|
1 2 3 4 |
JsonArray allBookedFlights = userData.getArray("flights"); if(allBookedFlights == null) { allBookedFlights = JsonArray.create(); } |
Adicionamos os novos voos aos voos 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); |
Em seguida, armazenamos a nova versão do documento do usuário.
|
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); } |
Logo abaixo, vemos como os voos de um usuário são recuperados.
|
1 2 |
public List<Map<String, Object>> getFlightsForUser(final Scope scope, final String username) { |
Obtenha o documento do usuário da coleção "users".
|
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(); |
Obtenha a matriz "flights" do documento do usuário. Ela contém uma lista de IDs de voos.
|
1 2 3 4 |
JsonArray flights = data.getArray("flights"); if (flights == null) { return Collections.emptyList(); } |
Recupere cada documento de voo da coleção "flights" 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; } |
Alterações em relação ao código anterior
O código para trabalhar com usuários e seus voos era bem diferente na versão anterior, que não usava coleções. Lá, os voos reservados eram armazenados diretamente no documento do usuário. O documento do usuário era armazenado diretamente na tabela "travel-sample". Aqui está o código original da função registerFlightForUser().
|
1 2 |
public Result<Map<String, Object>> registerFlightForUser(final Bucket bucket, final String username, final JsonArray newFlights) { |
Observe o uso de um prefixo para marcar o tipo de documento. Isso não é necessário quando as coleções estão disponíveis.
|
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 a matriz de voos, que já está no 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"); |
Adicionamos os voos ao conjunto de voos reservados.
|
1 2 3 4 5 |
allBookedFlights.add(t); added.add(t); } userData.content().put("flights", allBookedFlights); |
E armazenamos o documento do usuário.
|
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 os voos reservados do usuário não é muito atraente em um aplicativo de brinquedo. Mas em um aplicativo de produção, em que armazenamos muitos tipos de informações sobre cada usuário, faria sentido armazenar alguns registros fora do documento do usuário, especialmente aqueles que são grandes, numerosos ou propensos a mudar com frequência.
Documentação de escopos e coleções
Para saber mais sobre como trabalhar com escopos e coleções diretamente, consulte esta documentaçãoque explica a API RESTful para trabalhar com ambos, os comandos relevantes da CLI e informações sobre coleções disponíveis em cbstats.
Resumo
As coleções e os escopos permitem que você organize os documentos em um bucket do Couchbase, assim como as tabelas e os esquemas permitem que você organize as linhas em bancos de dados relacionais. A versão atual do Couchbase 6.5 GA fornece suporte antecipado e limitado para coleções e escopos como um recurso Developer Preview. Para começar a usar as coleções e os escopos, você pode começar a trabalhar com o aplicativo de demonstração em Java agora mesmo.
Recursos
Baixar
Faça o download do Couchbase Server 6.5
Documentação
Documentação do Couchbase Collections 6.5
Notas de versão do Couchbase Server 6.5
Couchbase Server 6.5 O que há de novo
Blogs
Apresentando as coleções - Developer Preview no Couchbase Server 6.5
Anunciando o Couchbase Server 6.5 - Novidades e aprimoramentos
Johan, esse é um recurso muito bom, especialmente para os aplicativos que estão implementando vários inquilinos com base na propriedade discriminadora de dados. Então, eu queria saber se é possível executar uma consulta em vários escopos... por exemplo, para algumas consultas, queremos obter dados de vários escopos e, para outras consultas, os dados devem voltar de um único escopo. Então, isso é possível na versão 6.5 do couchbase? Obrigado pela atenção
Olá, Malik. Não é possível escrever consultas N1QL em coleções na versão 6.5. No momento, as consultas N1QL não funcionam com coleções, apenas com compartimentos, como acontecia no passado. O N1QL para coleções será lançado na versão 7.0, provavelmente em algum momento de 2020.
Quando o N1QL funcionar para coleções, será possível escrever consultas que abranjam vários escopos, assim como agora é possível escrever consultas N1QL que abranjam vários buckets.
Como "Todos os documentos de uma coleção do couchbase devem ser do mesmo tipo, assim como todos os registros de uma tabela relacional são do mesmo tipo", isso implica que todos os documentos de uma coleção devem ter os mesmos campos?
Agradecimentos