Один из способов предотвращения дублирования в Rails - это проверки: Правильный способ предотвращения дублирования записей в Rails
Однако ваш критерий более сложен, так как он охватывает несколько строк.Я считаю, что ваш критерий заключается в том, чтобы не допускать запись транзитной записи, если последняя транзитная запись была создана менее 20 секунд назад.Это правильно?
Попытка применить ограничение, которое включает просмотр данных из многих строк, здесь упоминается как нежелательная: Подзапросы SQL в проверочном ограничении
Триггер можетбыть использованы для обеспечения соблюдения вашего ограничения на уровне базы данных.Можно было нажать на курок в исключении.Есть драгоценный камень под названием 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)
Добавление выдержки (эпохи) для преобразования этого значения в секунды:
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,мы поднимаем исключение базы данных </p>
Выполнение 2 вставок со второй задержкой менее 20:
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"
Короткий тест вне рельсов:
#!/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было упомянуто - может быть лучше для производительности