Ruby Workflow Issue During Migration - ruby-on-rails

I am using Ruby Workflow in my ActiveRecords using Gem: Workflow
Existing Running Code contains:
I am having an ActiveRecord: X
I am having two Migrations already:
(Ref1) CreateX migration (which creates table X)
(Ref2) CreateInitialEntryInX migration (which creates one entry in table X)
New Changes:
Now I wanted to add workflow in ActiveRecord X, hence I did:
(Ref3) I added the workflow code in ActiveRecord Model X (mentioning :status as my workflow field)
(Ref4) AddStatusFieldToX migration (which adds :status field in table X)
Now when I run rake db:migrate after the changes, the (Ref2) breaks cos Migration looks for :status field as it is mentioned in ActiveRecord Model in the Workflow section, but :status field has not been added yet as migration (Ref4) has not executed yet.
Hence, all the builds fail when all migrations are run in sequence, Any solution to this?
I do not want to resequence any of the migration or edit any old existing migrations.
My Model looks like:
class BaseModel < ActiveRecord::Base
#
# Workflow to define states of Role
#
# Initial State => Active
#
# # State Diagram::
# Active --soft_delete--> Deleted
# Deleted
#
# * activate, soft_delete are the event which triggers the state transition
#
include Workflow
workflow_column :status
workflow do
state :active, X_MODEL_STATES::ACTIVE do
event :soft_delete, transitions_to: :deleted
end
state :deleted, X_MODEL_STATES::DELETED
on_transition do |from, to, event, *event_args|
self.update_attribute(:status, to)
end
end
def trigger_event(event)
begin
case event.to_i
when X_MODEL_EVENTS::ACTIVATE
self.activate!
when X_MODEL_EVENTS::SOFT_DELETE
self.soft_delete!
end
rescue ....
end
end
class X_MODEL_STATES
ACTIVE = 1
DELETED = 2
end
class X_MODEL_EVENTS
ACTIVATE = 1
SOFT_DELETE = 2
end
# Migrations(posting Up functions only - in correct sequence)
#--------------------------------------------------
#1st: Migration - This is already existing migration
CreateX < ActiveRecord::Migration
def up
create_table :xs do |t|
t.string :name
t.timestamps null: false
end
end
end
#2nd: Migration - This is already existing migration
CreateInitialX < ActiveRecord::Migration
def up
X.create({:name => 'Kartik'})
end
end
#3rd: Migration - This is a new migration
AddStatusToX < ActiveRecord::Migration
def up
add_column :xs, :status, :integer
x.all.each do |x_instance|
x.status = X_MODEL_STATES::ACTIVE
x.save!
end
end
end
So, when Migration#2 runs, it tries to find :status field to write is with initial value of X_MODEL_STATES::ACTIVE as it is mentioned in Active Record Model files workflow as: workflow_column :status and does not find the field as Migration#3 is yet to execute.

You can wrap up your workflow code by a check for column_name.
if self.attribute_names.include?('status')
include Workflow
workflow_column :status
workflow do
...
end
end
This will result in running workflow code only after 'AddStatusToTable' migration ran successfully.

This is a pain, as your models need to be consistent for migrations. I don't know any auto solutions for this.
Theoreticaly the best way would be to have model code versions binded with migrations. But I don't know any system that allows this.
Every time I do big models refactoring i experience this problem. Possible solutions to this situation.
Run migrations on production manually to assure consitent state between migrations and models
Temporarily comment out workflow code in model to run blocking migration, then deploy, then uncomment workflow code and move on with deploy and next migrations
Version migrations and model changes on branches, so they are consistent. Deploy to production and run migration by chunks
Include temp workarounds in the model code, and remove them from source after migrations on production are deployed.
Monkey-patch model in migration code for backwards compatibility. In your situation this would be to dynamically remove 'workflow' from model code, which might be hard, and then run migration
All solutions are some kind of dirty hacks, but it's not easy to have migrations and model code versioned. The best way would be to deploy in chunks or if need to deploy all at once use some temp code in model and remove it after deploy on production.

THANKS ALL
I found the solution to this, and i am posting it here. The problems to issue here were:
Add :status field to ActiveRecord Model X without commenting out Workflow Code and not let Workflow disallow creation of an instance in Table X during migration.
Secondly, add an if condition to it as specified by #007sumit.
Thirdly, to be able to reload Model in migration with the updated column_information of Model X
Writing whole code solution here:
class BaseModel < ActiveRecord::Base
#
# Workflow to define states of Role
#
# Initial State => Active
#
# # State Diagram::
# Active --soft_delete--> Deleted
# Deleted
#
# * activate, soft_delete are the event which triggers the state transition
#
# if condition to add workflow only if :status field is added
if self.attribute_names.include?('status')
include Workflow
workflow_column :status
workflow do
state :active, X_MODEL_STATES::ACTIVE do
event :soft_delete, transitions_to: :deleted
end
state :deleted, X_MODEL_STATES::DELETED
end
end
def trigger_event(event)
...
end
end
class X_MODEL_STATES
...
end
class X_MODEL_EVENTS
...
end
# Migrations(posting Up functions only - in correct sequence)
#--------------------------------------------------
#1st: Migration - This is already existing migration
CreateX < ActiveRecord::Migration
def up
create_table :xs do |t|
t.string :name
t.timestamps null: false
end
end
end
#2nd: Migration - This is already existing migration
CreateInitialX < ActiveRecord::Migration
def up
X.create({:name => 'Kartik'})
end
end
#3rd: Migration - This is a new migration to add status field and
# modify status value in existing entries in X Model
AddStatusToX < ActiveRecord::Migration
def up
add_column :xs, :status, :integer
# This resets Model Class before executing this migration and
# Workflow is identified from the if condition specified which was
# being skipped previously without this line as Model Class is
# loaded only once before all migrations run.
# Thanks to post: http://stackoverflow.com/questions/200813/how-do-i-force-activerecord-to-reload-a-class
x.reset_column_information
x.all.each do |x_instance|
x.status = X_MODEL_STATES::ACTIVE
x.save!
end
end
end
#stan-brajewski Now, this code can go in one deployment.
Thanks all for inputs :)

Related

How to make sure Rails points to updated model name when running a migration?

I’m running into an issue where one of my migration is failing. This is how the existing migration looks like.
class AddColumnToTwitterPosts < ActiveRecord::Migration[6.0]
def up
add_column :twitter_posts, :status, :string, default: "new"
add_index :twitter_posts, :status
add_default_status_to_existing_posts
end
def down
remove_column :twitter_posts, :status
end
private
def add_default_status_to_existing_posts
TwitterPost.find_each do |post|
post.update!(status: "new")
end
end
end
Now I have moved model TwitterPost to namespace Twitter::Post.
So whenever this migration runs it fails to find this model. How can I make sure that rails picks up on the updated namespace instead of the old model name specified in the migration?
Actually you should not put any "data migrations" inside of your "schema migrations". This is considered bad practice, exactly for the reason which causes a problem for you now. One way to solve your problem is to use Rake tasks, as suggested by Thoughtbot. If you need to update data after a specific migration, you create a rake task doing the updates you need.
This way, your database schema migrations will always work since they don't depend on the presence of any specific model defined in your app.
A rake task might in your case look like:
namespace :twitter_posts do
desc "Update twitter posts"
task update_status_new: :environment do
puts "Going to update #{TwitterPost.count} twitter_posts"
ActiveRecord::Base.transaction do
TwitterPost.update_all(status: "new")
end
puts " All done now!"
end
end

add a database column with Rails migration and populate it based on another column

I'm writing a migration to add a column to a table. The value of the column is dependent on the value of two more existing columns. What is the best/fastest way to do this?
Currently I have this but not sure if it's the best way since the groups table is can be very large.
class AddColorToGroup < ActiveRecord::Migration
def self.up
add_column :groups, :color, :string
Groups = Group.all.each do |g|
c = "red" if g.is_active && is_live
c = "green" if g.is_active
c = "orange"
g.update_attribute(:type, c)
end
end
def self.down
end
end
It's generally a bad idea to reference your models from your migrations like this. The problem is that the migrations run in order and change the database state as they go, but your models are not versioned at all. There's no guarantee that the model as it existed when the migration was written will still be compatible with the migration code in the future.
For example, if you change the behavior of the is_active or is_live attributes in the future, then this migration might break. This older migration is going to run first, against the new model code, and may fail. In your basic example here, it might not crop up, but this has burned me in deployment before when fields were added and validations couldn't run (I know your code is skipping validations, but in general this is a concern).
My favorite solution to this is to do all migrations of this sort using plain SQL. It looks like you've already considered that, so I'm going to assume you already know what to do there.
Another option, if you have some hairy business logic or just want the code to look more Railsy, is to include a basic version of the model as it exists when the migration is written in the migration file itself. For example, you could put this class in the migration file:
class Group < ActiveRecord::Base
end
In your case, that alone is probably sufficient to guarantee that the model will not break. Assuming active and live are boolean fields in the table at this time (and thus would be whenever this migration was run in the future), you won't need any more code at all. If you had more complex business logic, you could include it in this migration-specific version of model.
You might even consider copying whole methods from your model into the migration version. If you do that, bear in mind that you shouldn't reference any external models or libraries in your app from there, either, if there's any chance that they will change in the future. This includes gems and even possibly some core Ruby/Rails classes, because API-breaking changes in gems are very common (I'm looking at you, Rails 3.0, 3.1, and 3.2!).
I would highly suggest doing three total queries instead. Always leverage the database vs. looping over a bunch of items in an array. I would think something like this could work.
For the purposes of writing this, I'll assume is_active checks a field active where 1 is active. I'll assume live is the same as well.
Rails 3 approach
class AddColorToGroup < ActiveRecord::Migration
def self.up
add_column :groups, :color, :string
Group.where(active: 1, live: 1).update_all(type: "red")
Group.where(active: 1, live: 0).update_all(type: "green")
Group.where(active: 0, live: 0).update_all(type: "orange")
end
end
Feel free to review the documentation of update_all here.
Rails 2.x approach
class AddColorToGroup < ActiveRecord::Migration
def self.up
add_column :groups, :color, :string
Group.update_all("type = red", "active = 1 AND live = 1")
Group.update_all("type = red", "active = 1 AND live = 0")
Group.update_all("type = red", "active = 0 AND live = 0")
end
end
Rails 2 documentation
I would do this in a
after_create
# or
after_save
in your ActiveRecord model:
class Group < ActiveRecord::Base
attr_accessor :color
after_create :add_color
private
def add_color
self.color = #the color (wherever you get it from)
end
end
or in the migration you'd probably have to do some SQL like this:
execute('update groups set color = <another column>')
Here is an example in the Rails guides:
http://guides.rubyonrails.org/migrations.html#using-the-up-down-methods
In a similar situation I ended up adding the column using add_column and then using direct SQL to update the value of the column. I used direct SQL and not the model per Jim Stewart's answer, since then it doesn't depend on the current state of the model vs. the current state of the table based on migrations being run.
class AddColorToGroup < ActiveRecord::Migration
def up
add_column :groups, :color, :string
execute "update groups set color = case when is_active and is_live then 'red' when is_active then 'green' else 'orange' end"
end
def down
remove_column :groups, :color
end
end

Specs for ActiveRecord::Migration helper method

I have added monetize and demonetize helpers inside ActiveRecord::Migration, ActiveRecord::ConnectionAdapters::TableDefinition and ActiveRecord::ConnectionAdapters::Table by that pull request.
That file shows usage examples. So you will understand invented changes at glance. (It works)
But I have no idea how to test my helpers. What way can I write specs for them? All my attempts of writing migrations in spec files and running them manually failed. Migration manual run did not change table (or I was unable to detect changes) and did not throw any exception.
Example of my attempt:
describe 'monetize' do
class MonetizeMigration < ActiveRecord::Migration
def change
create_table :items
monetize :items, :price
end
end
class Item < ActiveRecord::Base; end
it 'should monetize items' do
MonetizeMigration.up #=> nil
Item #=> Item(has no table)
end
end
This worked for me in the console:
[4667]foo#bar:~/dev/ror/foo$ rails c
Loading development environment (Rails 3.2.9)
irb(main):001:0> class MyMigration def change
irb(main):003:2> create_table :foo
irb(main):004:2> end
irb(main):005:1> end
=> nil
irb(main):006:0> MyMigration.new.change
-- create_table(:foo)
(4.5ms) select sqlite_version(*)
(133.2ms) CREATE TABLE "foo" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)
-> 0.2362s
=> []
You can execute any migration helper methods right on your database connection:
ActiveRecord::Base.connection.create_table :items
Thanks to #happy_user for showing my mistake in first attempt. I think someone may use my latest solution in the future, so I'll leave it here.

Migrating DATA - not just schema, Rails

Sometimes, data migrations are required. As time passes, code changes and migrations using your domain model are no longer valid and migrations fail. What are the best practices for migrating data?
I tried make an example to clarify the problem:
Consider this. You have a migration
class ChangeFromPartnerAppliedToAppliedAt < ActiveRecord::Migration
def up
User.all.each do |user|
user.applied_at = user.partner_application_at
user.save
end
end
this runs perfectly fine, of course. Later, you need a schema change
class AddAcceptanceConfirmedAt < ActiveRecord::Migration
def change
add_column :users, :acceptance_confirmed_at, :datetime
end
end
class User < ActiveRecord::Base
before_save :do_something_with_acceptance_confirmed_at
end
For you, no problem. It runs perfectly. But if your coworker pulls both these today, not having run the first migration yet, he'll get this error on running the first migration:
rake aborted!
An error has occurred, this and all later migrations canceled:
undefined method `acceptance_confirmed_at=' for #<User:0x007f85902346d8>
That's not being a team player, he'll be fixing the bug you introduced. What should you have done?
This is a perfect example of the Using Models in Your Migrations
class ChangeFromPartnerAppliedToAppliedAt < ActiveRecord::Migration
class User < ActiveRecord::Base
end
def up
User.all.each do |user|
user.applied_at = user.partner_application_at
user.save
end
end
Edited after Mischa's comment
class ChangeFromPartnerAppliedToAppliedAt < ActiveRecord::Migration
class User < ActiveRecord::Base
end
def up
User.update_all('applied_at = partner_application_at')
end
end
Best practice is: don't use models in migrations. Migrations change the way AR maps, so do not use them at all. Do it all with SQL. This way it will always work.
This:
User.all.each do |user|
user.applied_at = user.partner_application_at
user.save
end
I would do like this
update "UPDATE users SET applied_at=partner_application_at"
Some times 'migrating data' could not be performed as a part of schema migration, like discussed above. Sometimes 'migrating data' means 'fix historical data inconstancies' or 'update your Solr/Elasticsearch' index, so its a complex task. For these kind of tasks, check out this gem https://github.com/OffgridElectric/rails-data-migrations
This gem was designed to decouple Rails schema migrations from data migrations, so it wont cause downtimes at deploy time and make it easy to manage in overall

Add Rows on Migrations

I'd like to know which is the preferred way to add records to a database table in a Rails Migration. I've read on Ola Bini's book (Jruby on Rails) that he does something like this:
class CreateProductCategories < ActiveRecord::Migration
#defines the AR class
class ProductType < ActiveRecord::Base; end
def self.up
#CREATE THE TABLES...
load_data
end
def self.load_data
#Use AR object to create default data
ProductType.create(:name => "type")
end
end
This is nice and clean but for some reason, doesn't work on the lasts versions of rails...
The question is, how do you populate the database with default data (like users or something)?
Thanks!
The Rails API documentation for migrations shows a simpler way to achieve this.
http://api.rubyonrails.org/classes/ActiveRecord/Migration.html
class CreateProductCategories < ActiveRecord::Migration
def self.up
create_table "product_categories" do |t|
t.string name
# etc.
end
# Now populate the category list with default data
ProductCategory.create :name => 'Books', ...
ProductCategory.create :name => 'Games', ... # Etc.
# The "down" method takes care of the data because it
# drops the whole table.
end
def self.down
drop_table "product_categories"
end
end
Tested on Rails 2.3.0, but this should work for many earlier versions too.
You could use fixtures for that. It means having a yaml file somewhere with the data you want to insert.
Here is a changeset I committed for this in one of my app:
db/migrate/004_load_profiles.rb
require 'active_record/fixtures'
class LoadProfiles < ActiveRecord::Migration
def self.up
down()
directory = File.join(File.dirname(__FILE__), "init_data")
Fixtures.create_fixtures(directory, "profiles")
end
def self.down
Profile.delete_all
end
end
db/migrate/init_data/profiles.yaml
admin:
name: Admin
value: 1
normal:
name: Normal user
value: 2
You could also define in your seeds.rb file, for instance:
Grid.create :ref_code => 'one' , :name => 'Grade Única'
and after run:
rake db:seed
your migrations have access to all your models, so you shouldn't be creating a class inside the migration.
I am using the latest rails, and I can confirm that the example you posted definitely OUGHT to work.
However, migrations are a special beast. As long as you are clear, I don't see anything wrong with an ActiveRecord::Base.connection.execute("INSERT INTO product_types (name) VALUES ('type1'), ('type2')").
The advantage to this is, you can easily generate it by using some kind of GUI or web front-end to populate your starting data, and then doing a mysqldump -uroot database_name.product_types.
Whatever makes things easiest for the kind of person who's going to be executing your migrations and maintaining the product.
You should really not use
ProductType.create
in your migrations.
I have done similar but in the long run they are not guaranteed to work.
When you run the migration the model class you are using is the one at the time you run the migration, not the one at the time you created the migration. You will have to be sure you never change your model in such a way to stop you migration from running.
You are much better off running SQL for example:
[{name: 'Type', ..}, .. ].each do |type|
execute("INSERT INTO product_types (name) VALUES ('#{type[:name]} .. )
end

Resources