How to use PostgreSQL triggers? - ruby-on-rails

I am trying to use PostgreSQL triggers in my rails app. So I tried using this migration where execution of triggers is supposedly easy:
-- class AddTriggersToProducts < ActiveRecord::Migration
def self.up
table :products
execute %q{
create trigger trig1 before insert on products for each row
begin
price = price + 5
end;
}
end
def self.down
execute 'DROP TRIGGER trig1'
end
end
But this didn't change anything. I don't know where to write the procedure or function if I am going to use one here ...

"Creating a trigger" consists of two steps in PostgreSQL:
1.) Create a trigger function - with special return value trigger:
CREATE FUNCTION trg_update_prod_price()
RETURNS trigger AS
$func$
BEGIN
NEW.price := NEW.price + 5;
RETURN NEW;
END
$func$ LANGUAGE plpgsql;
Multiple triggers can use the same trigger function.
2.) Create a trigger calling an existing trigger function:
CREATE TRIGGER update_prod_price
BEFORE INSERT ON products
FOR EACH ROW EXECUTE PROCEDURE trg_update_prod_price();
To "drop the trigger" (meaning the trigger function), you have to first drop all triggers referencing it and then drop the trigger function itself.
DROP TRIGGER update_prod_price ON products;
DROP FUNCTION trg_update_prod_price();
If you drop a table, all attached triggers are dropped with it. No need to drop those separately.

Does something like this work? Creating a function and then executing the function for the trigger:
def self.up
execute %q{
create or replace function update_price() returns trigger as $$
begin
NEW.price := NEW.price + 5;
return NEW;
end;
$$ language plpgsql }
execute %{ create trigger trig1 before insert on products for each row execute function update_price()}
end

The hair_trigger gem is a nice way to manage the creation of triggers.
Here is an example from hair_trigger's docs:
class AccountUser < ActiveRecord::Base
trigger.after(:insert) do
"UPDATE accounts SET user_count = user_count + 1 WHERE id = NEW.account_id;"
end
trigger.after(:update).of(:name) do
"INSERT INTO user_changes(id, name) VALUES(NEW.id, NEW.name);"
end
end

Related

Rails Query a List for a CRON Job

I'm a complete novice with CRON jobs but I think I have that set up correctly.
Ultimately what I'm trying to do is send an email every day at 8:00 am to users (and a couple others) that have not logged in within the last 3 days, have not received the email, AND are marked as active OR temp as a status.
So from querying the db in console I know that I can do:
first = User.where(status: 'active').or(User.where(status: 'temp'))
second = first.where("last_login_at < ? ", Time.now-3.days)
third = second.where(notified: false)
That's not certainly clean but I was struggling with finding a contained query that grabbed all that data. Is there a cleaner way to do this query?
I believe I have my cron job set up correctly using a runner. I have whenever installed and in my schedule.rb I have:
every 1.day, at: '8:00 am' do
runner 'ReminderMailer.agent_mailer.deliver'
end
So under app > mailer I created ReminderMailer
class ReminderMailer < ApplicationMailer
helper ReminderHelper
def agent_reminder(user)
#user = user
mail(to: email_recipients(user), subject: 'This is your reminder')
end
def email_recipients(agent)
email_address = ''
email_addresses += agent.notification_emails + ',' if agent.notification_emails
email_addresses += agent.manager
email_address += agent.email
end
end
Where I'm actually struggling is where I should put my queries to send to the mailer, which is why I built a ReminderHelper.
module ReminderHelper
def applicable_agents(user)
agent = []
first = User.where(status: 'active').or(User.where(status: 'temp'))
second = first.where("last_login_at < ? ", Time.now-3.days)
third = second.where(notified: false)
agent << third
return agent
end
end
EDIT: So I know I could in theory do a chain of where queries. There's gotta be a better way right?
So what I need help on is: do I have the right structure in place? Is there a cleaner way to query this data in ActiveRecord for the CRON job? Is there a way to test this?
Try combining them together as if understand the conditions correct
Have not logged in within the last 3 days,
Have not received the email
Are marked as active OR temp as a status
User.where("last_login_at < ? ", 3.days.ago).
where(notified: false).
where(status: ['active', temp])
module ReminderHelper
def applicable_agents(user)
User.where("last_login_at < ? ", 3.days.ago).
where(notified: false).
where(status: ['active', temp])
end
end
You don't need to add/ assign them to array. Because this relation is already like an array. You can use .to_a if you need array. If you just want to iterate over them then users.each should work fine.
Update
class User
scope :not_notified, -> { where(notified: false) }
scope :active_or_temp, -> { where(status: ['active', 'temmp']) }
scope :last_login_in, -> (default_days = 3) { where("last_login_at < ?", default_days.days.ago) }
end
and then use
User.not_notified.active_or_temp.last_login_in(3)
Instead of Time.now-3.days it's better to use 3.days.ago because it keeps time zone also in consideration and avoids unnecessary troubles and failing test cases.
Additionally you can create small small scopes and combine them. More read on scopes https://guides.rubyonrails.org/active_record_querying.html

Rails, add a trigger to update more than one column

Following this guide here, I want to add a trigger, that updates more than one attribute of a model. Here is what I have now:
class AddTriggerToArguments < ActiveRecord::Migration[5.0]
def up
execute %{
CREATE TRIGGER tsvectorupdate BEFORE INSERT OR UPDATE
ON arguments FOR EACH ROW EXECUTE PROCEDURE
tsvector_update_trigger(tsv_body, 'pg_catalog.simple', description);
}
end
def down
execute %{DROP TRIGGER tsvectorupdate ON arguments}
end
end
As you can see in the line
tsvector_update_trigger(tsv_body, 'pg_catalog.simple', description);
updates the description attribute. But what do I have to do, so that multiple attributes will be updated?
My model has title and description as attributes. I want to add title to the trigger. I tried this, but it didnt work:
def up
execute %{
CREATE TRIGGER tsvectorupdate BEFORE INSERT OR UPDATE
ON arguments FOR EACH ROW EXECUTE PROCEDURE
tsvector_update_trigger(tsv_body, 'pg_catalog.simple', description)
tsvector_update_trigger(tsv_body, 'pg_catalog.simple', title);
}
end
But that didn't work. Can anyone help me?
tsvector_update_trigger(tsv_body, 'pg_catalog.simple', description, title)
Seems to work.

How to update all records that were created yesterday with Rails?

I need to update the info on all records that were created yesterday, for that I created a migration with the following code:
class UpdateFbidToVotes < ActiveRecord::Migration
def up
Vote.all.each do |v|
if v.created_at == Date.today-1
v.fbid = v.fbid + 1
v.update!
end
end
end
end
This, however, doesn't work. Can you please point me to what am I doing wrong?
Thanks!
Something like this:
Vote.where(created_at: Date.yesterday).update_all('fbid = fbid + 1')
Try:
yesterday = 1.day.ago # or Time.now.yesterday
Vote.where(created_at: yesterday.beginning_of_day..yesterday.end_of_day).update_all('fbid = fbid + 1')
Also, migration files are meant for managing table schema, I'd encourage you to move such db update/delete queries to a rake task instead of migration file, such queries are not reversible(i.e. you can not rely on getting the changes back to previous state) and should not be a part of migrations.

Why would this not work in Spree

I am having problems with modifying a function in spree. The function is called copy_price
The original version is something like this:
def copy_price
if variant
self.price = variant.price if price.nil?
self.currency = variant.currency if currency.nil?
end
end
which if I understand right will update the line_item's unit price only if the price is null, which I believe it shouldn't be inside the orders page (after the order is completed).
I noticed that order changes if the master price is changed inside the admin section even after the order is complete.
So i thought that the copy_price function was to blame, but each time i try to modify it there is no change.
E.g.
def copy_price
#price_run = true
self.price = 30.00
end
def get_price_run
if #price_run == true
return "true"
else
return "false"
end
end
and call get_price_run inside my view to print out if the price run was actually run. and It keeps outputting false. Does anyone know why that would be.
I have figured out the problem. The function copy_price is only called when the line item is first created (e.g. when you put it into the cart). So when I was trying to find out if it was called while looking at the admin orders page it was never getting called.

How to migrate the database when renaming an ActiveRecord model that is a subclass of a base class?

How can I write a Rails migration to rename an ActiveRecord subclass when the models are stored in the database using single table inheritance (STI)? The only thing I can think of is to use a raw sql update statement to change the value of type column value from the old subtype name to the new one.
You can use execute to run raw queries in the migrations
class YourMigration < ActiveRecord::Migration
def up
execute "UPDATE table_name SET type = 'Namespace::NewSubclass' WHERE type = 'Namespace::OldSubclass'"
end
def down
execute "UPDATE table_name SET type = 'Namespace::OldSubclass' WHERE type = 'Namespace::NewSubclass'"
end
end
Rails 4 lets you define a reversible migration for #deefour's answer:
def change
rename_sti_type :table_name, 'Namespace::OldSubclass', 'Namespace::NewSubclass'
end
def rename_sti_type(table_name, old_type, new_type)
reversible do |dir|
dir.up { execute "UPDATE #{table_name} SET type = '#{new_type}' WHERE type = '#{old_type}'" }
dir.down { execute "UPDATE #{table_name} SET type = '#{old_type}' WHERE type = '#{new_type}'"}
end
end
You could generalise this into a change_data(table, field, old_value, new_value) method; but be mindful that if the up migration sets the type/field to a new_value which is already in use, then the down migration will also change existing rows which already had new_value value into old_value.

Resources