As linguagens de consulta declarativas foram uma grande mudança no mundo dos mecanismos de banco de dados.
O Linguagem de consulta SQL++ (anteriormente N1QL) é principalmente uma linguagem de consulta declarativa. Você informa à consulta o que deseja obter e a N1QL resolve o restante dos detalhes de como fazer isso acontecer.
No entanto, a capacidade de instruir programaticamente sua consulta ao banco de dados é útil em várias situações. Afinal de contas, você conhece sua própria lógica de negócios - o N1QL não conhece.
Entrar funções definidas pelo usuário (UDFs). Os UDFs oferecem mais controle sobre uma determinada consulta e permitem que você instrua a linguagem de consulta sobre como determinadas tarefas são realizadas. A versão 7.0 do Couchbase Server inclui funções definidas pelo usuário para a linguagem de consulta N1QL.
O N1QL é bastante agnóstico no que diz respeito às linguagens de programação subjacentes. Em vez de especificar sua própria linguagem procedural, o N1QL emprega uma linguagem Gerente de idiomasou seja, ele já foi projetado para oferecer suporte a vários idiomas convidados.
Por enquanto, os idiomas suportados são inlineuma linguagem interna que permite que você codifique qualquer expressão N1QL válida (incluindo subconsultas) e JavaScript. Vamos começar comparando essas duas linguagens suportadas para UDFs N1QL.
Exemplo de UDF N1QL: Uso básico
Para adicionar sua lógica de negócios, você precisa criar funções, como a função inline exemplo abaixo:
|
1 |
CREATE FUNCTION add(arg1, arg2) { arg1 + arg2 } |
Assim, você pode usar sua lógica comercial livremente onde quer que uma expressão seja permitida ou diretamente por meio do EXECUTAR FUNÇÃO declaração:
|
1 2 |
select add(1, 2) EXECUTE FUNCTION add(1, 2) |
E quando você não tiver mais utilidade para ele, simplesmente o abandone:
|
1 |
DROP FUNCTION add |
Exemplo de UDF N1QL: JavaScript
Criar e descartar funções definidas pelo usuário em JavaScript é um pouco mais complicado, por motivos que ficarão evidentes na próxima seção.
Os UDFs JavaScript são tecnicamente externo funções. Isso ocorre porque elas são escritas em uma linguagem diferente e são executadas em um processo diferente do N1QL Query Service.
Sua primeira etapa é criar o código JavaScript:
|
1 |
curl -v -X POST https://localhost:8093/evaluator/v1/libraries/math -u Administrator:password -d 'function add(a, b) { let data = a + b; return data; }' |
Em seguida, crie a função N1QL:
|
1 |
CREATE FUNCTION javascriptAdd(a, b) LANGUAGE javascript AS "add" AT "math" |
Uma vez criada, você pode criar e soltar UDFs JavaScript exatamente como as inline. Quando não precisar mais da biblioteca, você poderá excluí-la usando este comando:
|
1 |
curl -v -X DELETE https://localhost:8093/evaluator/v1/libraries/math -u Administrator:password |
Novidades no Couchbase 7.0: Funções definidas pelo usuário para escopos e coleções
Um dos principais recursos da versão 7.0 do Couchbase Server é a introdução do Escopos e coleções para ajudá-lo ainda mais a organizar seu JSON documentos. As coleções são unidades de armazenamento para um grupo de documentos semelhantes, e os escopos são unidades de armazenamento maiores que contêm várias coleções, o que permite particionar aplicativos.
Com escopos e coleções,
Servidor Couchbase - um banco de dados de documentos - agora oferece todos os pontos fortes do modelo relacional - esquema, tabelas, colunas - sem nenhum de seus pontos fracos.
Passo lateral rápido: Para permitir a compatibilidade com versões anteriores, o Query Service agora tem um contexto_de_consulta Parâmetro da API REST que indica qual Bucket e Scope devem ser usados para resolver nomes de keyspace relativos, por exemplo:
|
1 2 |
cbq> \set -query_context "default:travel-sample.scope1"; cbq> select * from airlines; |
Na declaração acima, o SELECIONAR declaração resolve companhias aéreas para a coleção denominada default:travel-sample.scope1.airlines.
Muito bem, agora voltamos às funções definidas pelo usuário:
A lógica por trás dos Escopos e Coleções com UDFs é que você pode ter ambientes de desenvolvimento, pré-produção e produção do mesmo aplicativo em três Escopos diferentes, onde três cópias do mesmo aplicativo são implementadas com a mesma lógica. Por outro lado, os Escopos podem conter diferentes implementações do mesmo aplicativo em que é necessária uma lógica comercial diferente (por exemplo, lojas on-line em que são aplicadas diferentes estruturas de desconto ou entrega, ou um aplicativo de contabilidade em que são aplicadas diferentes regras de tributação).
No primeiro caso, ter uma definição global para funções individuais é suficiente porque a lógica comercial é a mesma. Mas, para dar suporte ao segundo caso, as UDFs tiveram de ser divididas em Escopos. Cada escopo pode ter sua própria instância do mesmo UDF, cada uma com uma lógica diferente.
Também vale a pena observar que o Couchbase Server 7.0 não força você a mudar para Escopos e Coleções, portanto, era importante que os UDFs não estivessem vinculados ao recurso Coleções.
Uma observação sobre UDFs globais
As UDFs globais não dependem de escopos, o que significa que são compatíveis com as UDFs introduzidas na versão 6.5.
Se você não estiver usando Coleções, não precisará fazer nada de especial para usá-las.
Os UDFs globais são referenciados usando um nome totalmente qualificado de duas partes, conforme abaixo:
|
1 2 |
CREATE FUNCTION default:bucket1.scope1.func1() { 0 }; EXECUTE FUNCTION default:bucket1.scope1.func1(); |
A importância do parâmetro query_context
"Ok, agora estou confuso", ouço você dizer. "Quais funções você estava usando nos exemplos anteriores? Os nomes não estavam totalmente qualificados."
A resposta está no contexto_de_consulta Configuração de parâmetro da API REST: se o parâmetro contexto_de_consulta não estiver definido, o analisador N1QL resolve o nome para global funções. Se a contexto_de_consulta estiver definido, o analisador usa o contexto_de_consulta para resolver os nomes para as funções relevantes do Scope, da mesma forma que fez com os nomes não qualificados do espaço de chaves.
Você pode implantar seu aplicativo com perfeição em escopos ou em buckets apenas alterando a configuração do parâmetro contexto_de_consulta Parâmetro da API REST.
Resolução de objetos dentro de funções
Quando as funções definidas pelo usuário fazem referência a objetos totalmente qualificados, não há ambiguidade quanto ao objeto a que se referem:
|
1 |
CREATE FUNCTION func { (SELECT * FROM default:bucket1.scope1.collection1) }; |
A próxima pergunta é: como as funções resolvem os objetos referenciados dentro delas? Por exemplo:
|
1 2 |
CREATE FUNCTION func1() { func2() }; CREATE FUNCTION default:func3 { (SELECT * FROM keyspace1) }; |
A chave aqui é o princípio da menor surpresa: durante a execução, as funções definidas pelo usuário mudam o contexto da consulta para o caminho no qual foram criadas e sempre fazem referência a objetos dentro desse caminho.
Não importa se você as chamou com um caminho relativo ou totalmente qualificado, uma função chamada com os mesmos parâmetros sempre retorna os mesmos resultados, que são obtidos dos mesmos objetos.
No primeiro exemplo acima, func2() seria resolvido em uma função global ou de escopo, dependendo do contexto_de_consulta no momento da criação.
Para func3(), espaço-chave1 seria o Bucket espaço-chave1.
Mistura de funções globais e de escopo
Não há nada que o impeça de usar UDFs globais juntamente com o recurso Collections: basta usar os nomes totalmente qualificados.
Da mesma forma, se quiser usar uma função do escopo criada em um escopo diferente, basta fazer referência a ela diretamente.
Funções JavaScript, revisitadas
Veja por que as funções JavaScript devem ser divididas em duas partes, ou seja, um corpo e uma definição de função separada:
-
- Quando você cria o corpo separadamente da definição da função, pode reutilizar o mesmo corpo em vários lugares. A mesma função em vários locais pode fazer referência ao mesmo corpo e, quando o corpo é editado ou redefinido, essa alteração se aplica automaticamente a todas as definições de função relacionadas.
- Da mesma forma, esse padrão também é importante quando se trata de eliminar ou excluir funções. Quando você precisa eliminar uma instância de uma função, o corpo precisa permanecer no lugar se for usado por outras instâncias.
Dicas e truques para usar funções definidas pelo usuário N1QL no Couchbase 7.0
Nomeando seus UDFs
Os nomes de UDFs são identificadores e não podem corresponder a nomes de funções predefinidas. Se os nomes fazer a função predefinida tem precedência, o que significa que sua UDF não será usada.
Se você quiser usar um nome de função predefinido (por algum motivo), deverá qualificá-lo totalmente quando fizer referência a ele. Por exemplo:
|
1 2 |
CREATE FUNCTION length(arg) { 0 }; SELECT default:length(type) FROM `travel-sample`; |
Parâmetros do UDF
As funções definidas pelo usuário do N1QL suportam três tipos de listas de parâmetros:
- Parâmetros vazios
1CREATE FUNCTION func1() { 0 }
A função não recebe parâmetros. Qualquer parâmetro passado resulta em um erro. - Parâmetros variáveis
1CREATE FUNCTION func1(...) { array_length(args) }
Três pontos denotam uma função variada. Ela pode receber qualquer número de parâmetros de qualquer tipo. Os parâmetros estão contidos em uma matriz de nomesargumentos. - Parâmetros nomeados
1CREATE FUNCTION func1(arg1) { arg1 }
Os parâmetros não são digitados, mas o número de argumentos passados é imposto.
Algumas dicas sobre sobrecarga e manipulação de tipos
Parâmetros não tipados e funções variadas são o mais próximo que o Couchbase chegará da sobrecarga de funções: onde a mesma função é definida várias vezes com uma lista de parâmetros diferente - ou tipos de parâmetros diferentes - para poder operar de forma diferente dependendo da entrada.
Em vez disso, eu recomendaria esta estratégia N1QL: Tenha uma única função que verifique os argumentos recebidos e aja de acordo.
Segue um exemplo de uma função variada:
|
1 |
CREATE FUNCTION variadic(...) { CASE WHEN array_length(args) != 1 THEN "wrong args: " || to_string(array_length(args)) WHEN type(args[0]) = "string" THEN args[0] ELSE "wrong type " || type(args[0]) || ": " || to_string(args[0]) END } |
E uma função não variada:
|
1 |
CREATE FUNCTION twoargs(arg1, arg2) { CASE WHEN type(arg1) != "string" THEN "wrong arg1 " || type(arg1) || ": " || to_string(arg1) WHEN type(arg2) != "string" THEN "wrong arg2 " || type(arg2) || ": " || to_string(arg2) ELSE arg1 || arg2 END } |
Nomes de parâmetros vs. campos de documentos
Considere a seguinte função:
|
1 |
CREATE FUNCTION docsOfType(type) { (SELECT * from `travel-sample` WHERE type=type) } |
Percebeu como a função declarou um parâmetro que tem o mesmo nome de um campo de documento? Claramente, essa função não atinge seu objetivo, pois não há como distinguir entre os dois.
Veja como usar os nomes dos parâmetros para contornar esse problema: os nomes dos parâmetros substituem os campos do documento (de qualquer forma, a consulta acima retorna todos os documentos no amostra de viagem conjunto de dados).
Para acessar os campos de documento, você tem duas opções. Referir-se ao campo do documento com seu nome totalmente qualificado, como você pode ver abaixo:
|
1 |
CREATE FUNCTION docsOfType(type) { (SELECT * from `travel-sample` WHERE travel-sample.type=type) } |
Ou então, remova a ambiguidade renomeando o parâmetro, como abaixo:
|
1 |
CREATE FUNCTION docsOfType(vType) { (SELECT * from `travel-sample` WHERE type=vType) } |
Valores de retorno com UDFs
As funções definidas pelo usuário retornam apenas um valor de qualquer tipo. Se você precisar retornar mais de um valor, retorne uma matriz ou um objeto.
Mas cuidado com seus tipos de retorno! Lembre-se de que SELECIONAR executadas dentro de UDFs retornam matrizes, portanto, mesmo que seu SELECIONAR retorna apenas um documento, ele é uma matriz de um elemento. Em vez disso, retorne o primeiro elemento da matriz.
Por exemplo, a função abaixo não faz o que você deseja:
|
1 |
CREATE FUNCTION tsample() { (SELECT * FROM `travel-sample` LIMIT 1) } |
Mas este sim:
|
1 |
CREATE FUNCTION tsample1st() { (SELECT * FROM `travel-sample` LIMIT 1)[0] } |
Uso de valores de retorno de UDF como dados para consulta
No lado oposto do espectro, como as UDFs podem retornar matrizes, elas podem ser usadas de forma proveitosa para gerar dinamicamente dados a serem consultados, da mesma forma que você faria em mecanismos relacionais com conceitos como funções de tabela ou tabelas derivadas de coleções, como no exemplo a seguir
|
1 |
SELECT * FROM tsample() sample |
Embora a função de exemplo selecione dados de um intervalo, qualquer forma válida de construir uma matriz pode ser usada para gerar os dados.
Quando você precisa atualizar uma função existente
Às vezes, é necessário redefinir uma função. A função OU SUBSTITUIR cláusula da CRIAR FUNÇÃO permite que você faça exatamente isso em uma única etapa:
|
1 |
CREATE OR REPLACE FUNCTION tsample1st() { (SELECT * FROM `travel-sample` LIMIT 1)[0] } |
Privilégios de acesso do usuário
Qualquer instrução N1QL executada dentro de um UDF é executada com os mesmos privilégios do usuário que enviou a solicitação. Portanto, seu usuário precisa ter os privilégios adequados para acessar todos os objetos aos quais o UDF em questão faz referência.
Além de tudo isso, o usuário também precisa ter o privilégio de executar funções. Existem diferentes níveis de privilégio para funções internas, externas, globais e em nível de escopo. Para criar e soltar funções, o usuário precisa ter permissão para gerenciar funções. Por exemplo:
|
1 2 |
GRANT query_manage_global_functions TO user1; GRANT query_execute_external_functions ON default:test.scope1 TO user1; |
Conclusão
Espero que este post tenha sido útil para entender quando e como aproveitar as funções definidas pelo usuário N1QL com o Couchbase 7.0. Confira a documentação de Mais informações sobre funções definidas pelo usuário, contexto em Objetos N1QL e contexto_de_consulta e funções de segurança para UDFs.
Dê uma olhada no Couchbase 7
[O post N1QL agora oferece suporte a funções definidas pelo usuário apareceu primeiro em The Couchbase [...]