我知道过去在这个主题上有一些问题,但是在Rails6发布之后,我希望事情有所改变。
- 目标**
有了Role
和Effect
ActiveRecord模型,我想临时设置关联(即应该有role.add_effect(effect)
和role.remove_effect(effect)
方法)来跟踪给定角色当前激活的效果。请注意,效果生存期超出了这些临时关联,许多角色可能在某个时候激活相同的效果。
假设中间对象(是否有此类模型的术语?)
class RoleEffect < ApplicationRecord
belongs_to :role
belongs_to :effect
end
class CreateRoleEffects < ActiveRecord::Migration[6.0]
def change
create_table :role_effects do |t|
t.belongs_to :role, null: false, foreign_key: true
t.references :effect, null: false, foreign_key: true
t.integer :counter, null: false, default: 0
t.timestamps
end
add_index :role_effects, [:role_id, :effect_id], unique: true
end
end
就SQL而言,它类似于
INSERT INTO role_effects (role_id, effect_id, counter, created_at, updated_at)
VALUES (:role_id, :effect_id, :delta, :ts, :ts)
ON CONFLICT (role_id, effect_id)
DO UPDATE SET counter = counter + excluded.counter, updated_at = :ts
"所以问题是"
现在是什么情况(从Rails6开始)实现此类add/remove方法的正确方法,以便它们是原子。我看到有upsert方法可用,但来自文档(也不知道源代码)我不明白如何执行counter = counter + excluded.counter
位。我也看到了使用https://github.com/zdennis/activerecord-import gem的建议,但是让第三方包只执行一个SQL查询似乎有些过头了。
如果upsert
方法的当前实现不适用于此类用例,我不介意将其作为自定义SQL查询,但我也不明白什么是适当的API来安全地执行它,安全地替换(净化)所有这些值,最好是通过名称而不是匿名的"?"和排序来提及。
我最初的尝试是
def add_effect(effect)
change_effect_counter(effect, 1)
end
def remove_effect(effect)
change_effect_counter(effect, -1)
end
private
def change_effect_counter(effect, delta)
RoleEffect.connection.exec_insert(<<~SQL.strip, {role_id: self.id, effect_id: effect.id, delta: delta, ts: Time.now})
INSERT INTO role_effects (role_id, effect_id, counter, created_at, updated_at)
VALUES (:role_id, :effect_id, :delta, :ts, :ts)
ON CONFLICT (role_id, effect_id)
DO UPDATE SET counter = counter + :delta, updated_at = :ts
SQL
nil
end
但是由于与预期的绑定格式(相当难看的对数组)不匹配,它失败得很惨。
由于PostgreSQL和SQLite都支持这样的查询,我希望解决方案对它们都有效。
- 更新**(第一次注解后)
我当然有
has_many :role_effects, dependent: :destroy
has_many :effects, through: :role_effects
但这不提供修改关联的原子操作。
我可以放弃相反的想法,删除唯一性索引并添加role.effects << effect
这样的效果,但删除一个关联将是棘手的,因为它将需要DELETE ... LIMIT 1
,这在SQLite中是默认不可用的(我知道它可以用一个特殊的标志重新编译,但这对开发环境来说是太多的要求)。
或者我可以用
RoleEffect.find_or_create_by!(role: role, effect: effect).increment!(:counter, delta)
但我不确定这两个调用是否都能保证并发情况下的正确行为,即使是这样,它也使用多个查询而不是INSERT ... ON CONFLICT UPDATE
。
1条答案
按热度按时间pb3skfrl1#
为了以防万一,我会发布我的发现,尽管结果是相当不完美的(例如,插入没有返回模型对象,这可能是意外的,像role.role_effects这样的缓存可能是陈旧的,等等)。
用途
2023年1月31日更新
Rails 7添加了一个选项来指定
ON CONFLICT
行为:老实说,我希望不需要显式的
:unique_by
,但它是:至少对于SQLite,生成的SQL具有ON CONFLICT ("id")
,忽略了表具有的唯一索引。无论如何,看起来比原始SQL更好。