No Couchbase Connect 2015, demonstramos um aplicativo de exemplo que usa o N1QL para consultar dados de um bucket de amostra do Couchbase.
Se você perdeu a conferência, não há problema. Vamos explicar como reproduzir esse aplicativo e conferir alguns dos destaques do Couchbase 4.0.
Pré-requisitos
- Apache Maven 3
- Kit de desenvolvimento Java (JDK) 1.7
- Servidor Couchbase 4.0
- IntelliJ IDEA 14.1+, Eclipse ou NetBeans. O IntelliJ IDEA será usado neste exemplo.
Criação de um novo projeto
Abra o IntelliJ IDEA e escolha criar um novo projeto Java, certificando-se de usar o JDK 1.7, se solicitado. Para fins deste guia, vamos chamar o projeto de try-cb-java.
Agora, clique com o botão direito do mouse try-cb-java em sua árvore de projetos e, em seguida, selecione Adicionar suporte a estruturas e selecione Maven. Isso adicionará um pom.xml em seu projeto.
Configuração do Maven
Dentro do pom.xml comece dando ao projeto um nome de grupo mais atraente:
|
1 2 3 |
<groupId>com.couchbase.example</groupId> |
Em seguida, adicione o restante de nossas dependências ao arquivo, que inclui o Spring Boot, o cliente Couchbase e a estrutura de segurança do Spring.
|
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 |
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.2.3.RELEASE</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-core</artifactId> </dependency> <dependency> <groupId>com.couchbase.client</groupId> <artifactId>java-client</artifactId> <version>2.2.0-dp</version> </dependency> </dependencies> <repositories> <repository> <id>couchbase</id> <name>couchbase repo</name> <url>https://files.couchbase.com/maven2</url> <snapshots><enabled>false</enabled></snapshots> </repository> </repositories> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> |
Criação de um perfil de execução
No momento, se você tentar executar o aplicativo, ocorrerá um erro ou nada acontecerá porque não há nenhum perfil configurado no momento.
Na barra de ferramentas, selecione Run -> Edit Configurations (Executar -> Editar configurações) e escolha adicionar uma nova configuração do Maven. Você pode dar a ela o nome que quiser, mas é importante ter o seguinte no campo da linha de comando:
|
1 2 3 |
spring-boot:run |
Para este artigo, nomearemos a configuração como Inicialização do Spring.
O IntelliJ IDEA agora deve estar pronto para o desenvolvimento.
Criação de índices no Bucket do Couchbase
Como este tutorial usa consultas N1QL, devemos primeiro adicionar índices ao nosso bucket do Couchbase Server. Isso pode ser feito facilmente por meio de código, mas, para este exemplo, vamos fazer um atalho e adicioná-los por meio do cliente Couchbase Query (CBQ) que é instalado automaticamente com uma instalação do Couchbase 4+ no Mac OS e no Windows.
No Mac OS, inicie o CBQ encontrado em /Applications/Couchbase Server.app/Contents/Resources/couchbase-core/bin/cbq e execute o seguinte:
|
1 2 3 |
CREATE PRIMARY INDEX def_primary ON `travel-sample` USING gsi; |
No Windows, inicie o CBQ encontrado em C:/Arquivos de Programas/Couchbase/Server/bin/cbq.exe e execute o mesmo comando N1QL que é feito no Mac OS.
Criação de uma classe de aplicativo principal
A classe principal deste projeto será Application.java e pode ser criado clicando com o botão direito do mouse no ícone trycb da árvore do projeto e escolhendo Novo -> Classe Java.
Adicione o seguinte para colocar a classe em seu estado executável mais básico:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
package trycb; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @SpringBootApplication @RestController @RequestMapping("/api") public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } } |
Certifique-se de que o projeto seja executado sem erros, selecionando Executar -> Executar 'Spring Boot' na barra de ferramentas do IntelliJ IDEA.
Manipulação do compartilhamento de recursos entre origens (CORS)
Como a maior parte dos nossos testes será feita localmente, precisamos garantir que o CORS esteja ativado; caso contrário, o navegador da Web reclamará ao tentar acessar os pontos de extremidade da API com JavaScript.
Certifique-se de que a classe Application implemente a classe Filter e adicione o seguinte código:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
@Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletResponse response = (HttpServletResponse) res; response.setHeader("Access-Control-Allow-Origin", "*"); response.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"); chain.doFilter(req, res); } @Override public void init(FilterConfig filterConfig) throws ServletException {} @Override public void destroy() {} |
Configurar opções de cluster e de bucket do Couchbase
No momento, temos basicamente um aplicativo Spring Boot sem interação com o Couchbase. Incluímos o cliente Java via Maven, portanto, é hora de começar a usá-lo.
Adicione o seguinte à classe Application:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
@Value("${hostname}") private String hostname; @Value("${bucket}") private String bucket; @Value("${password}") private String password; public @Bean Cluster cluster() { return CouchbaseCluster.create(hostname); } public @Bean Bucket bucket() { return cluster().openBucket(bucket, password); } |
Não configuramos o nome do host, baldee senha mas elas serão usadas para se conectar a um cluster do Couchbase e a um determinado bucket.
Adicionar variáveis de recursos
Você viu que estávamos usando nome do host, baldee senhaPortanto, agora é hora de defini-los.
Na árvore de projetos do IntelliJ IDEA, clique com o botão direito do mouse em src/main/resources e escolha Novo -> Arquivo. Nomeie o novo arquivo application.properties e adicione as seguintes linhas:
|
1 2 3 |
hostname=127.0.0.1 bucket=travel-sample password= |
O Spring Boot vai captar isso application.properties para você. Mais informações sobre as propriedades relacionadas ao aplicativo podem ser vistas na seção Documentação oficial do Spring.
Criação de pontos de extremidade RESTful
Esse aplicativo será baseado em API, portanto, determinados pontos de extremidade precisam ser criados:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
@RequestMapping(value="/airport/findAll", method=RequestMethod.GET) public List<Map<String, Object>> airports(@RequestParam String search) { } @RequestMapping(value="/flightPath/findAll", method=RequestMethod.GET) public List<Map<String, Object>> all(@RequestParam String from, @RequestParam String to, @RequestParam String leave) throws Exception { } @RequestMapping(value="/user/login", method=RequestMethod.GET) public Object login(@RequestParam String user, @RequestParam String password) { } @RequestMapping(value="/user/login", method=RequestMethod.POST) public Object createLogin(@RequestBody String json) { } @RequestMapping(value="/user/flights", method=RequestMethod.POST) public Object book(@RequestBody String json) { } @RequestMapping(value="/user/flights", method=RequestMethod.GET) public Object booked(@RequestParam String username) { } |
Essencialmente, teremos pontos de extremidade para registro e login de usuários, reserva e localização de voos, bem como pesquisa de informações de voos.
A lógica por trás desses pontos de extremidade aparecerá em outra classe para fins de limpeza.
Criação de uma classe de banco de dados
Acabamos de configurar os pontos de extremidade de condução do nosso aplicativo Spring Boot, mas agora é hora de dar uma olhada na lógica por trás da interação com o banco de dados.
A classe de banco de dados para este projeto será Banco de dados.java e pode ser criado clicando com o botão direito do mouse no ícone trycb na árvore de projetos do IntelliJ IDEA e escolhendo Novo -> Classe Java.
Adicione o seguinte para obter a classe para um bom esqueleto de onde estamos indo:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
package trycb; public class Database { private Database() { } public static List<Map<String, Object>> findAllAirports(final Bucket bucket, final String params) { } public static List<Map<String, Object>> findAllFlightPaths(final Bucket bucket, String from, String to, Calendar leave) { } public static ResponseEntity<String> login(final Bucket bucket, final String username, final String password) { } public static ResponseEntity<String> createLogin(final Bucket bucket, final String username, final String password) { } private static List<Map<String, Object>> extractResultOrThrow(QueryResult result) { } } |
A partir daqui, completaremos cada um desses métodos na ordem em que provavelmente serão interagidos pelo usuário.
Criação de um novo usuário
Quando o usuário emite um POST para o /api/user/logina seguinte função de banco de dados deve ser chamada:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
public static ResponseEntity<String> createLogin(final Bucket bucket, final String username, final String password) { JsonObject data = JsonObject.create() .put("type", "user") .put("name", username) .put("password", BCrypt.hashpw(password, BCrypt.gensalt())); JsonDocument doc = JsonDocument.create("user::" + username, data); try { bucket.insert(doc); JsonObject responseData = JsonObject.create() .put("success", true) .put("data", data); return new ResponseEntity<String>(responseData.toString(), HttpStatus.OK); } catch (Exception e) { JsonObject responseData = JsonObject.empty() .put("success", false) .put("failure", "There was an error creating account") .put("exception", e.getMessage()); return new ResponseEntity<String>(responseData.toString(), HttpStatus.OK); } } |
O nome de usuário e a senha incluídos na solicitação serão adicionados a um objeto JSON e, em seguida, a senha será criptografada com a biblioteca Spring BCrypt. Para manter o controle dos dados do usuário, os novos usuários serão colocados em documentos intitulados usuário::{USERNAME_HERE}. Usando bucket.insert(doc)Se o bucket for inserido, será feita uma tentativa de inserir os dados no bucket. Se não houver exceções lançadas, a tentativa foi bem-sucedida e uma resposta é retornada. Se houver uma exceção, a inserção falhou e o erro será retornado.
Como fazer login como um usuário existente
Quando o usuário emite um GET para o mesmo /api/user/login a seguinte função de banco de dados deve ser chamada:
|
1 2 3 4 5 6 7 8 9 10 11 12 |
public static ResponseEntity<String> login(final Bucket bucket, final String username, final String password) { JsonDocument doc = bucket.get("user::" + username); JsonObject responseContent; if(BCrypt.checkpw(password, doc.content().getString("password"))) { responseContent = JsonObject.create().put("success", true).put("data", doc.content()); } else { responseContent = JsonObject.empty().put("success", false).put("failure", "Bad Username or Password"); } return new ResponseEntity<String>(responseContent.toString(), HttpStatus.OK); } |
Usando bucket.get("user::" + nome de usuário) com o nome de usuário fornecido, o aplicativo Java obtém o documento do bucket, se ele existir. A biblioteca Spring BCrypt tem uma função excelente para verificar se a senha fornecida corresponde ou não à senha criptografada que está armazenada. Se corresponder, retorna um objeto de sucesso; caso contrário, retorna um objeto de falha de login.
Extraindo o resultado do N1QL e tornando-o legível em Java
O N1QL retorna um Resultado da consulta que pode ser menos desejável se estiver retornando dados para um front-end solicitante. O que realmente queremos fazer é convertê-lo em um objeto Lista objeto.
|
1 2 3 4 5 6 7 8 9 10 11 12 |
private static List<Map<String, Object>> extractResultOrThrow(QueryResult result) { if (!result.finalSuccess()) { throw new DataRetrievalFailureException("Query error: " + result.errors()); } List<Map<String, Object>> content = new ArrayList<Map<String, Object>>(); for (QueryRow row : result) { content.add(row.value().toMap()); } return content; } |
Essa função será chamada toda vez que os dados N1QL forem retornados.
Como encontrar todos os aeroportos
Agora veremos um pouco da mágica por trás do N1QL quando se trata de pesquisar aeroportos.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public static List<Map<String, Object>> findAllAirports(final Bucket bucket, final String params) { Statement query; AsPath prefix = select("airportname").from(i(bucket.name())); if (params.length() == 3) { query = prefix.where(x("faa").eq(s(params.toUpperCase()))); } else if (params.length() == 4 && (params.equals(params.toUpperCase()) || params.equals(params.toLowerCase()))) { query = prefix.where(x("icao").eq(s(params.toUpperCase()))); } else { query = prefix.where(i("airportname").like(s(params + "%"))); } QueryResult result = bucket.query(Query.simple(query)); return extractResultOrThrow(result); } |
Você pode ver no código acima que estamos usando a API do Fluent com o IntelliJ IDEA para criar nossa consulta N1QL. Essencialmente, se você fosse olhar para o SQL bruto, ele teria a seguinte aparência:
|
1 2 3 |
SELECT airportname FROM `travel-sample` WHERE faa = {{PARAMS}} |
No exemplo acima, {{PARAMS}} representa um aeroporto como LAX ou similar. É claro que isso ocorre desde que o comprimento dos params seja três.
Como encontrar todas as rotas de voo
Por fim, ficamos com o método responsável por encontrar rotas de voo:
|
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 |
public static List<Map<String, Object>> findAllFlightPaths(final Bucket bucket, String from, String to, Calendar leave) { Statement query = select(x("faa").as("fromAirport")) .from(i(bucket.name())) .where(x("airportname").eq(s(from))) .union() .select(x("faa").as("toAirport")) .from(i(bucket.name())) .where(x("airportname").eq(s(to))); QueryResult result = bucket.query(Query.simple(query)); if (!result.finalSuccess()) { throw new DataRetrievalFailureException("Query error: " + result.errors()); } String fromAirport = null; String toAirport = null; for (QueryRow row : result) { if (row.value().containsKey("fromAirport")) { fromAirport = row.value().getString("fromAirport"); } if (row.value().containsKey("toAirport")) { toAirport = row.value().getString("toAirport"); } } Statement joinQuery = select("a.name", "s.flight", "s.utc", "r.sourceairport", "r.destinationairport", "r.equipment") .from(i(bucket.name()).as("r")) .unnest("r.schedule AS s") .join(i(bucket.name()).as("a") + " ON KEYS r.airlineid") .where(x("r.sourceairport").eq(s(fromAirport)).and(x("r.destinationairport").eq(s(toAirport))).and(x("s.day").eq(leave.get(Calendar.DAY_OF_MONTH)))) .orderBy(Sort.asc("a.name")); QueryResult otherResult = bucket.query(joinQuery); return extractResultOrThrow(otherResult); } |
Estamos fazendo duas consultas N1QL nesse método. A primeira pode ser facilmente traduzida para o seguinte:
|
1 2 3 |
SELECT faa AS fromAirport FROM `travel-sample` WHERE airportname = {{PARAMS.FROM}} UNION SELECT faa AS toAirport FROM `travel-sample` WHERE airportname = {{PARAMS.TO}} |
É claro que {{PARAMS}} é o que foi passado para o seu endpoint. Na declaração, estamos combinando os conjuntos de resultados de todos os de aeroportos e todos os para aeroportos.
Depois de obter os dois conjuntos de resultados, estamos percorrendo-os para garantir que o para e de existem, caso contrário, o padrão será NULL, o que impedirá que a próxima consulta seja bem-sucedida.
A segunda consulta pode ser traduzida na seguinte consulta bruta:
|
1 2 3 |
SELECT a.name, s.flight, s.utc, r.sourceairport, r.destinationairport, r.equipment FROM `travel-sample` AS r UNNEST r.schedule AS s JOIN `travel-sample` AS a ON KEYS r.airlineid WHERE r.sourceairport = {{TO}} AND r.destinationairport = {{TO}} AND s.day = 3 ORDER BY a.name ASC |
Estamos obtendo informações de programação sobre os voos, aninhando-as no documento JSON e, em seguida, unindo-as à chave agora achatada.
Encerramento das classes de aplicativo e banco de dados
Agora temos nossos endpoints e métodos de banco de dados, mas eles não estão conectados uns aos outros. É hora de revisitar o Application.java e adicione algum código às funções que criamos anteriormente:
|
1 2 3 4 5 6 7 8 9 10 11 12 |
@RequestMapping(value="/user/login", method= RequestMethod.GET) public Object login(@RequestParam String user, @RequestParam String password) { return Database.login(bucket(), user, password); } @RequestMapping(value="/user/login", method=RequestMethod.POST) public Object createLogin(@RequestBody String json) { JsonObject jsonData = JsonObject.fromJson(json); return Database.createLogin(bucket(), jsonData.getString("user"), jsonData.getString("password")); } |
Você pode ver que as duas imagens estáticas Banco de dados são chamados de cada um dos pontos de extremidade relacionados às contas de usuário. O mesmo processo pode ser feito para os outros pontos de extremidade que criamos anteriormente:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
@RequestMapping("/airport/findAll") public List<Map<String, Object>> airports(@RequestParam String search) { return Database.findAllAirports(bucket(), search); } @RequestMapping("/flightPath/findAll") public List<Map<String, Object>> all(@RequestParam String from, @RequestParam String to, @RequestParam String leave) throws Exception { Calendar calendar = Calendar.getInstance(Locale.US); calendar.setTime(DateFormat.getDateInstance(DateFormat.SHORT, Locale.US).parse(leave)); return Database.findAllFlightPaths(bucket(), from, to, calendar); } |
Teste dos pontos finais da amostra
Há algumas maneiras de testar os endpoints do aplicativo. Neste exemplo, usaremos o cURL, mas você certamente pode usar o Postman para o Google Chrome ou algo semelhante.
Com o cURL instalado, abra um Terminal ou Prompt de Comando e digite o seguinte:
|
1 2 3 |
curl -X GET 'https://localhost:8080/api/airport/findAll?search=LAX' |
O comando cURL acima atingirá o api/airport/findAll e passar um parâmetro de search=LAX. Se for bem-sucedido, você deverá receber uma resposta de:
|
1 |
[{"airportname":"Los Angeles Intl"}] |
O mesmo tipo de teste pode ser feito para todos os outros endpoints.
Conclusão
Acabamos de ver como configurar um aplicativo de viagem de amostra que usa o Couchbase Server e o Spring Boot para Java. Embora não tenhamos configurado um front-end, é muito possível adicionar um usando linguagens como AngularJS, jQuery ou ReactJS.
Esse projeto completo, juntamente com um front-end AngularJS, pode ser obtido no site GitHub do Couchbase Labs canal.