Knex: Добавление возможности типа upsert

Созданный на 30 авг. 2013  ·  54Комментарии  ·  Источник: knex/knex

Поднятый @adamscybot в tgriesser/bookshelf#55 — это может быть хорошей функцией для добавления.

feature request

Самый полезный комментарий

@NicolajKN Вы не должны использовать toString (), это может вызвать множество проблем и не будет передавать значения через привязки к БД (потенциальная дыра в безопасности SQL-инъекций).

То же самое, если это сделано правильно, будет выглядеть так:

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

Все 54 Комментарий

Я согласен, это _было бы_ хорошей функцией!

:+1:

:+1:

Я импортирую некоторые данные из CSV, и есть большая вероятность, что несколько записей перекрываются с последнего импорта (т.е. последний раз был импортирован с 1 января по 31 мая, на этот раз импорт с 31 мая по 18 июня).

К счастью, сторонняя система надежно присваивает уникальные идентификаторы.

Как лучше всего вставить новые записи и обновить старые?

Я еще не пробовал, но я думал, что это будет что-то вроде этого:

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

Кроме того, иногда то, что я сохраняю, хранится с определенным значением идентификатора, например хешем. В этих случаях я просто хочу добавить или заменить данные.

Я не всегда использую SQL как традиционный SQL. Часто я использую его как гибридный NoSQL с преимуществом четкого сопоставления отношений и индексов.

:+1:

Привет,
есть новости об этой новой функции?

Или кто-нибудь может порекомендовать примеры, показывающие, как имитировать эту функциональность для mysql?

Спасибо

Прямо сейчас я делаю это с raw , но я усердно работаю над тем, чтобы скоро это стало доступно здесь.

Между прочим, Postgres только что реализовал поддержку upsert :+1:

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

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

Синтаксис INSERT ... ON CONFLICT DO UPDATE

Я искал способ сделать REPLACE INTO в MySql и нашел этот запрос функции. Поскольку REPLACE и INSERT имеют точно такой же синтаксис в MySql, я бы предположил, что его проще реализовать, чем ON DUPLICATE KEY UPDATE . Есть ли планы по реализации REPLACE ? Будет ли пиар чем-то ценным?

Есть какие-нибудь обновления по этому поводу, особенно с PostreSQL 9.5?

Я думаю, что один важный вопрос заключается в том, следует ли выставлять одну и ту же сигнатуру метода upsert для разных диалектов, таких как PostgreSQL и MySQL. В Sequelize возникла проблема с возвращаемым значением upsert : https://github.com/sequelize/sequelize/issues/3354.

Я понимаю, что некоторые из методов библиотеки KnexJS имеют различия в отношении возвращаемых значений в контексте разных диалектов (например, insert , где для Sqlite и MySQL возвращается массив первого вставленного идентификатора, а массив всех вставленный идентификатор возвращается вместе с PostgreSQL).

Согласно документации, синтаксис INSERT ... ON DUPLICATE KEY UPDATE в MySQL имеет следующее поведение (http://dev.mysql.com/doc/refman/5.7/en/insert-on-duplicate.html):

При ON DUPLICATE KEY UPDATE значение затронутых строк для каждой строки равно 1, если строка вставляется как новая строка, 2, если обновляется существующая строка, и 0, если для существующей строки устанавливаются ее текущие значения.

Находясь в PostgreSQL (http://www.postgresql.org/docs/9.5/static/sql-insert.html):

При успешном завершении команда INSERT возвращает тег команды вида

INSERT oid count

Счетчик — это количество вставленных или обновленных строк. Если count равно единице и целевая таблица имеет OID, то oid — это OID, присвоенный вставляемой строке. Одна строка должна быть вставлена, а не обновлена. В противном случае oid равен нулю.

Если команда INSERT содержит предложение RETURNING, результат будет аналогичен результату оператора SELECT, содержащего столбцы и значения, определенные в списке RETURNING, вычисленные для строк, вставленных или обновленных командой.

В этом случае возвращаемые значения можно изменить с помощью предложения RETURNING .

Мысли?

Я исправил Client_PG, чтобы добавить метод onConflict для вставки. Предположим, мы хотим обновить учетные данные github oauth, мы можем написать запрос следующим образом:

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

Массив имен столбцов задает ограничение уникальности.

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'

См. Суть: https://gist.github.com/hayeah/1c8d642df5cfeabc2a5b для патча обезьяны.

Это супер-хакерский эксперимент... так что не копируйте и не вставляйте патч обезьяны в свой производственный код: p

Известные проблемы:

  • Патч обезьяны на QueryBuilder влияет на все диалекты, потому что Client_PG не специализируется на сборщике.
  • Не поддерживает необработанные обновления, такие как count = count + 1
  • onConflict, вероятно, должен бросить, если метод запроса не вставляется.

Обратная связь?

@hayeah Мне нравится ваш подход, и он подходит для Postgres. Я собираюсь попробовать ваш патч для обезьян в проекте, чтобы увидеть, смогу ли я эмпирически обнаружить какие-либо проблемы, кроме тех, которые вы указали.

Предложение по синтаксису: knex('table').upsert(['col1','col2']).insert({...}).update({...}); , где upsert принимает оператор условия. Таким образом, это не зависит от БД.

Сводку различных реализаций upserts можно найти по адресу https://en.wikipedia.org/wiki/Merge_ (SQL).

Меня тоже интересует такая возможность. Пример использования: создание системы, зависящей от большого количества внешних данных из внешней службы; Я периодически опрашиваю его на наличие данных, которые сохраняю в локальной базе данных MySQL. Вероятно, сейчас будет использоваться knex.raw.

Также интересно, но в моем случае использования это должно работать таким образом, который не основан на конфликтах, поскольку столбцы не всегда имеют «уникальные» ограничения - просто обновите записи, соответствующие запросу, если они существуют, иначе вставьте новые ряды.

@haywirez Мне любопытно, почему нет уникальных ограничений? Разве вы не подверглись бы воздействию условий гонки?

@hayeah У меня есть конкретный вариант использования данных с временным окном, в котором хранятся записи, значения которых привязаны к данному дню. Поэтому я вставляю и обновляю записи, которые имеют «комбинированный ключ» совпадающей (дневной) временной метки и два других идентификатора, соответствующих PK в других таблицах. В течение 24 часов я должен либо вставить их, либо обновить их последними подсчетами.

Это была бы отличная функция!

Привет всем, кто когда-либо комментировал здесь. Я добавляю ярлык PR Please.

Рад получить PR, добавив эту функциональность, но я хотел бы сначала увидеть здесь обсуждение желаемого API.

PS.

^ Согласен.

Я собираюсь удалить такие комментарии, если вы хотите добавить +1, сделайте это с небольшой реакцией на смайлики.

У меня есть небольшая проблема с массивом ограничений столбцов, как в примерах @willfarrell и @hayeah . Не уверен, что эти примеры могут поддерживать свойства json . Есть ли причина, по которой ни одно из этих предложений не включает утверждения / правильные «запросы» для соответствия записи?

предложение 1

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

предложение 2

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

предложение 3

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

Я больше склоняюсь к чему-то вроде 3.

Просто добавим , как это делает mongodb .

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

@reggi Я считаю, что мой патч для обезьян совместим с where ...

@reggi Я не понимаю твоей точки зрения.
Не могли бы вы подробнее рассказать о том, какие функции отсутствуют в подходе, предложенном в примерах @willfarrell и @hayeah .
Зачем вообще нужны where ?
Это просто insert операция.

@reggi Предоставленный вами пример MongoDB гласит: «Сначала попробуйте ОБНОВИТЬ ГДЕ… затем выполните ВСТАВКУ, если ни один документ не соответствует запросу», тогда как SQL UPSERT читает «ВСТАВИТЬ В… ОБНОВЛЕНИЕ, если строка с этим первичным ключом уже существует» .
Итак, я думаю, вы говорите о совершенно другом «upsert», чем это реализовано в базах данных SQL.

Я бы предложил этот API:

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

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

Функция .insert() будет анализировать второй параметр.
Если это строка, то это старый параметр returning .
Если это объект, то это параметр options , имеющий options.returning и options.upsert , где options.upsert — список уникальных ключей (может быть > 1 в случае ограничения составного уникального ключа).
Затем генерируется SQL-запрос, который просто исключает первичный ключ и все ключи options.upsert из object (через clone(object) && delete cloned_object.id && delete cloned_object.unique ), а затем использует эти cloned_object , лишенные первичные (и уникальные) ключи для построения предложения SET во второй части SQL-запроса: ... ON CONFLICT DO UPDATE SET [iterate cloned_object] .

Думаю, это было бы самое простое и однозначное решение, однородное с нынешним API.

@slavafomin @ScionOfBytes Похоже, даже API еще не согласовано. Это будет следующим шагом, а затем кто-то, кто хочет его реализовать, может это сделать. Так что никаких новостей.

пс. Я начал удалять любые дополнительные запросы новостей, если их нет, чтобы предотвратить заполнение этой ветки спамом с запросами новостей и другими менее связанными сообщениями.

@amir-s Я согласен, но предметом этого вопроса является возможность upsert.

ИМО, настоящая проблема не в API, а в необычном способе выполнения upserts в каждой базе данных.

MySQL (ПРИ ОБНОВЛЕНИИ ДУБЛИКАЦИИ КЛЮЧА) и PostgreSQL 9.5+ (ПРИ КОНФЛИКТЕ ОБНОВЛЕНИЯ) поддерживают upsert по умолчанию.

MSSQL и Oracle могут поддерживать его с предложением слияния, но knex должен знать имена конфликтующих столбцов, чтобы иметь возможность построить запрос.

-- 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 (?, ?);

Но SQLite этого не сделал. Нам нужно два запроса для имитации 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;

Или используя INSERT OR REPLACE , также известный как REPLACE

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

К сожалению, если в целевой таблице больше столбцов, чем a и b, их значения будут заменены значениями по умолчанию.

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

Еще одно решение с использованием CTE, посмотрите этот ответ stackoverflow

Я приходил к этому вопросу несколько раз в поисках upsert Postgres на основе knex. Если кому-то еще это нужно, вот как это сделать. Я тестировал это как с одиночными, так и с составными уникальными ключами.

Установка

Создайте ограничение уникального ключа для таблицы, используя приведенное ниже. Мне нужно составное ключевое ограничение:

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

Функция

(изменить: обновлено для использования необработанных привязок параметров)

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

использование

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

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

Если ваше ограничение не составное, то, естественно, эта одна строка будет просто constraint: '(a)' .

Это вернет либо обновленный объект, либо вставленный объект.

Примечание о составных обнуляемых индексах

Если у вас есть составной индекс (a,b) и b обнуляемый, то значения (1, NULL) и (1, NULL) считаются взаимно уникальными для Postgres (я не понимаю либо). Если это ваш вариант использования, вам нужно будет создать частичный уникальный индекс, а затем проверить его на значение null перед upsert, чтобы определить, какое ограничение использовать. Вот как сделать частичный уникальный индекс: CREATE UNIQUE INDEX unique_index_name ON table (a) WHERE b IS NULL . Если ваш тест определяет, что b равно null, вам нужно будет использовать это ограничение в upsert: constraint: '(a) WHERE b IS NULL' . Если a также имеет значение null, я бы предположил, что вам понадобятся 3 уникальных индекса и 4 ветви if/else (хотя это не мой вариант использования, поэтому я не уверен).

Вот скомпилированный javascript .

Надеюсь, кто-то найдет это полезным. @elhigu Есть комментарий по поводу использования knex().update(object) ? (редактировать: неважно - видел предупреждение - сейчас использую knex.queryBuilder() )

@timhuff выглядит красиво, единственное, что нужно изменить, - это передавать каждый запрос в необработанное значение, используя привязку значения. В противном случае query.toString() используется для рендеринга каждой части запроса, что открывает возможную дыру для внедрения зависимостей (queryBuilder.toString() не так безопасен, как передача параметров драйверу в качестве привязок).

@elhigu Подождите ... query.toString() не использует привязки? Не могли бы вы привести пример модификации, которую вы рекомендуете? У меня... может быть много кода для обновления.

Нашел часть документации с пометкой Raw Bindings . Обновление сейчас Я обновил пример. Я думал, что query.toString в безопасности. Было бы хорошо, если бы раздел документации был помечен как "Как делать небезопасные запросы". Есть только несколько запретов, и таким образом люди могут использовать библиотеку, зная, что «пока я не делаю этих вещей, я в безопасности».

Я создал следующий upsert: https://gist.github.com/adnanoner/b6c53482243b9d5d5da4e29e109af9bd
Он обрабатывает одиночные и пакетные upserts. Я немного адаптировал его из @plurch . Улучшения всегда приветствуются :)

Для чего я использовал этот формат:

Изменить: обновлено для обеспечения безопасности для всех, кто ищет это. Спасибо @elhigu

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

@NicolajKN Вы не должны использовать toString (), это может вызвать множество проблем и не будет передавать значения через привязки к БД (потенциальная дыра в безопасности SQL-инъекций).

То же самое, если это сделано правильно, будет выглядеть так:

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

Удалено обсуждение несвязанной темы.

@elhigu Подождите, разве этот запрос на вставку не выполняется сразу после создания? Разве это не создает условия для гонки?

@cloutiertyler Вы не разговаривали со мной, но, может быть, я смогу сэкономить время @elhigu . Ни один из этих запросов не будет выполнен. Оператор knex('account').insert(accounts) не выполняет запрос. Он не выполняется до тех пор, пока данные не будут фактически вызваны (например, через .then ). Он отправляет это в knex.raw('? ON CONFLICT DO NOTHING', [query]) , который вызовет query.toString() , который только преобразует запрос в оператор SQL, который будет выполнен.

@timhuff Спасибо, Тим, я предполагал, что это должно быть что-то в этом роде, но это ненормальное поведение для обещания. Обещания обычно выполняются при создании. Причина, по которой я спрашиваю, заключается в том, что я время от времени получал сообщения об ошибках «Соединение прекращено», когда пытался запустить этот upsert. Как только я переключился на удаление вставки и создание полностью необработанного запроса, они исчезли. Кажется, что это соответствовало бы условиям гонки.

Однако knex QueryBuilder не являются Promise s. Когда вы начинаете писать knex-запрос, вы остаетесь в «knexland». Все, что вы делаете, — это более или менее просто настройка спецификации JSON запроса, который вы хотите создать. Если вы запустите .toString , он создаст его и выведет. Он не станет ( bluebird ) обещанием, пока вы не запустите на нем один из них . Вам может быть интересно использовать .return , если вы хотите немедленно выполнить оператор.

А, я вижу, что ж, это проясняет мое замешательство. Спасибо за разъяснения и подсказки! Тогда моя проблема должна существовать в другом месте.

Кроме того, тот факт, что он не запускается сразу, часто бывает полезен. Иногда вы хотите передать вещь, настроить ее перед выполнением. Есть также ситуации, когда вы можете делать что-то вроде...

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

(супер надуманный пример, но давайте попробуем)

На самом деле я не хочу запускать этот первый оператор - это всего лишь часть моего второго.

Не говоря уже о том, что если бы все построители запросов выполнялись при создании, запросы шаблона построителя срабатывали бы до завершения построения. Это вообще не сработало бы без какого-либо метода терминатора (который выполняет запрос).

@elhigu Я имею в виду ... Я думаю, вы всегда можете запустить его на следующем тике, верно? Я не утверждаю, что это было бы хорошей идеей, но сколько запросов на самом деле создается и выполняется на разных тиках?

@timhuff Я не думал об этом. Да, я тоже думаю, что это возможно. Я нахожу довольно частым случай, когда вы начинаете строить запрос, затем получаете некоторые асинхронные данные и продолжаете строить больше. Хотя я не так часто это делаю.

@lukewlms , что метод, похожий на «execute()», называется «.then()», вы всегда можете вызывать его, когда хотите выполнить запрос и получить обещание. Именно так работает «thenable», и это объясняется в спецификации обещания. Это одна важная и широко используемая концепция в javascript при работе с обещаниями и async/await (которые в значительной степени являются просто прославленными ярлыками для Promise.resolve и .then). Кроме того, если вы выполняете запросы без обработки результатов, вы ищете такие проблемы, как сбой приложения.

На самом деле лучше просто следовать этому PR о реализации функции upsert https://github.com/tgriesser/knex/pull/2197 , у него уже есть API, разработанный, как он должен работать. В этой ветке нет толком никакой полезной информации, которая уже не упомянута в комментариях этого пиара. При необходимости (PR закрыт и никогда не завершается) давайте откроем новый выпуск для этого с дополнительным описанием API.

@elhigu Спасибо за внимание! Я не знал об этой ветке. Приятно слышать, что мы делаем успехи в обновлении API. Похоже, что 6 месяцев назад он провалил 1 из 802 тестов и так и не прошел travis-ci. Является ли этот 1 неудачный тестовый пример единственным, что мешает ему стать частью API knex?

@timhuff была сделана только начальная реализация, ее нужно полностью переписать. Наиболее важной частью этого PR является общий дизайн API, который может поддерживаться большинством диалектов. Таким образом, функция появляется, когда кто-то просто решает реализовать этот API. Если никто больше этим не занимается и когда-нибудь у меня появится лишнее время или мне это будет очень нужно, я сделаю это сам. Это одна из самых важных функций, которую я хотел бы получить от knex (в дополнение к объединениям в обновлениях).

@elhigu Спасибо, что сообщили мне. Мне нужно будет прочитать о прогрессе здесь, когда у меня будет немного больше времени.

Я не уверен, поможет ли это кому-нибудь или я просто нуб, но для решения от @timhuff мне пришлось заключить ограничение в кавычки, потому что я получал синтаксическую ошибку запроса.

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

Я приходил к этому вопросу несколько раз в поисках upsert Postgres на основе knex. Если кому-то еще это нужно, вот как это сделать. Я тестировал это как с одиночными, так и с составными уникальными ключами.

Установка

Создайте ограничение уникального ключа для таблицы, используя приведенное ниже. Мне нужно составное ключевое ограничение:

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

Функция

(изменить: обновлено для использования необработанных привязок параметров)

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

использование

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

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

Если ваше ограничение не составное, то, естественно, эта одна строка будет просто constraint: '(a)' .

Это вернет либо обновленный объект, либо вставленный объект.

Примечание о составных обнуляемых индексах

Если у вас есть составной индекс (a,b) и b обнуляемый, то значения (1, NULL) и (1, NULL) считаются взаимно уникальными для Postgres (я не понимаю либо). Если это ваш вариант использования, вам нужно будет создать частичный уникальный индекс, а затем проверить его на значение null перед upsert, чтобы определить, какое ограничение использовать. Вот как сделать частичный уникальный индекс: CREATE UNIQUE INDEX unique_index_name ON table (a) WHERE b IS NULL . Если ваш тест определяет, что b имеет значение null, вам нужно будет использовать это ограничение в вашем upsert: constraint: '(a) WHERE b IS NULL' . Если a также обнуляется, я думаю, вам понадобятся 3 уникальных индекса и 4 ветви if/else (хотя это не мой вариант использования, поэтому я не уверен).

Вот скомпилированный javascript .

Надеюсь, кто-то найдет это полезным. @elhigu ~ Любой комментарий по поводу использования knex().update(object) ?~ (редактировать: неважно - видел предупреждение - сейчас использую knex.queryBuilder() )

Удалены некоторые несвязанные обсуждения (о том, как работают промисы/тенейблы).

Это добавилось?

Нет. В https://github.com/knex/knex/issues/3186 есть запрос функции и спецификация.

Была ли эта страница полезной?
0 / 5 - 0 рейтинги