How to stop a migration if a validation is false? - ruby-on-rails

Requirements
I need to create a migration, for a live production database that does the following:
For each company verify if a set of names are instances of an utterance class associated with it.
If said validation doesn't exist have the validation fail and return an error.
If not continue migrating
Current behaviour
I tried this:
class AddFeature < ActiveRecord::Migration[5.1]
def change
run_migration = true
Company.all.each do |organization|
Company.product_types_names.each { |type| run_migration &= Utterance.exists?("utter_#{type.to_s}", organization.id) }
end
if run_migration
# my code
end
end
end
Although the changes to the database, don't occur I need the migration to stop with an error. Currently, the migration isn't stopped by any form of error when I an utterance doesn't exist.
Expected behavior
I would like to know how to simply return an error and stop the migration when one any of the instances don't exist.
Something like this:
class AddFeature < ActiveRecord::Migration[5.1]
def change
Company.all.each do |organization|
Company.product_types_names.each { |type| run_migration &= Utterance.exists?("utter_#{type.to_s}", organization.id) }
# return_errors_and stop the app if validation false
end
# my code
end
end

Generally speaking, it is not recommended to write your custom code in Rails migrations. Migrations are for manipulations on database schema. You manipulate on data.
Answering your question: you can simply stop your migration by raising an exception, such as:
raise StandardError, "Invalid data"
In this case the migration will be stopped and not marked as completed (migration version won't be saved to schema_migrations table in your database). On next call of rake db:migrate it will try to run that migration again.

Related

Papertrail: records with nil whodunnit values and object_changes in logs

I've set papertrail to only record changes containing a whodunnit value/when an admin makes a change by using the below condition in my model:
has_paper_trail if: proc { |model| PaperTrail.request.whodunnit.present? }
However I've noticed there are still a decent amount of records being stored with empty whodunnit values. From having a look at the records, these seem to be mostly 'update' actions all having empty object changes for some reason. I am unsure as to why the value is empty, or how it would get saved at all considering the above condition.
I am getting whodunnit values from warden in my application controller using:
def user_for_paper_trail
request.env['warden']&.user(:admin)&.id
end
Has anyone come across similar behaviour before?
Adding a source_location and command field to your papertrail versions table will help you:
track what command is causing the nil whodunnit changes.
Ensure you record correctly changes initiated by a rake task or rails console.
To do this, create a migration to add the source_location and command fields to your Versions table:
class AddSourceLocationAndCommandToVersions < ActiveRecord::Migration
def change
add_column :versions, :source_location, :text
add_column :versions, :command, :text
end
end
I have the following set up in my papertrail.rb. I am using the JSON serializer but these changes may work with the normal serializer. The code after the serializer declaration is what is of interest to you:
require "paper_trail/frameworks/rails"
require "paper_trail"
# the following line is required for PaperTrail >= 4.0.0 with Rails
PaperTrail::Rails::Engine.eager_load!
PaperTrail.config.enabled = true
PaperTrail.serializer = PaperTrail::Serializers::JSON
# Store some metadata about where the change came from, even for rake tasks, etc.
# See: https://github.com/paper-trail-gem/paper_trail/wiki/Setting-whodunnit-in-the-rails-console
def PaperTrail.set_global_metadata
request.controller_info ||= {}
request.controller_info[:command] ||= "#{File.basename($PROGRAM_NAME)} #{ARGV.join ' '} (#{$PID})"
request.controller_info[:source_location] = caller.find { |line|
line.starts_with? Rails.root.to_s and
!line.starts_with? __FILE__
}
end
# Defer evaluation in case we're using spring loader (otherwise it would be something like "spring app | app | started 13 secs ago | development")
# There's no way to set up deferred evaluation of PaperTrail.request.controller_info from here like
# we can with whodunnit, so abuse that property of PaperTrail.request.whodunnit to set other
# metadata. Reserve the whodunnit field for storing the actual name or id of the human who made the
# change.
PaperTrail.request.whodunnit = ->() {
PaperTrail.set_global_metadata
if Rails.const_defined?("Console") || $rails_rake_task
"#{`whoami`.strip}: console"
else
"#{`whoami`.strip}: #{File.basename($PROGRAM_NAME)} #{ARGV.join ' '}"
end
}
Rails.application.configure do
console do
PaperTrail.request.controller_info = { command: "rails console" }
PaperTrail.request.whodunnit = ->() {
PaperTrail.set_global_metadata
#paper_trail_whodunnit ||= (
if Rails.const_defined?("Console") || $rails_rake_task
"#{`whoami`.strip}: console"
else
"#{`whoami`.strip}: #{File.basename($PROGRAM_NAME)} #{ARGV.join ' '}"
end
)
}
end
end
Note that in any place where record creation occurs outside of a request, you can manually set the whodunnit value to something specific if you don't want it blank. E.g. in my seed file I do the following:
class SeedCreator
def initialize
PaperTrail.request.whodunnit = ->() {
PaperTrail.set_global_metadata # save source_location & command
"seed" # set whodunnit value to "seed"
}
create_campaign
create_users
# more seeding of models....
end
end
In addition to improving the quality of your Papertrail table (knowing which command triggered a record update), this config should help you identify where phantom whodunnits are being saved.

Modifying the Rails Object for the seed process

Is it possible to modify the Rails obj?
I only want to modify it briefly and change it back.
My Reasoning:
I am trying to work on my seeds file and make it a little more robust.
In my model there is a process that looks at the current controller and the current user, it tracks this user during there session.
It throws an error though during my seed tests because there is no controller based user session.
What I wanted to do was to add
Rails.seed = true
at the start of my seed, it would get to the model and in the model I would wrap a control flow(if statement) for this property around the block that setups up tracking.
Then I would remove
Rails.seed = true
at the end of the seed file.
Instead of putting it directly on the Rails object, you can use custom configuration
config/initializers/custom_config.rb (name unimportant, just in an initializer)
Rails.configuration.seeding = false
db/seeds.rb
Rails.configuration.seeding = true
User.create
app/models/user.rb
class User < ApplicationRecord
# just as an example
after_initialize do
if Rails.configuration.seeding
puts "Seeding DB"
else
puts "Normal"
end
end
end
output
$ bin/rake db:seed
# Running via Spring preloader in process 19017
# Seeding DB
$ bin/rails c
User.create
# Normal
# => #<User ...>
I wouldn't necessarily recommend modifying the Rails class but to achieve that you could do something like:
class Rails
attr_accessor :seeding
def seeding=(bool)
#seeding = bool
end
def seeding?
#seeding ||= false
end
end
Then you could use Rails.seeding = true to set it and Rails.seeding? to access it. Also it will default to false if it is unset.
Another solution might be wrapping the part that is blowing up in a being rescue block to catch the error.

Rolling back a Rails migrations with execute

I would like to know what Rails does when rolling back a migration file, if it has an execute with some raw SQL statement.
Here is a contrived example:
def change
add_column :my_table, :new_column
execute "update my_table set new_column = some_value where some_condition"
end
When rolling back this migration, does Rails silently ignore the execute portion of the migration?
If you look in command_recorder.rb (in the active record gem) then you'll see that some migration methods (eg add_column) have a corresponding invert_foo method that reverses the change. If there is no corresponding invert method then rails will raise an error. This is the case for it has no way of knowing how to reverse an arbitrary statement.
If you would like your migration to be reversible you need to tell rails how to reverse it. The general form for this is
reversible do |direction|
direction.up { ... }
direction.down {...}
end
The up and down methods can be called as many times as you need and will only yield to the block if the direction matches whether the migration is running or being rolled back.
In your case this would just be
reversible do |direction|
direction.up { execute "..." }
end
Since you don't need to undo changes to the column if the column is about to be dropped.
I would assume as much. You should split this into an up and a down migration.
Down should look something like this:
def down
raise ActiveRecord::IrreversibleMigration
end

How to exclude associations build with model.associated.build?

I'm currently upgrading an existing application from rails 3 to 4. I've encountered changed behaviour, but I'm not sure how to fix this.
say the following exists;
class Program
has_many :pauses
end
class Pause
belongs_to :program
def dates
(starts_at...ends_at)
end
validate :validate_without_overlap
def validate_without_overlap
return if (program.pauses.map(&:dates).flatten & [starts_at, ends_at]).blank?
# set errors...
end
end
program = Program.create
program.pauses.build starts_at: 1.week.ago.to_date, ends_at: Date.today
# ...
program.save
To verify the pause does not have a overlap with existing pauses, within a
validation method the following happens:
program.pauses.map(&:dates)
This already includes the builded pause record. Which triggers a validation error because it overlaps itself. How to kill groundhog day?
Just exclude new records use something like this:
program.pauses.select { |o| !o.new_record? }.map(&:dates)
or
program.pauses.reject(&:new_record?).map(&:dates)

How to save something to the database after failed ActiveRecord validations?

Basically what I want to do is to log an action on MyModel in the table of MyModelLog. Here's some pseudo code:
class MyModel < ActiveRecord::Base
validate :something
def something
# test
errors.add(:data, "bug!!")
end
end
I also have a model looking like this:
class MyModelLog < ActiveRecord::Base
def self.log_something
self.create(:log => "something happened")
end
end
In order to log I tried to :
Add MyModelLog.log_something in the something method of MyModel
Call MyModelLog.log_something on the after_validation callback of MyModel
In both cases the creation is rolled back when the validation fails because it's in the validation transaction. Of course I also want to log when validations fail. I don't really want to log in a file or somewhere else than the database because I need the relationships of log entries with other models and ability to do requests.
What are my options?
Nested transactions do seem to work in MySQL.
Here is what I tried on a freshly generated rails (with MySQL) project:
./script/generate model Event title:string --skip-timestamps --skip-fixture
./script/generate model EventLog error_message:text --skip-fixture
class Event < ActiveRecord::Base
validates_presence_of :title
after_validation_on_create :log_errors
def log_errors
EventLog.log_error(self) if errors.on(:title).present?
end
end
class EventLog < ActiveRecord::Base
def self.log_error(event)
connection.execute('BEGIN') # If I do transaction do then it doesn't work.
create :error_message => event.errors.on(:title)
connection.execute('COMMIT')
end
end
# And then in script/console:
>> Event.new.save
=> false
>> EventLog.all
=> [#<EventLog id: 1, error_message: "can't be blank", created_at: "2010-10-22 13:17:41", updated_at: "2010-10-22 13:17:41">]
>> Event.all
=> []
Maybe I have over simplified it, or missing some point.
Would this be a good fit for an Observer? I'm not sure, but I'm hoping that exists outside of the transaction... I have a similar need where I might want to delete a record on update...
I've solved a problem like this by taking advantage of Ruby's variable scoping. Basically I declared an error variable outside of a transaction block then catch, store log message, and raise the error again.
It looks something like this:
def something
error = nil
ActiveRecord::Base.transaction do
begin
# place codez here
rescue ActiveRecord::Rollback => e
error = e.message
raise ActiveRecord::Rollback
end
end
MyModelLog.log_something(error) unless error.nil?
end
By declaring the error variable outside of the transaction scope the contents of the variable persist even after the transaction has exited.
I am not sure if it applies to you, but i assume you are trying to save/create a model from your controller. In the controller it is easy to check the outcome of that action, and you most likely already do to provide the user with a useful flash; so you could easily log an appropriate message there.
I am also assuming you do not use any explicit transactions, so if you handle it in the controller, it is outside of the transaction (every save and destroy work in their own transaction).
What do you think?
MyModelLog.log_something should be done using a different connection.
You can make MyModelLog model always use a different connection by using establish_connection.
class MyModelLog < ActiveRecord::Base
establish_connection Rails.env # Use different connection
def self.log_something
self.create(:log => "something happened")
end
end
Not sure if this is the right way to do logging!!
You could use a nested transaction. This way the code in your callback executes in a different transaction than the failing validation. The Rails documentations for ActiveRecord::Transactions::ClassMethods discusses how this is done.

Resources