在 Rails 中防止重复的一种方法是使用验证:
Correct way of prevent duplicate records in Rails
但是,您的条件更复杂,因为它涉及跨越多行。
我相信您的标准是,如果最近的过境记录是在不到 20 秒前创建的,则不允许输入过境记录。对吗?
这里提到了试图强制执行涉及查看多行数据的约束是不可取的:
SQL Sub queries in check constraint
触发器可用于在数据库级别强制执行您的约束。
可以在异常中捕获触发器。
有一个名为 HairTrigger 的宝石可能有用,但不确定。
从这里汲取灵感:
https://karolgalanciak.com/blog/2016/05/06/when-validation-is-not-enough-postgresql-triggers-for-data-integrity/
Postgresql 触发器示例:
bin/rails generate model transit tag:text
rails generate migration add_validation_trigger_for_transit_creation
class AddValidationTriggerForTransitCreation < ActiveRecord::Migration[5.2]
def up
execute <<-CODE
CREATE FUNCTION validate_transit_create_time() returns trigger as $$
DECLARE
age int;
BEGIN
age := (select extract(epoch from current_timestamp - t.created_at)
from transits t
where t.tag = NEW.tag
and t.id in (select id from transits u
where u.id = t.id
and u.tag = t.tag
and u.created_at = (select max(v.created_at) from transits v where v.tag = u.tag)
));
IF (age < 20) THEN
RAISE EXCEPTION 'created_at too early: %', NEW.created_at;
END IF;
RETURN NEW;
END;
$$ language plpgsql;
CREATE TRIGGER validate_transit_create_trigger BEFORE INSERT OR UPDATE ON transits
FOR EACH ROW EXECUTE PROCEDURE validate_transit_create_time();
CODE
end
def down
execute <<-CODE
drop function validate_transit_create_time() cascade;
CODE
end
end
user1@debian8 /home/user1/rails/dup_test > ../transit_test.rb ; sleep 20; ../transit_test.rb
dup_test_development=> select * from transits;
id | tag | created_at | updated_at
-----+----------+----------------------------+----------------------------
158 | test_tag | 2019-01-31 18:38:10.115891 | 2019-01-31 18:38:10.115891
159 | test_tag | 2019-01-31 18:38:30.609125 | 2019-01-31 18:38:30.609125
(2 rows)
这是我们的查询部分,它提供了带有我们标签的最新过境条目
dup_test_development=> select * from transits t
where t.tag = 'test_tag' and t.id in
(select id from transits u where u.id = t.id and u.tag = t.tag and u.created_at =
(select max(v.created_at) from transits v where v.tag = u.tag));
id | tag | created_at | updated_at
-----+----------+----------------------------+----------------------------
159 | test_tag | 2019-01-31 18:38:30.609125 | 2019-01-31 18:38:30.609125
(1 row)
修改以提供 current_timestamp(现在)和带有我们标签的最新过境条目之间的差异。这种差异是 postgresql 中的一个间隔。使用 UTC 匹配 Rails:
dup_test_development=> select current_timestamp at time zone 'utc' - created_at
from transits t where t.tag = 'test_tag' and t.id in
(select id from transits u where u.id = t.id and u.tag = t.tag and u.created_at =
(select max(v.created_at) from transits v where v.tag = u.tag));
?column?
-----------------
00:12:34.146536
(1 row)
添加 Extract(epoch) 以将其转换为秒:
dup_test_development=> select extract(epoch from current_timestamp at time zone 'utc' - created_at)
from transits t where t.tag = 'test_tag' and t.id in
(select id from transits u where u.id = t.id and u.tag = t.tag and u.created_at =
(select max(v.created_at) from transits v where v.tag = u.tag));
date_part
------------
868.783503
(1 row)
我们将秒存储为年龄,如果年龄
以小于 20 秒的延迟运行 2 次插入:
user1@debian8 /home/user1/rails/dup_test > ../transit_test.rb ; sleep 5; ../transit_test.rb
#<ActiveRecord::StatementInvalid: PG::RaiseException: ERROR: created_at too early: 2019-01-31 18:54:48.95695
: INSERT INTO "transits" ("tag", "created_at", "updated_at") VALUES ($1, $2, $3) RETURNING "id">
"ERROR: created_at too early: 2019-01-31 18:54:48.95695\n"
rails 外的短期测试:
#!/usr/bin/env ruby
require 'active_record'
require 'action_view'
path = "/home/user1/rails/dup_test/app/models"
require "#{path}/application_record.rb"
Dir.glob(path + "/*.rb").sort.each do | file |
require file
end
ActiveRecord::Base.establish_connection(
:adapter => "postgresql",
:database => 'dup_test_development',
encoding: "unicode",
username: "user1",
password: nil
)
class Test
def initialize()
end
def go()
begin
t = Transit.new(tag: 'test_tag')
t.save
rescue ActiveRecord::StatementInvalid => e
p e
p e.cause.message
end
end
end
def main
begin
t = Test.new()
t.go()
rescue Exception => e
puts e.message
end
end
main
已经提到使用 Redis 之类的东西 - 性能可能更好