SQL JSON
A Loja Aurora vai ganhar um aplicativo de celular. O programador do aplicativo pediu à Ana: "me mande os pedidos em JSON". JSON é o formato de texto que quase todo aplicativo e site usa para trocar dados. Nesta aula você vai fazer o próprio banco montar esse texto, ler um JSON guardado numa coluna e desmontar uma lista JSON em linhas de tabela. No fim, você monta o JSON completo de um pedido, com todos os seus itens, numa consulta só.
- O que é JSON: objetos
{…}, listas[…]e valores. - Montar JSON com
json_object()ejson_array(). - Ler um pedaço de um JSON com
json_extract()e com os operadores->e->>, e a diferença entre os dois. - Guardar JSON numa coluna e garantir que ele é válido com
json_valid(). - Transformar uma lista JSON em linhas com
json_each(). - Juntar várias linhas numa lista JSON com
json_group_array()ejson_group_object(). - Montar o JSON de um pedido com seus itens, e as mensagens reais de erro do SQLite.
Todos os exemplos rodam de verdade no seu navegador, no SQLite 3.50 com o banco da Loja Aurora (aula O Banco da Loja Aurora). As funções de JSON já vêm prontas no SQLite desde a versão 3.38: não é preciso instalar nada.
A ideia: uma ficha que viaja
Uma linha de tabela só faz sentido junto do cabeçalho: o valor 12.35 sozinho não diz nada; é a coluna preco que dá o sentido. Quando o dado sai do banco e vai para um aplicativo, ele precisa levar o "cabeçalho" junto. É como trocar a planilha por uma ficha preenchida, em que cada valor vem com o nome do campo ao lado:
{"id":1,"nome":"Caderno Pautado 96 folhas","preco":12.35}
Isso é JSON (de JavaScript Object Notation, "notação de objetos do JavaScript"; pronuncia-se "jêison"). Apesar do nome, qualquer linguagem lê JSON. Ele tem só três peças:
| Peça | Escrita | Comparação | Exemplo |
|---|---|---|---|
| Objeto | entre chaves { }, pares "nome": valor separados por vírgula | uma ficha com campos | {"nome":"Ana","vip":1} |
| Lista (array) | entre colchetes [ ], valores separados por vírgula | uma lista de compras | ["livros","jardim"] |
| Valor | texto entre aspas duplas, número, true, false, null, ou outro objeto/lista | o que vai escrito no campo | "Teresina", 4.9, null |
Para o SQLite, um JSON é só um texto (tipo TEXT). O que as funções desta aula fazem é escrever esse texto sem erro (com as aspas e vírgulas no lugar) e saber andar dentro dele.
Dentro do JSON, texto vai entre aspas duplas: "Ana". No SQL, texto vai entre aspas simples: 'Ana'. Por isso um JSON escrito dentro de um comando SQL fica assim: '{"nome":"Ana"}', aspas simples por fora (é um texto do SQL) e duplas por dentro (é o JSON).
1. Montando um objeto: json_object()
Primeiro, três produtos do jeito de sempre, em tabela:
SELECT id, nome, preco FROM produtos WHERE id <= 3;
Agora os mesmos três, cada um virando uma ficha JSON:
SELECT json_object('id', id, 'nome', nome, 'preco', preco) AS produto
FROM produtos
WHERE id <= 3;
- json_object(
- "objeto JSON": monta um objeto
{…}. Os argumentos vêm em pares: nome do campo, valor do campo, nome, valor… - 'id', id
- O primeiro par.
'id'(com aspas simples) é o nome que o campo vai ter no JSON;id(sem aspas) é a coluna de onde vem o valor. - 'nome', nome
- Segundo par. O SQLite põe as aspas duplas em volta do texto sozinho.
- 'preco', preco
- Terceiro par. Número vai sem aspas no JSON:
12.35. - AS produto
- O apelido da coluna (aula Apelidos (AS)). O resultado é uma coluna só, com um texto JSON por linha.
O nome do campo no JSON não precisa ser igual ao da coluna: json_object('valor', preco) gera {"valor":12.35}. Quem escolhe é você (ou o programador do aplicativo, que vai ler o JSON).
E o NULL? Ele vira o null do JSON. O cliente 7 não tem email (é um dos casos do banco):
SELECT json_object('nome', nome, 'email', email, 'vip', vip) AS cliente
FROM clientes
WHERE id IN (1, 7);
Repare também no "vip":1: no banco da Aurora o VIP é guardado como 0 ou 1, e é esse número que vai para o JSON. O SQLite não tem um tipo "verdadeiro/falso" (aula Os Tipos no SQLite), então ele não adivinha que você queria true.
Missão: o aplicativo quer a lista dos clientes VIP, cada um como uma ficha JSON com dois campos: nome e cidade, nessa ordem. Troque as duas colunas soltas por um json_object().
SELECT nome, cidade FROM clientes WHERE vip = 1;
SELECT json_object('nome', nome, 'cidade', cidade) AS cliente
FROM clientes
WHERE vip = 1;
2. Montando uma lista: json_array()
O json_array() ("lista JSON") junta os valores que você passar, na ordem, entre colchetes:
SELECT json_array('Teresina', 'Fortaleza', 42, 4.9, NULL) AS lista;
Os textos ganharam aspas duplas, os números ficaram sem aspas e o NULL virou null. Uma lista pode ter valores de tipos diferentes misturados.
Um dentro do outro
O valor de um campo pode ser outra lista (ou outro objeto). Basta colocar uma função dentro da outra:
SELECT json_object('nome', 'Ana', 'gosta', json_array('livros', 'jardim')) AS ficha;
O json_object() percebeu que o valor de gosta já era JSON (veio de uma função de JSON) e o encaixou como lista. Guarde isso: em Quando dá errado você vai ver o que acontece quando o valor é um texto que só parece JSON.
3. Lendo um pedaço: json_extract()
Agora o caminho contrário: você tem um JSON e quer um valor lá de dentro. Para isso, diga o caminho (path) até ele:
SELECT json_extract('{"nome":"Ana","cidade":"Teresina","gosta":["livros","jardim"]}', '$.cidade') AS cidade;
- json_extract(
- "extrair do JSON". Recebe o JSON e o caminho, e devolve o valor que estiver lá.
- '{"nome":"Ana",…}'
- O JSON, como texto do SQL (aspas simples por fora).
- '$.cidade'
- O caminho. O
$é "o JSON inteiro";.cidadeé "o campocidadedele". Leia como "do JSON, pegue a cidade".
Os caminhos podem descer quantos níveis precisar. Numa lista, a posição vai entre colchetes, e a contagem começa do zero:
SELECT
json_extract('{"nome":"Ana","gosta":["livros","jardim"]}', '$.gosta') AS lista,
json_extract('{"nome":"Ana","gosta":["livros","jardim"]}', '$.gosta[0]') AS primeiro,
json_extract('{"nome":"Ana","gosta":["livros","jardim"]}', '$.gosta[#-1]') AS ultimo,
json_extract('{"nome":"Ana","gosta":["livros","jardim"]}', '$.idade') AS idade;
| Caminho | Quer dizer | Aqui deu |
|---|---|---|
$ | o JSON inteiro | o próprio JSON |
$.gosta | o campo gosta | a lista ["livros","jardim"] |
$.gosta[0] | o primeiro item da lista (posição 0) | livros |
$.gosta[#-1] | o último item (# é o tamanho da lista; #-1, a última posição) | jardim |
$.idade | um campo que não existe | NULL, sem erro |
$.a.b | o campo b de dentro do objeto a | (desce um nível) |
Repare na última coluna do resultado: pedir um campo que não existe não é erro, dá NULL. Isso é bom (um cliente sem idade não derruba a consulta) e é perigoso (um nome de campo digitado errado também dá NULL calado).
4. Os atalhos -> e ->>
Escrever json_extract(…) o tempo todo cansa. O SQLite tem dois operadores que fazem quase a mesma coisa, com uma setinha: -> (uma seta) e ->> (seta dupla). A diferença entre eles é o que volta:
SELECT
'{"nome":"Ana"}' -> '$.nome' AS seta,
'{"nome":"Ana"}' ->> '$.nome' AS seta_dupla,
'{"nome":"Ana"}' ->> 'nome' AS sem_cifrao;
- ->
- Devolve o pedaço como JSON. Um texto no JSON tem aspas duplas, então volta
"Ana", com as aspas. Serve para quando o resultado vai continuar sendo JSON (para encaixar em outro JSON, por exemplo). - ->>
- Devolve o pedaço como valor do SQL: o texto
Ana, sem aspas; um número vira número de verdade. É o que você quer para comparar, somar, ordenar. - 'nome' (sem
$.) - Atalho dos atalhos: com as setas, um nome simples vale como
'$.nome', e um número como2vale como'$[2]'. (Nojson_extract()isso não vale; lá o$é obrigatório.)
O tipo do que volta mostra a diferença sem sombra de dúvida. A função typeof() diz o tipo de um valor:
SELECT
typeof(json_extract('{"n":5}', '$.n')) AS com_extract,
typeof('{"n":5}' -> '$.n') AS com_seta,
typeof('{"n":5}' ->> '$.n') AS com_seta_dupla;
O json_extract() e o ->> devolvem o número 5 (integer); o -> devolve o texto JSON 5 (text). Para o dia a dia, a regra é simples: use ->>. Deixe o -> para quando você quer um pedaço de JSON para encaixar em outro JSON.
| Forma | Texto "Ana" volta como | Lista volta como | Aceita 'nome' sem $. |
|---|---|---|---|
json_extract(j, '$.x') | Ana (valor do SQL) | texto JSON | não (erro) |
j ->> '$.x' | Ana (valor do SQL) | texto JSON | sim |
j -> '$.x' | "Ana" (JSON, com aspas) | texto JSON | sim |
5. JSON guardado numa coluna
Até aqui o JSON estava escrito na própria consulta. Na vida real ele fica guardado numa coluna. A Ana quer guardar os carrinhos do aplicativo, as compras que o cliente começou e ainda não fechou. Cada carrinho chega do aplicativo como um JSON, com a origem, o cupom (se houver) e a lista de itens. Ela cria uma tabela com uma coluna TEXT para o JSON:
CREATE TABLE carrinhos (
id INTEGER PRIMARY KEY,
cliente_id INTEGER REFERENCES clientes(id),
dados TEXT CHECK (json_valid(dados))
);
INSERT INTO carrinhos (id, cliente_id, dados) VALUES
(1, 3, '{"origem":"app","cupom":"AURORA10","itens":[{"produto_id":1,"qtd":2},{"produto_id":5,"qtd":1}]}'),
(2, 18, '{"origem":"site","itens":[{"produto_id":3,"qtd":10}]}'),
(3, 27, '{"origem":"app","itens":[]}');
SELECT id,
dados ->> '$.origem' AS origem,
dados ->> '$.cupom' AS cupom,
json_array_length(dados, '$.itens') AS n_itens
FROM carrinhos;
- dados TEXT
- O JSON fica numa coluna de texto comum. O SQLite não tem um tipo "JSON" separado.
- CHECK (json_valid(dados))
- A trava (aula CHECK):
json_valid()devolve 1 se o texto é um JSON válido e 0 se não é. Com ela, o banco recusa JSON quebrado na entrada, em vez de você descobrir o problema meses depois. - dados ->> '$.origem'
- Lê o campo
origemde cada linha. O carrinho 2 não temcupom, então ali vemNULL. - json_array_length(dados, '$.itens')
- "Tamanho da lista": quantos itens tem a lista
itens. O carrinho 3 está vazio:[]tem 0 itens.
A tabela carrinhos não existe no banco da Aurora: ela é criada no exemplo. E cada Rodar do editor parte do banco limpo (aula O Editor do Guia). Por isso, nos exemplos seguintes, o CREATE TABLE e o INSERT se repetem antes da consulta.
Com o ->>, um campo do JSON entra no WHERE como qualquer coluna:
CREATE TABLE carrinhos (id INTEGER PRIMARY KEY, cliente_id INTEGER, dados TEXT CHECK (json_valid(dados)));
INSERT INTO carrinhos VALUES
(1, 3, '{"origem":"app","cupom":"AURORA10","itens":[{"produto_id":1,"qtd":2},{"produto_id":5,"qtd":1}]}'),
(2, 18, '{"origem":"site","itens":[{"produto_id":3,"qtd":10}]}'),
(3, 27, '{"origem":"app","itens":[]}');
SELECT id, cliente_id
FROM carrinhos
WHERE dados ->> '$.origem' = 'app';
Mudando um campo: json_set()
Para mudar um pedaço do JSON guardado, não é preciso reescrever o texto todo. O json_set() devolve o JSON com o campo trocado (ou acrescentado, se ele não existia), e o UPDATE grava:
CREATE TABLE carrinhos (id INTEGER PRIMARY KEY, cliente_id INTEGER, dados TEXT CHECK (json_valid(dados)));
INSERT INTO carrinhos VALUES
(2, 18, '{"origem":"site","itens":[{"produto_id":3,"qtd":10}]}');
UPDATE carrinhos
SET dados = json_set(dados, '$.cupom', 'FRETEGRATIS')
WHERE id = 2;
SELECT dados FROM carrinhos WHERE id = 2;
O campo cupom não existia e foi acrescentado no fim. A família tem quatro irmãos, que diferem só no que fazem quando o campo já existe ou não:
SELECT
json_set('{"a":1}', '$.b', 2) AS set_novo,
json_insert('{"a":1}', '$.a', 9) AS insert_existente,
json_replace('{"a":1}', '$.a', 9) AS replace_existente,
json_remove('{"a":1,"b":2}', '$.b') AS remove;
| Função | Campo já existe | Campo não existe |
|---|---|---|
json_set() | troca o valor | cria o campo |
json_insert() | não mexe (por isso o "a" continuou 1) | cria o campo |
json_replace() | troca o valor | não mexe |
json_remove() | apaga o campo | não mexe |
Missão: a Ana quer o id dos carrinhos que vieram do site. A consulta abaixo roda sem erro, mas devolve zero linhas, e existe um carrinho do site. Descubra o porquê (releia a seção 4) e conserte só o operador.
CREATE TABLE carrinhos (id INTEGER PRIMARY KEY, cliente_id INTEGER, dados TEXT CHECK (json_valid(dados)));
INSERT INTO carrinhos VALUES
(1, 3, '{"origem":"app","cupom":"AURORA10","itens":[{"produto_id":1,"qtd":2},{"produto_id":5,"qtd":1}]}'),
(2, 18, '{"origem":"site","itens":[{"produto_id":3,"qtd":10}]}'),
(3, 27, '{"origem":"app","itens":[]}');
SELECT id
FROM carrinhos
WHERE dados -> '$.origem' = 'site';
CREATE TABLE carrinhos (id INTEGER PRIMARY KEY, cliente_id INTEGER, dados TEXT CHECK (json_valid(dados)));
INSERT INTO carrinhos VALUES
(1, 3, '{"origem":"app","cupom":"AURORA10","itens":[{"produto_id":1,"qtd":2},{"produto_id":5,"qtd":1}]}'),
(2, 18, '{"origem":"site","itens":[{"produto_id":3,"qtd":10}]}'),
(3, 27, '{"origem":"app","itens":[]}');
SELECT id
FROM carrinhos
WHERE dados ->> '$.origem' = 'site';
6. Da lista para as linhas: json_each()
Uma lista JSON numa célula é ótima para guardar, e ruim para consultar: você não consegue fazer JOIN com uma lista. O json_each() ("cada um do JSON") resolve isso: ele desmonta a lista e devolve uma linha para cada item, como se fosse uma tabela:
SELECT * FROM json_each('["livros","jardim","papelaria"]');
Ele aparece no FROM, no lugar de uma tabela: é uma função de tabela (table-valued function). As colunas que importam no começo são duas:
- key
- "Chave": a posição do item na lista (0, 1, 2…) ou, num objeto, o nome do campo.
- value
- "Valor": o próprio item, já como valor do SQL.
- type, atom, id, parent, fullkey, path
- Detalhes para JSON complicado: o tipo do item (
text,integer,object…), o caminho completo até ele (fullkey:$[0],$[1]…). Você raramente vai precisar.
Num objeto, cada campo vira uma linha, com o nome em key:
SELECT key, value
FROM json_each('{"nome":"Ana","cidade":"Teresina","vip":1}');
Os itens de cada carrinho
Agora o uso de verdade: abrir a lista itens de cada carrinho e juntar com a tabela produtos para saber o nome e o preço. O segundo argumento do json_each() é o caminho da lista dentro do JSON:
CREATE TABLE carrinhos (id INTEGER PRIMARY KEY, cliente_id INTEGER, dados TEXT CHECK (json_valid(dados)));
INSERT INTO carrinhos VALUES
(1, 3, '{"origem":"app","cupom":"AURORA10","itens":[{"produto_id":1,"qtd":2},{"produto_id":5,"qtd":1}]}'),
(2, 18, '{"origem":"site","itens":[{"produto_id":3,"qtd":10}]}'),
(3, 27, '{"origem":"app","itens":[]}');
SELECT c.id AS carrinho,
p.nome,
i.value ->> 'qtd' AS qtd,
p.preco
FROM carrinhos c
JOIN json_each(c.dados, '$.itens') i
JOIN produtos p ON p.id = i.value ->> 'produto_id';
- JOIN json_each(c.dados, '$.itens') i
- Para cada carrinho
c, desmonta a listaitensdele. Cada item vira uma linha com o apelidoi. O carrinho 3 tem a lista vazia, então não gera linha nenhuma. - i.value
- O item da vez, que é um objeto:
{"produto_id":1,"qtd":2}. - i.value ->> 'qtd'
- Dentro do item, o campo
qtd, como número. - ON p.id = i.value ->> 'produto_id'
- Liga o item ao produto pelo id, como um
JOINcomum (aula INNER JOIN).
Virou tabela, então todo o resto do SQL funciona. O total de cada carrinho, com SUM e GROUP BY:
CREATE TABLE carrinhos (id INTEGER PRIMARY KEY, cliente_id INTEGER, dados TEXT CHECK (json_valid(dados)));
INSERT INTO carrinhos VALUES
(1, 3, '{"origem":"app","cupom":"AURORA10","itens":[{"produto_id":1,"qtd":2},{"produto_id":5,"qtd":1}]}'),
(2, 18, '{"origem":"site","itens":[{"produto_id":3,"qtd":10}]}'),
(3, 27, '{"origem":"app","itens":[]}');
SELECT c.id AS carrinho,
ROUND(SUM(p.preco * (i.value ->> 'qtd')), 2) AS total
FROM carrinhos c
JOIN json_each(c.dados, '$.itens') i
JOIN produtos p ON p.id = i.value ->> 'produto_id'
GROUP BY c.id;
O carrinho 1 dá 2 × 12.35 + 1 × 89.99 = 114.69; o carrinho 2, 10 × 1.15 = 11.5.
Experimente: troque a lista por outra (pode ser de números: '[10, 20, 30]'), por um objeto, ou some os valores com SELECT SUM(value) FROM json_each('[10, 20, 30]');.
SELECT key, value, type
FROM json_each('["livros", "jardim", 42, null]');
7. Das linhas para a lista: json_group_array()
O caminho de volta. O json_group_array() é uma função agregada, da mesma família do COUNT() e do SUM() (aula O que são Funções Agregadas): ela junta os valores de várias linhas numa lista JSON só.
SELECT json_group_array(nome) AS categorias FROM categorias;
Oito linhas viraram uma lista com oito nomes. Como toda agregada, ela combina com GROUP BY (aula GROUP BY): uma lista por grupo. Os produtos de cada um dos três primeiros pedidos:
SELECT pedido_id, json_group_array(produto_id) AS produtos FROM itens_pedido WHERE pedido_id <= 3 GROUP BY pedido_id;
Quer a lista em ordem? Ponha um ORDER BY dentro dos parênteses da função:
SELECT json_group_array(nome ORDER BY nome) AS categorias FROM categorias;
Um objeto em vez de lista: json_group_object()
O irmão json_group_object(nome, valor) junta as linhas num objeto: o primeiro argumento vira o nome do campo e o segundo, o valor. Os produtos da categoria Papelaria, com seus preços:
SELECT json_group_object(nome, preco) AS precos FROM produtos WHERE categoria_id = 1;
8. O JSON de um pedido com seus itens
Agora o pedido do programador do aplicativo: o pedido 1 inteiro, num JSON só, com o cliente e a lista de itens. Primeiro, as duas partes separadas, em tabela, para ver o que vai entrar:
SELECT p.id, p.data, p.frete, c.nome AS cliente FROM pedidos p JOIN clientes c ON c.id = p.cliente_id WHERE p.id = 1; SELECT pr.nome, ip.quantidade, ip.preco_unitario FROM itens_pedido ip JOIN produtos pr ON pr.id = ip.produto_id WHERE ip.pedido_id = 1;
Os itens viram uma lista de objetos com json_group_array(json_object(…)): o json_object() monta a ficha de cada item, e o json_group_array() junta as fichas numa lista:
SELECT json_group_array(
json_object('produto', pr.nome, 'qtd', ip.quantidade, 'preco', ip.preco_unitario)
) AS itens
FROM itens_pedido ip
JOIN produtos pr ON pr.id = ip.produto_id
WHERE ip.pedido_id = 1;
Por fim, essa lista entra como o campo itens do objeto do pedido, por uma subconsulta (aula Subconsultas) que roda para cada pedido:
SELECT json_object(
'pedido', p.id,
'data', p.data,
'cliente', c.nome,
'frete', p.frete,
'itens', (SELECT json_group_array(
json_object('produto', pr.nome,
'qtd', ip.quantidade,
'preco', ip.preco_unitario))
FROM itens_pedido ip
JOIN produtos pr ON pr.id = ip.produto_id
WHERE ip.pedido_id = p.id)
) AS pedido_json
FROM pedidos p
JOIN clientes c ON c.id = p.cliente_id
WHERE p.id = 1;
- A consulta de fora pega o pedido 1 e o nome do cliente (José Ribamar Silva), como sempre.
- Para esse pedido, a subconsulta entre parênteses monta a lista de itens. O
WHERE ip.pedido_id = p.idliga a subconsulta ao pedido de fora. - O
json_object()de fora junta tudo. Como o valor deitensveio de uma função de JSON, ele entra como lista de verdade, e não como texto entre aspas.
Esse texto pode ir direto para o aplicativo. Para ler com calma, o json_pretty() devolve o mesmo JSON com quebras de linha e recuo. Ele é recente (SQLite 3.46) e serve para gente ler, não para guardar: SELECT json_pretty(json_object('pedido', 1, 'itens', json_array(1, 2)));.
9. No terminal: o modo JSON do sqlite3
O programa sqlite3 (aula Comandos de Ponto) tem um jeito de mostrar qualquer resultado como JSON, sem função nenhuma: a opção -json (ou o comando .mode json dentro dele). Cada linha vira um objeto, e o resultado inteiro vira uma lista:
$ sqlite3 -json loja.db "SELECT id, nome, preco FROM produtos WHERE id <= 2;"
[{"id":1,"nome":"Caderno Pautado 96 folhas","preco":12.34999999999999965},
{"id":2,"nome":"Caneta Esferográfica Azul","preco":2.790000000000000035}]
$ sqlite3 -json loja.db "SELECT nome, email FROM clientes WHERE id = 7;"
[{"nome":"Luís Otávio Brito","email":null}]
$ sqlite3 loja.db "SELECT json_object('id', id, 'preco', preco) FROM produtos WHERE id <= 2;"
{"id":1,"preco":12.35}
{"id":2,"preco":2.79}
Repare no preço: o modo JSON escreveu 12.34999999999999965, enquanto o json_object() escreveu 12.35. Não é erro de nenhum dos dois. Um REAL é guardado em binário, e 12.35 não tem representação exata em binário; o modo JSON mostra todos os dígitos, e as funções de JSON escrevem o número mais curto que volta ao mesmo valor. Para dinheiro, prefira montar o JSON com as funções. O .mode json é só do programa sqlite3: no editor do guia, ele não existe.
Formas diferentes de fazer a mesma coisa
Ler o campo origem do carrinho, de três jeitos:
SELECT
json_extract('{"origem":"app"}', '$.origem') AS com_extract,
'{"origem":"app"}' ->> '$.origem' AS com_seta_dupla,
'{"origem":"app"}' ->> 'origem' AS curto;
E a lista de produtos do pedido 1, montada com json_group_array() ou escrita à mão com json_array():
SELECT json_group_array(produto_id) AS agregada FROM itens_pedido WHERE pedido_id = 1; SELECT json_array(1, 2, 3) AS a_mao;
| Para… | Use | Por quê |
|---|---|---|
| Ler um valor para comparar, somar, ordenar | ->> | Curto e devolve valor do SQL; é a forma que o MySQL e o PostgreSQL também entendem |
| Ler um valor num SQL que precisa rodar em versões antigas do SQLite | json_extract() | É a forma mais antiga; as setas só chegaram no SQLite 3.38 |
| Pegar um pedaço que vai continuar sendo JSON | -> | Mantém as aspas e a forma de JSON |
| Montar uma lista com valores que você já sabe | json_array() | Os valores vão direto nos parênteses |
| Montar uma lista com as linhas de uma consulta | json_group_array() | É agregada: junta quantas linhas vierem |
| Despejar um resultado inteiro em JSON, no terminal | sqlite3 -json | Sem escrever função nenhuma (cuidado com os decimais) |
É tentador guardar tudo em JSON: "os itens do pedido? põe uma lista na coluna". Mas o banco não consegue garantir nada lá dentro: não há FOREIGN KEY para o produto_id do JSON, nem NOT NULL na qtd. Os carrinhos da Ana são um bom uso, porque são rascunhos que chegam prontos do aplicativo. Os pedidos de verdade ficam em pedidos e itens_pedido, com cada coisa na sua coluna (aula Normalização). Use JSON para trocar dados e para guardar o que não tem forma fixa.
Quando dá errado
-> e não achou nada
SELECT '{"nome":"Ana"}' -> '$.nome' = 'Ana' AS com_seta,
'{"nome":"Ana"}' ->> '$.nome' = 'Ana' AS com_seta_dupla;
O que acontece: nenhum erro, e é isso que torna este o engano mais comum. A comparação com -> dá 0 (falso), porque o -> devolve "Ana" com as aspas, e "Ana" não é igual a Ana. Num WHERE, a consulta volta vazia e você acha que não existe o dado.
Como consertar: para comparar, use ->> (ou json_extract()).
SELECT json('isso nao e json');
A mensagem: malformed JSON, "JSON malformado". As funções que precisam entender o JSON param a consulta com esse erro. A função json() do exemplo só confere o texto e o devolve arrumado; é um bom jeito de testar um JSON.
Na tabela com a trava CHECK (json_valid(dados)), um JSON quebrado nem entra. O texto abaixo foi cortado no meio:
CREATE TABLE carrinhos (id INTEGER PRIMARY KEY, cliente_id INTEGER, dados TEXT CHECK (json_valid(dados)));
INSERT INTO carrinhos VALUES (4, 1, '{"origem":"app",');
A mensagem: CHECK constraint failed: json_valid(dados), "a trava CHECK falhou". Melhor ouvir isso na hora de gravar do que achar o JSON quebrado depois.
Um detalhe: o SQLite (desde o 3.42) aceita na leitura algumas folgas do JSON5, como uma vírgula sobrando no fim ('{"nome":"Ana",}'). Mas o json_valid() sem opções segue a regra estrita do JSON e diz 0 para esse texto. Outros programas também podem recusá-lo, então escreva sempre o JSON certinho.
$ no caminho
SELECT json_extract('{"nome":"Ana"}', 'nome');
A mensagem: bad JSON path: 'nome', "caminho JSON ruim". No json_extract(), todo caminho começa com $. O mesmo erro aparece para '$nome' (faltou o ponto) e '$.a.' (ponto sobrando no fim).
Como consertar: '$.nome'. Só as setas aceitam o nome sozinho: '{"nome":"Ana"}' ->> 'nome' funciona.
json_object()
SELECT json_object('nome', nome, 'cidade') FROM clientes;
A mensagem: json_object() requires an even number of arguments, "json_object() exige um número par de argumentos". Os argumentos vêm em pares nome-valor; aqui o campo 'cidade' ficou sem valor. Outro parecido: json_object(1, 'Ana') dá json_object() labels must be TEXT, "os nomes dos campos têm de ser texto".
Como consertar: complete o par: 'cidade', cidade.
Como o JSON usa aspas duplas, é comum escrever o nome do campo com elas também:
SELECT json_object("nome", 'Ana');
A mensagem: no such column: "nome" - should this be a string literal in single-quotes?, "não existe a coluna "nome"; isto devia ser um texto entre aspas simples?". No SQL, aspas duplas marcam nome de coluna, não texto. O próprio SQLite sugere o conserto.
Como consertar: no SQL, aspas simples: json_object('nome', 'Ana'). As aspas duplas do JSON quem põe é a função.
SELECT json_object('nome', 'Ana', 'gosta', '["livros","jardim"]') AS errado,
json_object('nome', 'Ana', 'gosta', json('["livros","jardim"]')) AS certo;
O que acontece: sem erro, mas na primeira coluna a lista virou um texto entre aspas, cheio de \" (a barra é como o JSON escreve uma aspa dentro de um texto). Para o json_object(), '["livros","jardim"]' é um texto como outro qualquer. O aplicativo recebe uma frase, não uma lista.
Como consertar: passe o valor por json() (ou monte com json_array()): assim o SQLite sabe que aquilo é JSON. Valores tirados de uma coluna de JSON com -> também entram como JSON.
Todo banco grande tem JSON, mas os nomes mudam. As setas do SQLite foram feitas para serem compatíveis com o MySQL e o PostgreSQL. Os trechos abaixo são ilustrativos: não rodam no SQLite.
- MySQL:
JSON_OBJECT(),JSON_EXTRACT()e as setascoluna->'$.nome'ecoluna->>'$.nome'. Na documentação, à esquerda da seta vai o nome de uma coluna, não uma expressão qualquer. - PostgreSQL: tem os tipos
jsonejsonb. As setas recebem o nome do campo ou a posição, sem$:dados ->> 'origem',lista -> 2. Para montar:json_build_object()ejson_agg(); para desmontar uma lista:json_array_elements(). - SQL Server:
JSON_VALUE(coluna, '$.nome')para um valor simples eJSON_QUERY()para um pedaço que é objeto ou lista;OPENJSON()faz o papel dojson_each().
-- PostgreSQL (ilustrativo) SELECT id, dados ->> 'origem' AS origem FROM carrinhos; -- SQL Server (ilustrativo) SELECT id, JSON_VALUE(dados, '$.origem') AS origem FROM carrinhos;
Referência rápida
| Escrita | O que faz | Exemplo → resultado |
|---|---|---|
json_object(n1, v1, n2, v2…) | Monta um objeto com pares nome-valor | json_object('a', 1) → {"a":1} |
json_array(v1, v2…) | Monta uma lista | json_array(1, 'x') → [1,"x"] |
json_extract(j, caminho) | Lê um valor (como valor do SQL) | json_extract('{"a":1}', '$.a') → 1 |
j ->> caminho | Lê um valor como valor do SQL | '{"a":"x"}' ->> 'a' → x |
j -> caminho | Lê um pedaço como JSON | '{"a":"x"}' -> 'a' → "x" |
json_each(j [, caminho]) | Função de tabela: uma linha por item (colunas key, value…) | SELECT value FROM json_each('[1,2]') |
json_group_array(x) | Agregada: junta as linhas numa lista | json_group_array(nome) |
json_group_object(n, v) | Agregada: junta as linhas num objeto | json_group_object(nome, preco) |
json_valid(j) | 1 se é JSON válido, 0 se não | CHECK (json_valid(dados)) |
json(j) | Confere e devolve o JSON arrumado; marca o texto como JSON | json('[1, 2]') → [1,2] |
json_array_length(j [, caminho]) | Quantos itens tem a lista | json_array_length('[1,2,3]') → 3 |
json_type(j [, caminho]) | O tipo do pedaço: object, array, text, integer… | json_type('{"a":[1]}', '$.a') → array |
json_set / json_insert / json_replace / json_remove | Devolvem o JSON com um campo trocado, criado ou apagado | json_set('{}', '$.a', 1) → {"a":1} |
json_pretty(j) | JSON com quebras de linha e recuo, para ler | (3.46 ou mais novo) |
| Mensagem do SQLite | Quer dizer |
|---|---|
malformed JSON | O texto não é um JSON válido |
bad JSON path: 'nome' | O caminho está mal escrito (falta o $, falta ou sobra ponto) |
json_object() requires an even number of arguments | Um nome de campo ficou sem valor |
json_object() labels must be TEXT | O nome do campo não é texto |
CHECK constraint failed: json_valid(dados) | A trava recusou um JSON inválido |
Exercícios
1. Montar uma ficha
Complete para gerar {"nome":"Lápis Grafite HB"} a partir do produto 3:
2. O caminho
Complete o caminho para ler o campo cupom com json_extract():
3. Qual é o resultado de SELECT '{"cidade":"Picos"}' -> '$.cidade';?
4. Você quer transformar a lista JSON '[4, 8, 15]' em três linhas, para fazer um JOIN. Qual função usa?
5. O que dá json_extract('{"nome":"Ana"}', '$.idade')?
6. Por que json_object('gosta', '["livros"]') não gera uma lista dentro do objeto?
Missão final: o pedido 2 em JSON
Missão: a consulta abaixo lista os itens do pedido 2 em tabela. Transforme-a numa linha só, com um JSON assim: campos pedido (o id), cliente (o nome) e itens, uma lista de objetos com produto (o nome do produto) e qtd (a quantidade), nessa ordem. Dica: siga a seção 8, com uma subconsulta para os itens.
SELECT p.id, c.nome, pr.nome, ip.quantidade FROM pedidos p JOIN clientes c ON c.id = p.cliente_id JOIN itens_pedido ip ON ip.pedido_id = p.id JOIN produtos pr ON pr.id = ip.produto_id WHERE p.id = 2;
SELECT json_object(
'pedido', p.id,
'cliente', c.nome,
'itens', (SELECT json_group_array(json_object('produto', pr.nome, 'qtd', ip.quantidade))
FROM itens_pedido ip
JOIN produtos pr ON pr.id = ip.produto_id
WHERE ip.pedido_id = p.id)
) AS pedido_json
FROM pedidos p
JOIN clientes c ON c.id = p.cliente_id
WHERE p.id = 2;
Se deu certo, a resposta tem uma linha só, com a Maria Clara Mendes e três itens: Marca-texto Amarelo, Mochila Escolar Azul (2 unidades) e Estojo Duplo. Troque o WHERE p.id = 2 por WHERE p.id <= 3 e veja três pedidos, cada um no seu JSON.
Resumo
- JSON é texto com objetos
{"nome": valor}e listas[…]. No SQLite, ele fica numa colunaTEXT. json_object('nome', valor, …)monta um objeto;json_array(…), uma lista. Uma função pode ir dentro da outra.- Para ler:
json_extract(j, '$.campo')ouj ->> 'campo', que devolvem valor do SQL. O->devolve JSON (texto com aspas): não use para comparar. - Campo que não existe dá
NULL; caminho mal escrito dábad JSON path; texto que não é JSON dámalformed JSON. CHECK (json_valid(dados))barra JSON quebrado na entrada.json_each()desmonta uma lista em linhas (vai noFROM);json_group_array()ejson_group_object()juntam linhas em JSON.- O JSON de um pedido com itens:
json_object(…, 'itens', (SELECT json_group_array(json_object(…)) …)).