Knex: Adicionando capacidade de tipo upsert

Criado em 30 ago. 2013  ·  54Comentários  ·  Fonte: knex/knex

Trazido por @adamscybot em tgriesser/bookshelf#55 - esse pode ser um bom recurso para adicionar.

feature request

Comentários muito úteis

@NicolajKN Você não deve usar toString(), pode causar muitos tipos de problemas e não passará valores por meio de ligações para o banco de dados (potencial falha de segurança de injeção de SQL).

Mesmo isso feito corretamente ficaria assim:

const query = knex('account').insert(accounts);
const safeQuery = knex.raw('? ON CONFLICT DO NOTHING', [query]);

Todos 54 comentários

Concordo, isso _seria_ um bom recurso!

:+1:

:+1:

Estou importando alguns dados de um CSV e há uma boa chance de que alguns dos registros se sobreponham à última importação (ou seja, a última vez foi importada de 1º de janeiro a 31 de maio, desta vez importando de 31 de maio a 18 de junho).

Felizmente, o sistema de terceiros atribui IDs exclusivos de forma confiável.

Qual é a melhor maneira de inserir os novos registros e atualizar os antigos?

Ainda não testei, mas pensei que seria algo assim:

var ids = records.map(function (json) { return json.id })
  ;

Records.forge(ids).fetchAll().then(function () {
  records.forEach(function (record) {
    // now the existing records are loaded in the collection ?
    Object.keys(record).forEach(function (key) {
      Records.forge(record.id).set(key, record[key]);
    });
  });
  Records.invokeThen('save').then(function () {
    console.log('Records have been either inserted or updated');
  });
});

Além disso, às vezes o que estou armazenando é armazenado por um determinado valor de id, como um hash. Nesses casos, quero apenas adicionar ou substituir os dados.

Eu nem sempre uso SQL como SQL tradicional. Muitas vezes eu o uso como NoSQL híbrido com o benefício de mapeamento de relacionamento e índices claros.

:+1:

Oi,
há novidades sobre esse novo recurso?

Ou alguém pode recomendar exemplos, que mostram como simular essa funcionalidade para o mysql?

THX

No momento estou fazendo isso com raw , mas estou trabalhando duro para disponibilizar isso aqui em breve.

O Postgres acabou de implementar o suporte upsert :+1:

http://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=168d5805e4c08bed7b95d351bf097cff7c07dd65

https://news.ycombinator.com/item?id=9509870

A sintaxe é INSERT ... ON CONFLICT DO UPDATE

Eu estava procurando uma maneira de fazer um REPLACE INTO no MySql e encontrei esta solicitação de recurso. Como REPLACE e INSERT têm exatamente a mesma sintaxe no MySql, imagino que seja mais fácil de implementar do que um ON DUPLICATE KEY UPDATE . Existem planos para implementar um REPLACE ? Um PR seria algo de valor?

Alguma atualização sobre isso, especialmente com o PostreSQL 9.5?

Acho que uma questão importante é expor ou não a mesma assinatura de método upsert para diferentes dialetos, como PostgreSQL e MySQL. No Sequelize, foi levantado um problema em relação ao valor de retorno de upsert : https://github.com/sequelize/sequelize/issues/3354.

Percebo que alguns dos métodos da biblioteca KnexJS têm distinções em relação aos valores de retorno no contexto de diferentes dialetos (como insert , onde um array do primeiro id inserido é retornado para SQLite e MySQL, enquanto um array de todos os id's inseridos são retornados com PostgreSQL).

De acordo com a documentação, a sintaxe INSERT ... ON DUPLICATE KEY UPDATE no MySQL tem o seguinte comportamento (http://dev.mysql.com/doc/refman/5.7/en/insert-on-duplicate.html):

Com ON DUPLICATE KEY UPDATE, o valor das linhas afetadas por linha é 1 se a linha for inserida como uma nova linha, 2 se uma linha existente for atualizada e 0 se uma linha existente for definida com seus valores atuais.

Enquanto estiver no PostgreSQL (http://www.postgresql.org/docs/9.5/static/sql-insert.html):

Após a conclusão bem-sucedida, um comando INSERT retorna uma tag de comando no formato

INSERT oid count

A contagem é o número de linhas inseridas ou atualizadas. Se count for exatamente um e a tabela de destino tiver OIDs, oid será o OID atribuído à linha inserida. A única linha deve ter sido inserida em vez de atualizada. Caso contrário, oid é zero.

Se o comando INSERT contiver uma cláusula RETURNING, o resultado será semelhante ao de uma instrução SELECT contendo as colunas e valores definidos na lista RETURNING, computados sobre a(s) linha(s) inserida(s) ou atualizada(s) pelo comando.

Neste caso, os valores de retorno podem ser alterados com a cláusula RETURNING .

Pensamentos?

Eu remendei Client_PG para adicionar o método "onConflict" para inserir. Suponha que queremos fazer upsert das credenciais oauth do github, podemos escrever a consulta assim:

const profile = {
    access_token: "blah blah",
    username: "foobar",
    // ... etc
  }

  const oauth = {
    uid: "13344398",
    provider: "github",
    created_at: new Date(),
    updated_at: new Date(),
    info: profile,
  };

  // todo: add a "timestamp" method

const insert = knex("oauths").insert(oauth).onConflict(["provider", "uid"],{
  info: profile,
  updated_at: new Date(),
});

console.log(insert.toString())

A matriz de nomes de coluna especifica a restrição de exclusividade.

insert into "authentications" ("created_at", "info", "provider", "uid", "updated_at") values ('2016-02-14T14:42:18.342+08:00', '{\"access_token\":\"blah blah\",\"username\":\"foobar\"}', 'github', '13344398', '2016-02-14T14:42:18.342+08:00') on conflict ("provider", "uid")  do update set "info" = '{\"access_token\":\"blah blah\",\"username\":\"foobar\"}', "updated_at" = '2016-02-14T14:42:18.343+08:00'

Veja a essência: https://gist.github.com/hayeah/1c8d642df5cfeabc2a5b para o patch de macaco.

Este é um experimento super hacky... então não copie e cole exatamente o patch de macaco em seu código de produção: p

Problemas conhecidos:

  • O patch de macaco está no QueryBuilder afeta todos os dialetos, porque Client_PG não especializa o construtor.
  • Não suporta atualização bruta como count = count + 1
  • onConflict provavelmente deve ser lançado se o método de consulta não for inserido.

Comentários?

@hayeah Eu gosto da sua abordagem e combina com o Postgres. Vou tentar seu patch de macaco em um projeto para ver se consigo detectar empiricamente quaisquer problemas além dos que você apontou.

Sugestão de Sintaxe: knex('table').upsert(['col1','col2']).insert({...}).update({...}); onde upsert receberia a declaração de condição. Desta forma, não é específico do banco de dados.

O resumo das diferentes implementações de upserts pode ser encontrado em https://en.wikipedia.org/wiki/Merge_ (SQL)

Estou interessado em ter essa capacidade também. Caso de uso: construir um sistema que dependa de muitos dados externos de um serviço externo; Eu periodicamente faço uma pesquisa para dados que salvei em um banco de dados MySQL local. Provavelmente estará usando o knex.raw por enquanto.

Também estou interessado, mas no meu caso de uso precisaria que funcionasse de uma maneira que não fosse baseada em conflitos, pois as colunas nem sempre têm restrições 'únicas' - basta atualizar as entradas correspondentes à consulta, se existirem, caso contrário, insira novas linhas.

@haywirez Estou curioso para saber por que não há restrições exclusivas? Você não estaria exposto a condições de corrida?

@hayeah Eu tenho um caso de uso específico com dados de janela de tempo, armazenando entradas que têm um valor vinculado a um determinado dia. Portanto, estou inserindo e atualizando entradas que possuem uma "chave combinada" de um timestamp (dia) correspondente e dois outros IDs correspondentes a PKs em outras tabelas. Dentro de uma janela de 24 horas, tenho que inseri-los ou atualizá-los com as contagens mais recentes.

Este seria um ótimo recurso para ter!

Olá a todos que já comentaram aqui. Estou adicionando um rótulo PR Please.

Feliz em receber um PR adicionando essa funcionalidade, mas gostaria de ver uma discussão sobre a API desejada aqui primeiro.

PS.

^ Acordado.

Vou excluir comentários como este, se você quiser adicionar um +1, faça isso com a pequena reação de emoji.

Eu tenho um pouco de problema com a matriz de restrições de coluna, como nos exemplos de @willfarrell e @hayeah . Não tenho certeza se esses exemplos podem suportar propriedades json . Existe uma razão para nenhuma dessas propostas não incluir instruções where / "consultas" adequadas para corresponder ao registro?

proposta 1

knex('table')
  .where('id', '=', data.id)
  .upsert(data)

proposta 2

knex('table')
  .upsertQuery(knex => {
    return knex('table')
      .where('id', '=', data.id)
  })
  .upsertUpdate(knex => {
    return knex('table')
      .insert(data)
  })

proposta 3

knex('table')
  .where('id', '=', data.id)
  .insert(data)
  .upsert() // or .onConflictDoUpdate()

Estou mais inclinado para algo como 3.

Apenas para adicionar aqui está como o mongodb faz isso .

db.collection.update(
   <query>,
   <update>,
   {
     upsert: <boolean>,
     multi: <boolean>,
     writeConcern: <document>
   }
)

@reggi Eu acredito que meu patch de macaco é compatível com where ...

@reggi , não vejo seu ponto.
Você pode elaborar mais sobre qual funcionalidade está faltando na abordagem proposta nos exemplos de @willfarrell e @hayeah .
Por que você precisa de where ?
É apenas uma operação insert .

@reggi O exemplo do MongoDB que você forneceu lê "Primeiro tente UPDATE WHERE ... depois faça um INSERT se nenhum documento corresponder à consulta", enquanto o SQL UPSERT lê "INSERT INTO ... UPDATING caso já exista uma linha com essa chave primária" .
Então, eu acho, você está falando de um "upsert" totalmente diferente do que é implementado em bancos de dados SQL.

Eu proporia esta API:

knex.createTable('test')
   .bigserial('id')
   .varchar('unique').notNull().unique()
   .varchar('whatever')

knex.table('test').insert(object, { upsert: ['unique'] })

A função .insert() analisaria o segundo parâmetro.
Se for uma string, então é o antigo parâmetro returning .
Se for um objeto, então é um parâmetro options com options.returning e options.upsert , onde options.upsert é uma lista das chaves exclusivas (pode ser > 1 em caso de uma restrição de chave exclusiva composta).
Em seguida, é gerada uma consulta SQL que simplesmente exclui a chave primária e todas as chaves options.upsert do object (via clone(object) && delete cloned_object.id && delete cloned_object.unique ) e, em seguida, usa esse cloned_object despojado de as chaves primárias (e exclusivas) para construir a cláusula SET na segunda parte da consulta SQL: ... ON CONFLICT DO UPDATE SET [iterate cloned_object] .

Acho que essa seria a solução mais simples e inequívoca homogênea com a API atual.

@slavafomin @ScionOfBytes Parece que até mesmo a API ainda não foi acordada. Esse seria o próximo passo e então alguém que gosta de implementá-lo pode fazê-lo. Então sem novidades.

obs. Comecei a excluir quaisquer solicitações adicionais de notícias se não houver nenhuma para impedir que este tópico seja preenchido com spam de solicitações de notícias e outras mensagens menos relacionadas.

@amir-s Eu concordo, mas o assunto deste problema é o recurso upsert.

IMO, o verdadeiro problema não é a API, mas a forma incomum de fazer upserts em cada banco de dados.

MySQL (ON DUPLICATE KEY UPDATE) e PostgreSQL 9.5+ (ON CONFLICT DO UPDATE) suportam upsert por padrão.

MSSQL e Oracle podem suportá-lo com uma cláusula de mesclagem, mas o knex deve saber os nomes das colunas de conflito para poder construir a consulta.

-- in this case the conflict column is 'a'
merge into target
using (values (?)) as t(a)
on (t.a = target.a)
when matched then
  update set b = ?
when not matched then
  insert (a, b) values (?, ?);

Mas o SQLite não. Precisamos de duas consultas para simular o upsert

-- 'a' is the conflict column
insert or ignore into target (a, b) values (?, ?);
update target set b = ?2 where changes() = 0 and a = ?1;

Ou usando INSERT OR REPLACE , também conhecido REPLACE

-- replace will delete the matched row then add a new one with the given data
replace into target (a, b) values (?, ?);

Infelizmente, se a tabela de destino tiver mais colunas do que a e b, seus valores serão substituídos pelos padrões

insert or replace into target (a, b, c) values (?, ?, (select c from target where a = ?1))

Outra solução usando CTE, veja esta resposta do stackoverflow

Eu vim para este problema várias vezes em busca de um upsert Postgres baseado em knex. Se alguém mais precisar disso, aqui está como fazê-lo. Eu testei isso contra chaves exclusivas únicas e compostas.

A configuração

Crie uma restrição de chave exclusiva na tabela usando o abaixo. Eu precisava de uma restrição de chave composta:

table.unique(['a', 'b'])

A função

(editar: atualizado para usar ligações de parâmetros brutos)

const upsert = (params)=> {
  const {table, object, constraint} = params;
  const insert = knex(table).insert(object);
  const update = knex.queryBuilder().update(object);
  return knex.raw(`? ON CONFLICT ${constraint} DO ? returning *`, [insert, update]).get('rows').get(0);
};

Uso

const objToUpsert = {a:1, b:2, c:3}

upsert({
    table: 'test',
    object: objToUpsert,
    constraint: '(a, b)',
})

Se sua restrição não for composta então, naturalmente, essa linha seria apenas constraint: '(a)' .

Isso retornará o objeto atualizado ou o objeto inserido.

Uma observação sobre índices anuláveis ​​compostos

Se você tem um índice composto (a,b) e b é anulável, então os valores (1, NULL) e (1, NULL) são considerados mutuamente exclusivos pelo Postgres (não entendi qualquer). Se este for o seu caso de uso, você precisará criar um índice exclusivo parcial e, em seguida, testar nulo antes do upsert para determinar qual restrição usar. Veja como fazer o índice único parcial: CREATE UNIQUE INDEX unique_index_name ON table (a) WHERE b IS NULL . Se seu teste determinar que b é nulo, você precisará usar essa restrição em seu upsert: constraint: '(a) WHERE b IS NULL' . Se a também for anulável, acho que você precisaria de 3 índices exclusivos e 4 ramos if/else (embora este não seja o meu caso de uso, então não tenho certeza).

Aqui está o javascript compilado .

Espero que alguém ache isso útil. @elhigu Algum comentário sobre o uso de knex().update(object) ? (edit: nevermind - vi o aviso - usando knex.queryBuilder() agora)

@timhuff parece legal, uma coisa a mudar seria passar cada consulta para raw, usando value binding. Caso contrário query.toString() é usado para renderizar cada parte da consulta e abre um possível buraco de injeção de dependência (queryBuilder.toString() não é tão seguro quanto passar parâmetros para o driver como ligações).

@elhigu Espere... query.toString() não usa ligações? Você poderia me dar um exemplo aproximado da modificação que você está recomendando? Eu... talvez tenha muito código para atualizar.

Encontrou a parte da documentação rotulada Ligações brutas . Atualizando agora atualizei o exemplo. Achei que query.toString era seguro. Seria bom ter uma seção da documentação rotulada como "Como fazer consultas inseguras". Há apenas um punhado de não-nãos e dessa forma as pessoas podem usar a biblioteca sabendo que "desde que eu não faça essas coisas, estou seguro".

Eu criei o seguinte upsert: https://gist.github.com/adnanoner/b6c53482243b9d5d5da4e29e109af9bd
Ele lida com upserts únicos e em lote. Eu adaptei um pouco do @plurch . Melhorias são sempre bem-vindas :)

Para o que vale a pena eu tenho usado este formato:

Editar: atualizado para ser seguro para quem procura por isso. Obrigado @elhigu

const query = knex( 'account' ).insert( accounts );
const safeQuery = knex.raw( '? ON CONFLICT DO NOTHING', [ query ]);

@NicolajKN Você não deve usar toString(), pode causar muitos tipos de problemas e não passará valores por meio de ligações para o banco de dados (potencial falha de segurança de injeção de SQL).

Mesmo isso feito corretamente ficaria assim:

const query = knex('account').insert(accounts);
const safeQuery = knex.raw('? ON CONFLICT DO NOTHING', [query]);

Discussão excluída de problema não relacionado.

@elhigu Espere, essa consulta de inserção não é executada imediatamente após ser criada? Isso não cria uma condição de corrida?

@cloutiertyler Você não estava falando comigo, mas talvez eu possa salvar @elhigu algum tempo aqui. Nenhuma dessas consultas seria executada. A instrução knex('account').insert(accounts) não executa uma consulta. Ele não é executado até que os dados sejam realmente chamados (por exemplo, por meio de um .then ). Ele envia isso para knex.raw('? ON CONFLICT DO NOTHING', [query]) que chamará query.toString() , que apenas converte a consulta na instrução SQL que seria executada.

@timhuff Obrigado Tim, eu assumi que tinha que ser algo assim, mas esse não é um comportamento normal para uma promessa. As promessas geralmente são executadas na criação. A razão pela qual eu pergunto é que eu estava recebendo erros dizendo "Conexão encerrada" de vez em quando quando tentei executar este upsert. Uma vez que mudei para remover a inserção e criar uma consulta totalmente bruta, eles foram embora. Parece que isso seria consistente com uma condição de corrida.

knex QueryBuilder s não são Promise s, no entanto. Quando você começa a escrever uma consulta knex, você fica em "knexland". Tudo o que você faz é mais ou menos apenas configurar uma especificação JSON da consulta que você deseja criar. Se você executar .toString , ele o compilará e o exibirá. Não se torna uma promessa ( bluebird ) até que você execute uma dessas nele. Você pode estar interessado em usar .return se quiser executar a instrução imediatamente.

Ah, entendo, bem, isso esclarece minha confusão. Obrigado pelos esclarecimentos e indicações! Meu problema deve existir em outro lugar então.

Como um aparte, o fato de que ele não é executado imediatamente é frequentemente útil. Às vezes você quer passar a coisa, configurando, antes de executar. Há também situações em que você pode fazer coisas como...

const medicalBuildings = knex.select('building_id').from('buildings').where({type: 'medical'})
const medicalWorkers = knex.select().from('workers').whereIn('building', medicalBuildings)

(exemplo super inventado, mas vamos correr com ele)

Na verdade, não quero executar essa primeira declaração - é apenas parte da minha segunda.

Sem mencionar que, se todos os construtores de consulta fossem executados na criação, as consultas de padrão do construtor seriam acionadas antes que a construção fosse concluída. Não funcionaria sem algum método terminador (que executa a consulta).

@elhigu , quero dizer... acho que você pode sempre executá-lo no próximo tick, certo? Não estou sugerindo que isso seria uma boa ideia, mas quantas consultas são realmente criadas e executadas em diferentes ticks?

@timhuff eu não tinha pensado nisso. Sim, acho que seria possível também. Acho bastante comum o caso em que alguém começa a criar uma consulta, depois busca alguns dados assíncronos e continua construindo mais. Eu não faço isso com muita frequência embora.

@lukewlms que o método 'execute()' -like é chamado '.then()' você sempre pode chamá-lo quando quiser executar a consulta e obter a promessa. É exatamente como 'thenable' funciona e é explicado nas especificações da promessa. É um conceito importante e amplamente usado em javascript ao lidar com promessas e async/await (que são apenas atalhos glorificados para Promise.resolve e .then). Além disso, se você estiver executando consultas sem manipular os resultados, estará procurando problemas como travamento do aplicativo.

Na verdade, é melhor seguir este PR sobre a implementação do recurso upsert https://github.com/tgriesser/knex/pull/2197 ele já projetou a API como deve funcionar. Neste tópico não há realmente nenhuma informação útil que já não seja mencionada nos comentários daquele PR. Se necessário (PR é fechado e nunca concluído), vamos abrir um novo problema para este com descrição adicional da API.

@elhigu Obrigado pelo aviso! Eu desconhecia esse tópico. É bom saber que estamos progredindo em um upsert chegando à API. Parece que há 6 meses ele falhou em 1 dos 802 testes e nunca passou no travis-ci. Esse 1 caso de teste com falha é a única coisa que impede que isso se torne parte da API knex?

@timhuff houve apenas implementação inicial, deve ser completamente reescrita. A parte mais importante desse PR é o design de API comum, que pode ser suportado pela maioria dos dialetos. Portanto, o recurso surge quando alguém decide implementar essa API. Se ninguém mais fizer isso e algum dia eu tiver algum tempo extra ou precisar muito, eu mesmo o farei. Esse é um dos recursos mais importantes que eu gostaria que o knex obtivesse (além de junções em atualizações).

@elhigu Obrigado por me informar. Vou ter que ler o progresso aqui quando tiver um pouco mais de tempo.

Não tenho certeza se isso ajuda alguém ou se sou apenas um noob, mas para a solução do @timhuff eu tive que colocar minha restrição entre aspas porque estava recebendo um erro de sintaxe de consulta.

const contraint = '("a", "b")'

Eu vim para este problema várias vezes em busca de um upsert Postgres baseado em knex. Se alguém mais precisar disso, aqui está como fazê-lo. Eu testei isso contra chaves exclusivas únicas e compostas.

A configuração

Crie uma restrição de chave exclusiva na tabela usando o abaixo. Eu precisava de uma restrição de chave composta:

table.unique(['a', 'b'])

A função

(editar: atualizado para usar ligações de parâmetros brutos)

const upsert = (params)=> {
  const {table, object, constraint} = params;
  const insert = knex(table).insert(object);
  const update = knex.queryBuilder().update(object);
  return knex.raw(`? ON CONFLICT ${constraint} DO ? returning *`, [insert, update]).get('rows').get(0);
};

Uso

const objToUpsert = {a:1, b:2, c:3}

upsert({
  table: 'test',
  object: objToUpsert,
  constraint: '(a, b)',
})

Se sua restrição não for composta então, naturalmente, essa linha seria apenas constraint: '(a)' .

Isso retornará o objeto atualizado ou o objeto inserido.

Uma observação sobre índices anuláveis ​​compostos

Se você tem um índice composto (a,b) e b é anulável, então os valores (1, NULL) e (1, NULL) são considerados mutuamente exclusivos pelo Postgres (não entendi qualquer). Se este for o seu caso de uso, você precisará criar um índice exclusivo parcial e, em seguida, testar nulo antes do upsert para determinar qual restrição usar. Veja como fazer o índice único parcial: CREATE UNIQUE INDEX unique_index_name ON table (a) WHERE b IS NULL . Se seu teste determinar que b é nulo, você precisará usar essa restrição em seu upsert: constraint: '(a) WHERE b IS NULL' . Se a também for anulável, acho que você precisaria de 3 índices exclusivos e 4 ramos if/else (embora este não seja meu caso de uso, então não tenho certeza).

Aqui está o javascript compilado .

Espero que alguém ache isso útil. @elhigu ~Algum comentário sobre o uso de knex().update(object) ?~ (edit: nevermind - vi o aviso - usando knex.queryBuilder() agora)

Removidas algumas discussões não relacionadas (sobre como as promessas/entíveis funcionam).

Isso foi adicionado?

Não. Há solicitação e especificação de recursos em https://github.com/knex/knex/issues/3186

Esta página foi útil?
0 / 5 - 0 avaliações

Questões relacionadas

aj0strow picture aj0strow  ·  3Comentários

PaulOlteanu picture PaulOlteanu  ·  3Comentários

mattgrande picture mattgrande  ·  3Comentários

legomind picture legomind  ·  3Comentários

mishitpatel picture mishitpatel  ·  3Comentários