Knex: 添加 upsert 类型功能

创建于 2013-08-30  ·  54评论  ·  资料来源: knex/knex

@adamscybot在 tgriesser/bookshelf#55 中提出 - 这可能是一个不错的添加功能。

feature request

最有用的评论

@NicolajKN您不应该使用 toString(),它可能会导致多种问题,并且不会通过绑定将值传递给 DB(潜在的 SQL 注入安全漏洞)。

同样正确完成将是这样的:

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

所有54条评论

我同意,这_将_是一个不错的功能!

:+1:

:+1:

我正在从 CSV 导入一些数据,并且很有可能一些记录与上次导入重叠(即上次从 1 月 1 日到 5 月 31 日导入,这次从 5 月 31 日到 6 月 18 日导入)。

幸运的是,第三方系统分配了可靠的唯一 ID。

插入新记录和更新旧记录的最佳方法是什么?

我还没有尝试过,但我想它会是这样的:

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

此外,有时我存储的东西是由确定的 id 值存储的,例如哈希。 在这些情况下,我只想添加或替换数据。

我并不总是将 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

我正在寻找一种在 MySql 中执行REPLACE INTO的方法,并找到了这个功能请求。 由于REPLACEINSERT在 MySql 中具有完全相同的语法,我想它比ON DUPLICATE KEY UPDATE更容易实现。 是否有计划实施REPLACE ? 公关会有价值吗?

对此有何更新,尤其是 PostreSQL 9.5?

我认为一个重要的问题是是否为不同的方言(例如 PostgreSQL 和 MySQL)公开相同的upsert方法签名。 在 Sequelize 中,关于upsert的返回值提出了一个问题: https://github.com/sequelize/sequelize/issues/3354。

我意识到一些 KnexJS 库方法在不同方言的上下文中的返回值上有区别(例如insert ,其中为 Sqlite 和 MySQL 返回第一个插入的 id 的数组,而所有插入的 id 与 PostgreSQL 一起返回)。

根据文档,MySQL 中的INSERT ... ON DUPLICATE KEY UPDATE语法具有以下行为(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 正好是 1,并且目标表具有 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。

这是一个超级 hacky 实验......所以不要将猴子补丁复制并粘贴到您的生产代码中:p

已知问题:

  • QueryBuilder 上的猴子补丁会影响所有方言,因为 Client_PG 没有专门化构建器。
  • 不支持像count = count + 1这样的原始更新
  • 如果查询方法不是插入,onConflict 应该可能会抛出。

回馈?

@hayeah我喜欢你的方法,它适合 Postgres。 我将在一个项目中尝试你的猴子补丁,看看我是否可以凭经验检测除你指出的问题之外的任何问题。

语法建议: knex('table').upsert(['col1','col2']).insert({...}).update({...});其中upsert将包含在条件语句中。 这样它就不是特定于数据库的。

可以在https://en.wikipedia.org/wiki/Merge_ (SQL) 中找到 upsert 的不同实现的摘要

我也有兴趣拥有这种能力。 用例:构建一个依赖于来自外部服务的大量外部数据的系统; 我会定期轮询它以获取我保存到本地 MySQL 数据库的数据。 现在可能会使用 knex.raw。

也很感兴趣,但在我的用例中,它需要以不基于冲突的方式工作,因为列并不总是具有“唯一”约束 - 如果存在匹配查询的条目,只需更新它们,否则插入新行。

@haywirez我很好奇为什么没有独特的约束? 你不会暴露在比赛条件下吗?

@hayeah我有一个带有时间窗数据的特定用例,存储具有与给定日期相关的值的条目。 因此,我正在插入和更新具有匹配(日)时间戳的“组合键”的条目,以及与其他表中的 PK 对应的另外两个 ID。 在 24 小时内,我必须插入它们,或者用最新的计数更新它们。

这将是一个很棒的功能!

大家好,曾经在这里评论过的人。 我正在添加 PR Please 标签。

很高兴通过 PR 添加此功能,但我希望先在此处查看有关所需 API 的讨论。

PS。

^ 同意。

我要删除这样的评论,如果你想添加+1,请使用小表情符号反应。

我对@willfarrell@hayeah的示例中的列限制数组有一点问题。 不确定这些示例是否可以支持json属性。 这些提案中没有一个不包括 where 语句/正确的“查询”以匹配记录是否有原因?

提案一

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示例显示“首先尝试更新WHERE ...然后如果没有文档与查询匹配则执行INSERT”,而SQL UPSERT显示“INSERT INTO ...更新以防具有此主键的行已经存在” .
所以,我想,你说的是一个完全不同的“upsert”,而不是它在 SQL 数据库中实现的。

我会提出这个API:

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

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

.insert()函数将分析第二个参数。
如果它是一个字符串,那么它就是旧的returning参数。
如果它是一个对象,那么它是一个具有options.returningoptions.upsertoptions参数,其中options.upsert是唯一键的列表(可以是 > 1 in复合唯一键约束的情况)。
然后生成一个 SQL 查询,它只是从object (通过clone(object) && delete cloned_object.id && delete cloned_object.unique )中排除主键和所有options.upsert键,然后使用剥离的cloned_object在 SQL 查询的第二部分中构造SET子句的主键(和唯一键): ... ON CONFLICT DO UPDATE SET [iterate cloned_object]

我想这将是与当前 API 同质的最简单、最明确的解决方案。

@slavafomin @ScionOfBytes看起来甚至还没有就 API 达成一致。 那将是下一步,然后喜欢实施它的人可能会这样做。 所以没有消息。

附言。 如果没有任何其他新闻请求,我开始删除任何其他新闻请求,以防止该线程被新闻请求垃圾邮件和其他不太相关的消息填满。

@amir-s 我同意,但这个问题的主题是 upsert 功能。

IMO,真正的问题不是 API,而是在每个数据库中进行 upserts 的不常见方式。

MySQL (ON DUPLICATE KEY UPDATE) 和 PostgreSQL 9.5+ (ON CONFLICT DO UPDATE) 默认支持 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 答案

为了寻找基于 knex 的 Postgres upsert,我多次遇到这个问题。 如果其他人需要这个,这里是如何做到的。 我已经针对单个和复合唯一键对此进行了测试。

设置

使用以下方法在表上创建唯一键约束。 我需要一个复合键约束:

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可以为空,那么 Postgres 认为值(1, NULL)(1, NULL)是相互唯一的(我不明白任何一个)。 如果这是您的用例,您需要创建一个部分唯一索引,然后在 upsert 之前测试 null 以确定要使用的约束。 以下是制作部分唯一索引的方法: CREATE UNIQUE INDEX unique_index_name ON table (a) WHERE b IS NULL 。 如果您的测试确定b为空,那么您需要在 upsert 中使用此约束: constraint: '(a) WHERE b IS NULL' 。 如果a也可以为空,我猜你需要 3 个唯一索引和 4 个if/else分支(尽管这不是我的用例,所以我不确定)。

这是编译后的 javascript

希望有人觉得这很有用。 @elhiguknex().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(),它可能会导致多种问题,并且不会通过绑定将值传递给 DB(潜在的 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谢谢蒂姆,我认为它必须是这样的,但这不是承诺的正常行为。 Promise 通常在创建时执行。 我问的原因是,当我尝试运行这个 upsert 时,我经常收到“连接终止”的错误消息。 一旦我切换到删除插入并创建一个完全原始的查询,它们就消失了。 这似乎与比赛条件一致。

但是,knex QueryBuilder不是Promise 。 当您开始编写 knex 查询时,您将停留在“knexland”中。 您所做的一切或多或少只是配置您要构建的查询的 JSON 规范。 如果您运行.toString ,它会构建它并输出它。 它不会成为 ( bluebird ) Promise,直到您在其上运行其中一个。 如果您想立即执行该语句,您可能对使用.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”的工作原理,并在 promise 规范中进行了解释。 在处理 Promise 和 async/await 时,它是 javascript 中一个重要且广泛使用的概念(它们几乎只是 Promise.resolve 和 .then 的美化快捷方式)。 此外,如果您在不处理结果的情况下执行查询,您正在寻找应用程序崩溃等问题。

实际上,最好只关注这个关于 upsert 功能实现的 PR https://github.com/tgriesser/knex/pull/2197它已经有 API 设计了它应该如何工作。 在这个线程中,实际上并没有在该 PR 的评论中提到的任何有用信息。 如果需要(PR 已关闭且从未完成),请为该问题打开新问题,并提供额外的 API 描述。

@elhigu感谢您的提醒! 我不知道那个线程。 很高兴听到我们在 API 的 upsert 方面取得了进展。 看起来 6 个月前它没有通过 802 测试中的 1 次,因此它从未通过 travis-ci。 那 1 个失败的测试用例是唯一阻止它成为 knex API 的一部分的原因吗?

@timhuff只完成了初始实现,必须完全重写。 该 PR 最重要的部分是通用 API 设计,大多数方言都可以支持。 因此,当有人决定实现该 API 时,该功能就会出现。 如果没有其他人这样做,并且有一天我有一些额外的时间或非常需要它,我会自己做。 这是我希望 knex 获得的最重要的功能之一(除了加入更新)。

@elhigu感谢您填写。当我有更多时间时,我将不得不在这里阅读进度。

我不确定这是否对任何人都有帮助,或者我只是一个菜鸟,但对于@timhuff的解决方案,我不得不将我的约束用引号括起来,因为我遇到了查询语法错误。

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

为了寻找基于 knex 的 Postgres upsert,我多次遇到这个问题。 如果其他人需要这个,这里是如何做到的。 我已经针对单个和复合唯一键对此进行了测试。

设置

使用以下方法在表上创建唯一键约束。 我需要一个复合键约束:

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可以为空,那么 Postgres 认为(1, NULL)(1, NULL)的值是相互唯一的(我不明白任何一个)。 如果这是您的用例,您需要创建一个部分唯一索引,然后在 upsert 之前测试 null 以确定要使用的约束。 以下是制作部分唯一索引的方法: CREATE UNIQUE INDEX unique_index_name ON table (a) WHERE b IS NULL 。 如果您的测试确定b为空,那么您需要在 upsert 中使用此约束: constraint: '(a) WHERE b IS NULL' 。 如果a也可以为空,我猜你需要 3 个唯一索引和 4 个if/else分支(尽管这不是我的用例,所以我不确定)。

这是编译后的 javascript

希望有人觉得这很有用。 @elhigu ~对knex().update(object)的用法有任何评论吗?~(编辑:没关系 - 看到警告 - 现在使用knex.queryBuilder()

删除了一些不相关的讨论(关于 promises/thenables 的工作方式)。

这被添加了吗?

没有。 https://github.com/knex/knex/issues/3186中有功能请求和规范

此页面是否有帮助?
0 / 5 - 0 等级

相关问题

hyperh picture hyperh  ·  3评论

fsebbah picture fsebbah  ·  3评论

rarkins picture rarkins  ·  3评论

legomind picture legomind  ·  3评论

arconus picture arconus  ·  3评论