Como executar operações de atualização em colunas do tipo JSONB no Postgres 9.4

132

Examinando a documentação do tipo de dados JSONB do Postgres 9.4, não é imediatamente óbvio para mim como fazer atualizações nas colunas JSONB.

Documentação para tipos e funções JSONB:

http://www.postgresql.org/docs/9.4/static/functions-json.html http://www.postgresql.org/docs/9.4/static/datatype-json.html

Como exemplo, eu tenho essa estrutura de tabela básica:

CREATE TABLE test(id serial, data jsonb);

A inserção é fácil, como em:

INSERT INTO test(data) values ('{"name": "my-name", "tags": ["tag1", "tag2"]}');

Agora, como eu atualizaria a coluna 'dados'? Esta é uma sintaxe inválida:

UPDATE test SET data->'name' = 'my-other-name' WHERE id = 1;

Isso está documentado em algum lugar óbvio que eu perdi? Obrigado.

jvous
fonte

Respostas:

32

Idealmente, você não usa documentos JSON para dados estruturados e regulares que deseja manipular dentro de um banco de dados relacional. Use um design relacional normalizado .

O JSON se destina principalmente a armazenar documentos inteiros que não precisam ser manipulados dentro do RDBMS. Palavras-chave:

A atualização de uma linha no Postgres sempre grava uma nova versão da linha inteira . Esse é o princípio básico do modelo MVCC do Postgres . De uma perspectiva de desempenho, pouco importa se você altera uma única parte dos dados dentro de um objeto JSON ou todos eles: uma nova versão da linha precisa ser gravada.

Assim, o conselho no manual :

Os dados JSON estão sujeitos às mesmas considerações de controle de simultaneidade que qualquer outro tipo de dados quando armazenados em uma tabela. Embora seja possível armazenar documentos grandes, lembre-se de que qualquer atualização adquire um bloqueio no nível da linha em toda a linha. Considere limitar os documentos JSON a um tamanho gerenciável para diminuir a contenção de bloqueio entre as transações de atualização. Idealmente, cada documento JSON deve representar um dado atômico que as regras de negócios determinam, não pode ser razoavelmente subdividido em dados menores que podem ser modificados independentemente.

O essencial: para modificar qualquer coisa dentro de um objeto JSON, você deve atribuir um objeto modificado à coluna. O Postgres fornece meios limitados para criar e manipular jsondados, além de seus recursos de armazenamento. O arsenal de ferramentas cresceu substancialmente a cada novo lançamento desde a versão 9.2. Mas o principal permanece: você sempre deve atribuir um objeto modificado completo à coluna e o Postgres sempre grava uma nova versão de linha para qualquer atualização.

Algumas técnicas de como trabalhar com as ferramentas do Postgres 9.3 ou posterior:

Esta resposta atraiu tantos votos negativos quanto todas as minhas outras respostas no SO juntos . As pessoas não parecem gostar da idéia: um design normalizado é superior para dados não dinâmicos. Este excelente post de Craig Ringer explica em mais detalhes:

Erwin Brandstetter
fonte
6
Esta resposta refere-se apenas ao tipo JSON e ignora JSONB.
Fiatjaf 15/07/2015
7
@ fiatjaf: Esta resposta é totalmente aplicável aos tipos de dados jsone jsonbsimilares. Ambos armazenam dados JSON, jsonbem um formato binário normalizado que possui algumas vantagens (e poucas desvantagens). stackoverflow.com/a/10560761/939860 Nenhum tipo de dado é bom para manipular muito dentro do banco de dados. Nenhum tipo de documento é. Bem, tudo bem para documentos JSON pequenos e pouco estruturados. Mas documentos grandes e aninhados seriam uma loucura.
Erwin Brandstetter
7
"Instruções para trabalhar com as ferramentas do Postgres 9.3" realmente são as primeiras em sua resposta, pois respondem à pergunta .. às vezes faz sentido atualizar o json para alterações de manutenção / esquema etc. e os motivos para não atualizar o json don realmente se aplica
Michael Wasser
22
Esta resposta não é útil, desculpe. @ jvous, você não quer aceitar a resposta de Jimothy, pois realmente responde à sua pergunta?
Bastian Voigt
10
Responda à pergunta antes de adicionar seu próprio comentário / opinião / discussão.
Ppp
330

Se você pode atualizar para o Postgresql 9.5, o jsonb_setcomando está disponível, como outros já mencionaram.

Em cada uma das seguintes instruções SQL, omiti a wherecláusula por brevidade; obviamente, você gostaria de adicionar isso de volta.

Nome da atualização:

UPDATE test SET data = jsonb_set(data, '{name}', '"my-other-name"');

Substitua as tags (em vez de adicionar ou remover tags):

UPDATE test SET data = jsonb_set(data, '{tags}', '["tag3", "tag4"]');

Substituindo a segunda tag (indexada 0):

UPDATE test SET data = jsonb_set(data, '{tags,1}', '"tag5"');

Anexar uma tag ( isso funcionará desde que haja menos de 999 tags; a alteração do argumento 999 para 1000 ou acima gera um erro . Isso não parece mais ser o caso no Postgres 9.5.3; um índice muito maior pode ser usado) :

UPDATE test SET data = jsonb_set(data, '{tags,999999999}', '"tag6"', true);

Remova a última tag:

UPDATE test SET data = data #- '{tags,-1}'

Atualização complexa (exclua a última tag, insira uma nova tag e altere o nome):

UPDATE test SET data = jsonb_set(
    jsonb_set(data #- '{tags,-1}', '{tags,999999999}', '"tag3"', true), 
    '{name}', '"my-other-name"');

É importante observar que em cada um desses exemplos, na verdade, você não está atualizando um único campo dos dados JSON. Em vez disso, você está criando uma versão modificada temporária dos dados e atribuindo essa versão modificada de volta à coluna. Na prática, o resultado deve ser o mesmo, mas manter isso em mente deve tornar as atualizações complexas, como o último exemplo, mais compreensíveis.

No exemplo complexo, há três transformações e três versões temporárias: Primeiro, a última tag é removida. Em seguida, essa versão é transformada adicionando uma nova tag. Em seguida, a segunda versão é transformada alterando o namecampo. O valor na datacoluna é substituído pela versão final.

Jimothy
fonte
42
você ganha pontos de bônus para mostrar como atualizar uma coluna em uma tabela como o OP pediu
chadrik
1
@chadrik: adicionei um exemplo mais complexo. Não faz exatamente o que você solicitou, mas deve lhe dar uma ideia. Observe que a entrada para a jsonb_setchamada externa é a saída da chamada interna e que a entrada para essa chamada interna é o resultado de data #- '{tags,-1}'. Ou seja, os dados originais com a última tag removida.
Jimothy
1
@PranaySoni: Para esse propósito, eu provavelmente usaria um procedimento armazenado ou, se a sobrecarga não for uma preocupação, traga esses dados de volta, manipule-os no idioma do aplicativo e depois os escreva novamente. Isso parece pesado, mas lembre-se, em todos os exemplos que eu dei, você ainda não está atualizando um único campo no JSON (B): você está substituindo a coluna inteira de qualquer maneira. Portanto, um processo armazenado não é realmente diferente.
Jimothy
1
@ Alex: Sim, um pouco de um hack. Se eu dissesse {tags,0}, isso significaria "o primeiro elemento da matriz tags", permitindo que eu desse um novo valor a esse elemento. Usando um número grande em vez de 0, em vez de substituir um elemento existente na matriz, ele adiciona um novo elemento à matriz. No entanto, se a matriz tivesse realmente mais de 999.999.999 elementos, isso substituiria o último elemento em vez de adicionar um novo.
Jimothy
1
e se o campo contiver nulo? Parece que não funciona. Por exemplo, o campo info jsonb é nulo: "UPDATE organizer SET info = jsonb_set (info, '{country}', '" FRA "') onde info - >> 'country' :: text IS NULL;" recebo o registro UPDATE 105, mas nenhuma alteração no db
stackdave
24

Isso está chegando na versão 9.5, na forma de jsonb_set, por Andrew Dunstan, com base em uma extensão existente, jsonbx, que funciona com a 9.4

philofinfinitejest
fonte
Outro problema nessa linha é o uso de jsonb_build_object(), porque x->key, não retorna o par de chave-objeto, para preencher o necessário jsonb_set(target, path, jsonb_build_object('key',x->key)).
Peter Krauss
18

Para aqueles que se deparam com esse problema e desejam uma correção muito rápida (e estão presos no 9.4.5 ou anterior), aqui está o que eu fiz:

Criação da tabela de teste

CREATE TABLE test(id serial, data jsonb);
INSERT INTO test(data) values ('{"name": "my-name", "tags": ["tag1", "tag2"]}');

Instrução de atualização para alterar o nome da propriedade jsonb

UPDATE test 
SET data = replace(data::TEXT,'"name":','"my-other-name":')::jsonb 
WHERE id = 1;

Por fim, a resposta aceita está correta, pois você não pode modificar uma parte individual de um objeto jsonb (na 9.4.5 ou anterior); no entanto, você pode converter o objeto jsonb em uma string (:: TEXT) e, em seguida, manipular a string e converter de volta no objeto jsonb (:: jsonb).

Existem duas advertências importantes

  1. isso substituirá todas as propriedades chamadas "name" no json (no caso de você ter várias propriedades com o mesmo nome)
  2. isso não é tão eficiente quanto o jsonb_set seria se você estivesse usando 9.5

Com isso dito, me deparei com uma situação em que eu tinha que atualizar o esquema para o conteúdo nos objetos jsonb e essa era a maneira mais simples de realizar exatamente o que o pôster original estava pedindo.

Chad Capra
fonte
1
Meu Deus, eu estive pesquisando como fazer uma atualização para o jsonb por duas horas, para que eu pudesse substituir todos \u0000os caracteres nulos, o exemplo mostrou a imagem completa. Obrigado por isso!
Joshua Robinson
3
parece bom! Entre o segundo argumento a ser substituído no seu exemplo inclui dois pontos e o terceiro não. Parece que sua chamada deve serreplace(data::TEXT, '"name":', '"my-other-name":')::jsonb
Davidicus
Obrigado @davidicus! Desculpe a atualização muito atrasada, mas agradeço o compartilhamento de outras pessoas!
Chad Capra
12

Essa pergunta foi feita no contexto do postgres 9.4, no entanto, os novos visualizadores que chegarem a essa pergunta devem estar cientes de que no postgres 9.5, o sub-documento Criar / Atualizar / Excluir operações nos campos JSONB é suportado nativamente pelo banco de dados, sem a necessidade de extensão funções.

Consulte: JSONB modificando operadores e funções

bguiz
fonte
7

atualize o atributo 'name':

UPDATE test SET data=data||'{"name":"my-other-name"}' WHERE id = 1;

e se você deseja remover, por exemplo, os atributos 'name' e 'tags':

UPDATE test SET data=data-'{"name","tags"}'::text[] WHERE id = 1;
Arthur
fonte
5

Escrevi para mim uma pequena função que funciona recursivamente no Postgres 9.4. Eu tive o mesmo problema (bom que eles resolveram um pouco dessa dor de cabeça no Postgres 9.5). Enfim, aqui está a função (espero que funcione bem para você):

CREATE OR REPLACE FUNCTION jsonb_update(val1 JSONB,val2 JSONB)
RETURNS JSONB AS $$
DECLARE
    result JSONB;
    v RECORD;
BEGIN
    IF jsonb_typeof(val2) = 'null'
    THEN 
        RETURN val1;
    END IF;

    result = val1;

    FOR v IN SELECT key, value FROM jsonb_each(val2) LOOP

        IF jsonb_typeof(val2->v.key) = 'object'
            THEN
                result = result || jsonb_build_object(v.key, jsonb_update(val1->v.key, val2->v.key));
            ELSE
                result = result || jsonb_build_object(v.key, v.value);
        END IF;
    END LOOP;

    RETURN result;
END;
$$ LANGUAGE plpgsql;

Aqui está o uso da amostra:

select jsonb_update('{"a":{"b":{"c":{"d":5,"dd":6},"cc":1}},"aaa":5}'::jsonb, '{"a":{"b":{"c":{"d":15}}},"aa":9}'::jsonb);
                            jsonb_update                             
---------------------------------------------------------------------
 {"a": {"b": {"c": {"d": 15, "dd": 6}, "cc": 1}}, "aa": 9, "aaa": 5}
(1 row)

Como você pode ver, analise no fundo e atualize / adicione valores sempre que necessário.

J. Raczkiewicz
fonte
Isso não funciona no 9.4, porque jsonb_build_objectfoi introduzido no 9.5
Greg
@ Greg Você está certo, acabei de verificar e estou executando o PostgreSQL 9.5 agora - é por isso que funciona. Obrigado por apontar isso - minha solução não funcionará na 9.4.
J. Raczkiewicz
4

Talvez: UPDATE test SET data = '"meu-outro-nome"' :: json WHERE id = 1;

Funcionou com o meu caso, em que dados são do tipo json

Gianluigi Sartori
fonte
1
Também funcionou para mim no postgresql 9.4.5. O registro inteiro é reescrito para que não seja possível atualizar um único atm de campo.
Kometen