Knex: Adición de capacidad de tipo upsert

Creado en 30 ago. 2013  ·  54Comentarios  ·  Fuente: knex/knex

Presentado por @adamscybot en tgriesser/bookshelf#55 - esta podría ser una buena característica para agregar.

feature request

Comentario más útil

@NicolajKN No debe usar toString (), podría causar muchos tipos de problemas y no pasará valores a través de enlaces a DB (agujero de seguridad de inyección de SQL potencial).

Mismo esto hecho correctamente sería así:

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

Todos 54 comentarios

Estoy de acuerdo, ¡esto _sería_ una buena característica!

:+1:

:+1:

Estoy importando algunos datos de un CSV y es muy probable que algunos de los registros se superpongan con respecto a la última importación (es decir, la última vez se importó del 1 de enero al 31 de mayo, esta vez se importó del 31 de mayo al 18 de junio).

Afortunadamente, el sistema de terceros asigna identificaciones únicas confiables.

¿Cuál es la mejor manera de insertar los nuevos registros y actualizar los antiguos?

Todavía no lo he probado, pero estaba pensando que sería algo como esto:

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');
  });
});

Además, a veces lo que estoy almacenando se almacena mediante un valor de identificación determinado, como un hash. En esos casos, solo quiero agregar o reemplazar los datos.

No siempre uso SQL como SQL tradicional. A menudo lo uso como NoSQL híbrido con el beneficio de índices y mapeo de relaciones claros.

:+1:

Hola,
hay noticias sobre esta nueva característica?

¿O alguien puede recomendar ejemplos que muestren cómo simular esta funcionalidad para mysql?

gracias

En este momento lo estoy haciendo con raw , pero estoy trabajando duro para que esté disponible aquí pronto.

Postgres acaba de implementar el soporte upsert por cierto :+1:

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

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

La sintaxis es INSERT ... ON CONFLICT DO UPDATE

Estaba buscando una manera de hacer REPLACE INTO en MySql y encontré esta solicitud de función. Dado que REPLACE y INSERT tienen exactamente la misma sintaxis en MySql, me imagino que es más fácil de implementar que un ON DUPLICATE KEY UPDATE . ¿Hay algún plan para implementar un REPLACE ? ¿Sería un PR algo de valor?

¿Alguna actualización sobre esto, especialmente con PostreSQL 9.5?

Creo que una pregunta importante es si exponer o no la misma firma de método upsert para diferentes dialectos, como PostgreSQL y MySQL. En Sequelize, se planteó un problema con respecto al valor de retorno de upsert : https://github.com/sequelize/sequelize/issues/3354.

Me doy cuenta de que algunos de los métodos de la biblioteca KnexJS tienen distinciones con respecto a los valores devueltos en el contexto de diferentes dialectos (como insert , donde se devuelve una matriz de la primera identificación insertada para Sqlite y MySQL, mientras que una matriz de todos la identificación insertada se devuelve con PostgreSQL).

Según la documentación, la sintaxis INSERT ... ON DUPLICATE KEY UPDATE en MySQL tiene el siguiente comportamiento (http://dev.mysql.com/doc/refman/5.7/en/insert-on-duplicate.html):

Con ON DUPLICATE KEY UPDATE, el valor de las filas afectadas por fila es 1 si la fila se inserta como una fila nueva, 2 si se actualiza una fila existente y 0 si una fila existente se establece en sus valores actuales.

Mientras esté en PostgreSQL (http://www.postgresql.org/docs/9.5/static/sql-insert.html):

Al completarse con éxito, un comando INSERT devuelve una etiqueta de comando del formulario

INSERT oid count

El recuento es el número de filas insertadas o actualizadas. Si count es exactamente uno y la tabla de destino tiene OID, entonces oid es el OID asignado a la fila insertada. La única fila debe haber sido insertada en lugar de actualizada. De lo contrario, oid es cero.

Si el comando INSERT contiene una cláusula RETURNING, el resultado será similar al de una instrucción SELECT que contiene las columnas y los valores definidos en la lista RETURNING, calculados sobre las filas insertadas o actualizadas por el comando.

En este caso, los valores devueltos se pueden cambiar con la cláusula RETURNING .

¿Pensamientos?

Parcheé Client_PG para agregar el método "onConflict" para insertar. Supongamos que queremos alterar las credenciales de github oauth, podemos escribir la consulta de esta manera:

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())

La matriz de nombres de columna especifica la restricción de unicidad.

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'

Ver esencia: https://gist.github.com/hayeah/1c8d642df5cfeabc2a5b para el parche de mono.

Este es un experimento súper hacky... así que no copie y pegue exactamente el parche del mono en su código de producción: p

Problemas conocidos:

  • El parche mono está en QueryBuilder afecta a todos los dialectos, porque Client_PG no especializa al constructor.
  • No admite actualizaciones sin procesar como count = count + 1
  • onConflict probablemente debería lanzar si el método de consulta no se inserta.

¿Reacción?

@hayeah Me gusta tu enfoque y se adapta a Postgres. Voy a probar su parche de mono en un proyecto para ver si puedo detectar empíricamente cualquier problema que no sea el que usted señaló.

Sugerencia de sintaxis: knex('table').upsert(['col1','col2']).insert({...}).update({...}); donde upsert incluiría la declaración de condición. De esta manera no es específico de db.

El resumen de las diferentes implementaciones de upserts se puede encontrar en https://en.wikipedia.org/wiki/Merge_ (SQL)

Estoy interesado en tener esta capacidad también. Caso de uso: crear un sistema que dependa de muchos datos externos de un servicio externo; Lo sondeo periódicamente en busca de datos que guardo en una base de datos MySQL local. Probablemente usará knex.raw por ahora.

También me interesa, pero en mi caso de uso necesitaría que funcione de una manera que no se base en conflictos, ya que las columnas no siempre tienen restricciones 'únicas': simplemente actualice las entradas que coincidan con la consulta si existen, de lo contrario inserte filas nuevas

@haywirez Tengo curiosidad por saber por qué no hay restricciones únicas. ¿No estarías expuesto a las condiciones de carrera?

@hayeah Tengo un caso de uso específico con datos de ventana de tiempo, almacenando entradas que tienen un valor vinculado a un día determinado. Por lo tanto, estoy insertando y actualizando entradas que tienen una "clave combinada" de una marca de tiempo coincidente (día) y otras dos ID correspondientes a PK en otras tablas. Dentro de una ventana de 24 horas, tengo que insertarlos o actualizarlos con los últimos recuentos.

¡Esta sería una gran característica para tener!

Hola a todos los que han comentado aquí. Estoy agregando una etiqueta PR Por favor.

Feliz de tomar un PR agregando esta funcionalidad, pero me gustaría ver una discusión de la API deseada aquí primero.

PD.

^ De acuerdo.

Voy a eliminar comentarios como este, si quieres agregar un +1 hazlo con la pequeña reacción de emoji.

Tengo un pequeño problema con la variedad de restricciones de columna como en los ejemplos de @willfarrell y @hayeah . No estoy seguro de si estos ejemplos pueden admitir propiedades json . ¿Hay alguna razón por la que ninguna de estas propuestas no incluya declaraciones where / "consultas" adecuadas para hacer coincidir el registro?

propuesta 1

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

propuesta 2

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

propuesta 3

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

Me inclino más hacia algo como 3.

Solo para agregar , así es como lo hace mongodb .

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

@reggi Creo que mi parche de mono es compatible con where ...

@reggi No veo tu punto.
¿Puede dar más detalles sobre qué funcionalidad falta en el enfoque propuesto en los ejemplos de @willfarrell y @hayeah ?
¿Por qué necesita where en absoluto?
Es solo una operación insert .

@reggi El ejemplo de MongoDB que proporcionó dice "Primero intente ACTUALIZAR DONDE ... luego haga un INSERTAR si ningún documento coincide con la consulta", mientras que SQL UPSERT dice "INSERTAR EN ... ACTUALIZAR en caso de que ya exista una fila con esta clave principal" .
Entonces, supongo, estás hablando de un "upsert" completamente diferente al que se implementa en las bases de datos SQL.

Yo propondría esta API:

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

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

La función .insert() analizaría el segundo parámetro.
Si es una cadena, entonces es el antiguo parámetro returning .
Si es un objeto, entonces es un parámetro options que tiene options.returning y options.upsert , donde options.upsert es una lista de claves únicas (puede ser > 1 en caso de una restricción de clave única compuesta).
Luego se genera una consulta SQL que simplemente excluye la clave principal y todas las claves options.upsert de object (a través clone(object) && delete cloned_object.id && delete cloned_object.unique ) y luego usa ese cloned_object despojado las claves primarias (y únicas) para construir la cláusula SET en la segunda parte de la consulta SQL: ... ON CONFLICT DO UPDATE SET [iterate cloned_object] .

Supongo que esa sería la solución más simple e inequívoca homogénea con la API actual.

@slavafomin @ScionOfBytes Parece que aún no se ha acordado la API. Ese sería el siguiente paso y luego alguien a quien le guste implementarlo puede hacerlo. Así que no hay noticias.

PD. Comencé a eliminar cualquier solicitud adicional de noticias si no hay ninguna para evitar que este hilo se llene con solicitudes de noticias no deseadas y otros mensajes menos relacionados.

@amir-s Estoy de acuerdo, pero el tema de este problema es la capacidad de inserción.

En mi opinión, el problema real no es la API, sino la forma poco común de hacer upserts en cada base de datos.

MySQL (EN ACTUALIZACIÓN DE CLAVE DUPLICADA) y PostgreSQL 9.5+ (EN CONFLICTO HACER ACTUALIZACIÓN) admiten upsert de forma predeterminada.

MSSQL y Oracle pueden admitirlo con una cláusula de combinación, pero knex debe conocer los nombres de las columnas en conflicto para poder construir la 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 (?, ?);

Pero SQLite no lo hizo. Necesitamos dos consultas para simular el 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;

O usando INSERT OR REPLACE , también conocido como REPLACE

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

Desafortunadamente, si la tabla de destino tiene más columnas que a y b, sus valores serán reemplazados por valores predeterminados

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

Otra solución usando CTE, mira esta respuesta de stackoverflow

He venido a este problema varias veces en busca de un upsert de Postgres basado en knex. Si alguien más necesita esto, aquí está cómo hacerlo. He probado esto con claves únicas únicas y compuestas.

La puesta en marcha

Cree una restricción de clave única en la tabla usando lo siguiente. Necesitaba una restricción de clave compuesta:

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

La función

(editar: actualizado para usar enlaces de parámetros sin procesar)

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)',
})

Si su restricción no es compuesta entonces, naturalmente, esa única línea sería constraint: '(a)' .

Esto devolverá el objeto actualizado o el objeto insertado.

Una nota sobre los índices anulables compuestos

Si tiene un índice compuesto (a,b) y b es anulable, Postgres considera que los valores (1, NULL) y (1, NULL) son mutuamente únicos (no lo entiendo cualquiera). Si este es su caso de uso, deberá crear un índice único parcial y luego probar el valor nulo antes de la inserción para determinar qué restricción usar. Así es como se crea el índice único parcial: CREATE UNIQUE INDEX unique_index_name ON table (a) WHERE b IS NULL . Si su prueba determina que b es nulo, entonces deberá usar esta restricción en su inserción: constraint: '(a) WHERE b IS NULL' . Si a también es anulable, supongo que necesitaría 3 índices únicos y 4 ramas if/else (aunque este no es mi caso de uso, así que no estoy seguro).

Aquí está el javascript compilado .

Espero que alguien encuentre esto útil. @elhigu ¿ Algún comentario sobre el uso de knex().update(object) ? (editar: no importa - vi la advertencia - usando knex.queryBuilder() ahora)

@timhuff se ve bien, una cosa para cambiar sería pasar cada consulta a raw, usando el enlace de valor. De lo contrario, se usa query.toString() para representar cada parte de la consulta y abre un posible agujero de inyección de dependencia (queryBuilder.toString() no es tan seguro como pasar parámetros al controlador como enlaces).

@elhigu Espera... ¿ query.toString() no usa enlaces? ¿Podría darme un ejemplo aproximado de la modificación que recomienda? Yo... podría tener mucho código para actualizar.

Encontré la parte de la documentación etiquetada Raw bindings . Actualizando ahora he actualizado el ejemplo. Pensé que query.toString era seguro. Sería bueno tener una sección de la documentación etiquetada como "Cómo hacer consultas inseguras". Solo hay un puñado de no-nos y de esa manera la gente puede usar la biblioteca sabiendo "mientras no haga estas cosas, estoy a salvo".

Creé el siguiente upsert: https://gist.github.com/adnanoner/b6c53482243b9d5d5da4e29e109af9bd
Maneja upserts individuales y por lotes. Lo adapté un poco de @plurch . Siempre se agradecen las mejoras :)

Por lo que vale, he estado usando este formato:

Editar: Actualizado para ser seguro para cualquier persona que busque esto. Gracias @elhigu

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

@NicolajKN No debe usar toString (), podría causar muchos tipos de problemas y no pasará valores a través de enlaces a DB (agujero de seguridad de inyección de SQL potencial).

Mismo esto hecho correctamente sería así:

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

Discusión eliminada de un problema no relacionado.

@elhigu Espera, ¿no se ejecuta esa consulta de inserción inmediatamente después de crearse? ¿Eso no crea una condición de carrera?

@cloutiertyler No estabas hablando conmigo, pero tal vez pueda ahorrarle a @elhigu algo de tiempo aquí. Ninguna de estas consultas se ejecutaría. La instrucción knex('account').insert(accounts) no ejecuta una consulta. No se ejecuta hasta que se solicitan los datos (por ejemplo, a través de .then ). Lo envía a knex.raw('? ON CONFLICT DO NOTHING', [query]) que llamará a query.toString() , que solo convierte la consulta en la instrucción SQL que se ejecutará.

@timhuff Gracias Tim, asumí que tenía que ser algo así, pero ese no es un comportamiento normal para una promesa. Las promesas generalmente se ejecutan en el momento de la creación. La razón por la que pregunto es que recibía errores que decían "Conexión finalizada" de vez en cuando cuando intentaba ejecutar este upsert. Una vez que cambié a eliminar el inserto y crear una consulta completamente sin procesar, desaparecieron. Parece que eso sería consistente con una condición de carrera.

Sin embargo, knex QueryBuilder no son Promise . Cuando comienza a escribir una consulta knex, permanece en "knexland". Todo lo que hace es más o menos configurar una especificación JSON de la consulta que desea crear. Si ejecuta .toString , lo compila y lo genera. No se convierte en una Promesa ( bluebird ) hasta que ejecute uno de estos en él. Es posible que le interese usar .return si desea ejecutar la declaración de inmediato.

Ah, ya veo, bueno, eso aclara mi confusión. ¡Gracias por la aclaración y las indicaciones! Mi problema debe existir en otro lugar entonces.

Aparte, el hecho de que no se ejecute inmediatamente suele ser útil. A veces quieres pasar la cosa, configurarla, antes de ejecutarla. También hay situaciones en las que puedes hacer cosas como...

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

(ejemplo super inventado pero sigamos con él)

En realidad, no quiero ejecutar esa primera declaración, es solo parte de la segunda.

Sin mencionar que si todos los generadores de consultas se ejecutaran en la creación, las consultas de patrón del generador se activarían antes de que se complete la construcción. No funcionaría en absoluto sin tener algún método de terminación (que ejecuta la consulta).

@elhigu Quiero decir... Supongo que siempre puedes ejecutarlo en el siguiente tick, ¿verdad? No estoy sugiriendo que de ninguna manera sería una buena idea, pero ¿cuántas consultas se crean y ejecutan realmente en diferentes ticks?

@timhuff No había pensado en eso. Sí, creo que eso también sería posible. Encuentro un caso bastante común en el que uno comienza a crear una consulta, luego obtiene algunos datos asíncronos y sigue construyendo más. Aunque no lo hago muy a menudo.

@lukewlms ese método similar a 'ejecutar ()' se llama '. entonces ()', siempre puede llamarlo cuando desee ejecutar la consulta y obtener la promesa. Así es como funciona 'thenable' y se explica en la especificación de promesa. Es un concepto importante y ampliamente utilizado en javascript cuando se trata de promesas y async/await (que son básicamente accesos directos glorificados para Promise.resolve y .then). Además, si está ejecutando consultas sin manejar los resultados, está buscando problemas como el bloqueo de la aplicación.

En realidad, es mejor simplemente seguir este PR sobre la implementación de la función upsert https://github.com/tgriesser/knex/pull/2197 ya tiene una API diseñada sobre cómo debería funcionar. En este hilo no hay realmente ninguna información útil que no se haya mencionado en los comentarios de ese PR. Si es necesario (PR está cerrado y nunca se completa), abra un nuevo problema para este con una descripción adicional de la API.

@elhigu ¡ Gracias por el aviso! Desconocía ese hilo. Es bueno escuchar que estamos progresando en una actualización que llegará a la API. Parece que hace 6 meses falló 1 de las 802 pruebas, por lo que nunca pasó travis-ci. ¿Es ese 1 caso de prueba fallido lo único que impide que esto se convierta en parte de la API knex?

@timhuff solo se realizó una implementación inicial, debe reescribirse por completo. La parte más importante de ese PR es el diseño de API común, que puede ser compatible con la mayoría de los dialectos. Entonces, la función surge cuando alguien simplemente decide implementar esa API. Si nadie más hace eso y algún día tengo algo de tiempo extra o lo necesito mucho, lo haré yo mismo. Esa es una de las características más importantes que me gustaría que obtuviera Knex (además de unirse a las actualizaciones).

@elhigu Gracias por informarme. Tendré que leer sobre el progreso aquí cuando tenga un poco más de tiempo.

No estoy seguro de si esto ayuda a alguien o si solo soy un novato, pero para la solución de @timhuff tuve que poner mi restricción entre comillas porque estaba recibiendo un error de sintaxis de consulta.

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

He venido a este problema varias veces en busca de un upsert de Postgres basado en knex. Si alguien más necesita esto, aquí está cómo hacerlo. He probado esto con claves únicas únicas y compuestas.

La puesta en marcha

Cree una restricción de clave única en la tabla usando lo siguiente. Necesitaba una restricción de clave compuesta:

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

La función

(editar: actualizado para usar enlaces de parámetros sin procesar)

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)',
})

Si su restricción no es compuesta, entonces, naturalmente, esa única línea sería constraint: '(a)' .

Esto devolverá el objeto actualizado o el objeto insertado.

Una nota sobre los índices anulables compuestos

Si tiene un índice compuesto (a,b) y b es anulable, Postgres considera que los valores (1, NULL) y (1, NULL) son mutuamente únicos (no lo entiendo cualquiera). Si este es su caso de uso, deberá crear un índice único parcial y luego probar el valor nulo antes de la inserción para determinar qué restricción usar. Así es como se crea el índice único parcial: CREATE UNIQUE INDEX unique_index_name ON table (a) WHERE b IS NULL . Si su prueba determina que b es nulo, entonces deberá usar esta restricción en su inserción: constraint: '(a) WHERE b IS NULL' . Si a también es anulable, supongo que necesitaría 3 índices únicos y 4 ramas if/else (aunque este no es mi caso de uso, así que no estoy seguro).

Aquí está el javascript compilado .

Espero que alguien encuentre esto útil. @elhigu ~¿Algún comentario sobre el uso de knex().update(object) ?~ (editar: no importa - vi la advertencia - usando knex.queryBuilder() ahora)

Se eliminaron algunas discusiones no relacionadas (sobre cómo funcionan las promesas/entonces).

¿Se agregó esto?

No. Hay una solicitud de funciones y especificaciones en https://github.com/knex/knex/issues/3186

¿Fue útil esta página
0 / 5 - 0 calificaciones

Temas relacionados

marianomerlo picture marianomerlo  ·  3Comentarios

mishitpatel picture mishitpatel  ·  3Comentarios

tjwebb picture tjwebb  ·  3Comentarios

fsebbah picture fsebbah  ·  3Comentarios

hyperh picture hyperh  ·  3Comentarios