Where is the right place to put _changed? methods in Rails? - ruby-on-rails

I have a user model that checks to see if a value has changed before_save (by running User.zipcode_changed?). The idea is that this will queue up a delayed job if it has.
Problem is, when I migrate the app from scratch I get an error:
An error has occurred, all later migrations canceled:
undefined method `postcode_changed?' for #<User:0x105db6158>
Therefore, where should I be putting these? Is the model the wrong place?

When you checkout a new project from scratch, you shouldn't use migrations to build the database.
You should use rake db:schema:load instead.
Let me show you why.
Let's assume you create a new Post model with a post table on migration 10.
On migration 11, you execute some special elaborations on the Post model.
After a while, you decide to drop the Post model and the post table because no longer required.
Six month later, you checkout the project from scratch. If you try to run rake db:migrate the migration 11 will fail complaining about missing model. It's true, the model has been removed many month before and it's no longer available.
Instead, if you run rake db:schema:load you'll initialize the database with the right schema version.
Talking about migrations, if you just created the postcode method and you are trying to use the _changed? magic method in the same migration, you need to reload the schema before.
class MigrationFile < ...
self.up
add_column :user, :postcode, :string
User.all.each { |user| puts user.postcode_changed? } # will fail
User.reset_column_information
User.all.each { |user| puts user.postcode_changed? } # now it works
end
...
end

You say both zipcode_changed? and postcode_changed? in your question. What's the actual column name in your database - zipcode or postcode? ActiveRecord will only create the _changed? convenience method for the actual column name.

What you are doing is reasonable model code, but not so good in a migration.
Using model code in your migrations is problematic because of problems like this. I recommend sticking with SQL-oriented code.

Related

Rails: How to modify data using migrations due to change in the schema

I have following two migrations:
One, Add column contextual_page_number to transcripts table:
class AddContextualPageNumberToTranscripts < ActiveRecord::Migration[5.2]
def change
add_column :transcripts, :contextual_page_number, :integer, default: 1
end
end
Second, changing the value of the previous added column contextual_page_number based on value of another column:
class ChangePageOffsetAndContextualPageNumberOfTranscripts < ActiveRecord::Migration[5.2]
def up
Firm.all.find_in_batches do |group|
group.each do |firm|
Apartment::Tenant.switch(firm.tenant) do
Transcript.where.not(page_offset: 0).each do |transcript|
transcript.update(
contextual_page_number: ((transcript.page_offset - 1) * -1),
page_offset: 1
)
end
end
end
end
end
def down
..
end
end
After running the migration, I am getting unknown attribute contextual_page_number error.
== 20211108132509 AddContextualPageNumberToTranscripts: migrating =============
-- add_column(:transcripts, :contextual_page_number, :integer, {:default=>1}) -> 0.0095s
== 20211108132509 AddContextualPageNumberToTranscripts: migrated (0.0096s) ====
== 20220113095658 ChangePageOffsetAndContextualPageNumberOfTranscripts: migrating rails
aborted! StandardError: An error has occurred, this and all later
migrations canceled:
unknown attribute 'contextual_page_number' for Transcript.
I have even tried reset_column_information, but no luck:
Apartment::Tenant.switch(firm.tenant) do
Transcript.connection.schema_cache.clear!
Transcript.reset_column_information
..
end
Any clue would be of great help, thanks.
As mentioned in one of the answer, I tried reset_column_information just right after the add_column, but that didn't worked. Finally, SQL to the rescue..
sql_cmd = "UPDATE transcripts
SET contextual_page_number = ((page_offset - 1) * -1),
page_offset = 1
WHERE page_offset != 0"
Transcript.connection.execute(sql_cmd)
You need two migration files.
First, try running the migration and check schema.rb for the table transcripts and verify that the newly added column contextual_page_number is being added or not.
Once you are sure that your new column is added, then again create a new migration like, eg: MigrateTransriptsCloningsData, and then add the desired changes in the up block, then execute db:migrate to update the required changes.
My choice would be To add a new rake task and executing it. like bundle exec rake migrate_transcripts_data:start instead of keeping that logic in the db/migrate/your_new_migration_file, choice is yours.
reset_column_information should be the correct way to resolve this sort of problem if you want to use models in a migration. This isn't without its problems though.
I suspect the issue is that you are calling it too late somehow. Put it first thing in the up method of the second migration or after the add_column in the first migration.
I may assume that the issue is in Apartment.tenant_names.
In the second migration, you are switching tenants by Apartment::Tenant.switch(firm.tenant), but I do not see similar in the first migrations. Probably tenant names are in DB, not in configs.
I am pretty sure that you may find samples of the appropriate add_column in your previous migrations.
Do not use structure migrations to modify data.
Use rake tasks, or data-migrate gem instead.
Also, do not use automatic data migrations, if you not ensure, that it working as expected on production server.
Always store data before modifications and write modification rollback code.

Add column in rails migration but column doesn't exist right away

I have an old migration that adds a column into a table, then immediate add some data in that column.
def change
add_column :table_name, :column_name, :string
TableName.create(
column_name: "some string"
)
end
Now, whenever I have to rerun rake db:migrate after dropping the database, I get this error
NoMethodError: undefined method `column_name=' for #< TableName:0x007fa483a18838>
This migration worked when it was initially created, why doesn't it work if I rerun this migration?
I understand this isn't a great practice, but my research indicates that this should still work.
I would not recommend to make data changes within migrations. It it usually anti-pattern. It is recommended to do data migration inside a rake task:
namespace :table_names do
task :create_first_table_name do
TableName.create(
column_name: "some string"
)
end
end
Read Data Migrations in Rails if you want to know more about data migrations
https://stackoverflow.com/a/46613312/1949363 is a solid response and typically a great pattern to follow.
Alternatively, if the data must be there for the migration to keep the app in a valid state, you could write raw SQL in the migration and insert data using
ActiveRecord::Base.connection.execute(...)

I gat a error due to the influence of migration that I wrote a long time ago

I want to add new column 'piece_id' and I added it to Bucket table.
class AddPieceIdToBuckets < ActiveRecord::Migration
def change
add_column :buckets, :piece_id, :string
end
end
class Bucket < ActiveRecord::Base
validates :piece_id, presence: true
end
Also I need to validate piece_id. But Adding validation gives me the following error. I tried run migration without validation and then it passed.
undefined method `piece_id'
Although I do not have piece_id in migration I wrote in old days, I think that it is the influence that writing validation in the model. And I created several objects for migration I wrote before. Should I edit it?
def change
Bucket.reset_column_information
Bucket.create(
name: 'test_name',
about: 'test',
)
end
I have been bitten by this many times, I no longer put any data changes, via models, in my migrations for this reason.
Since you can be confident that the old migration where you create a Bucket has already run you could remove that bit of code, or better still add piece_id to the attributes and give it a value so it passes when migrating from an empty database.
I now use rake db:seeds to create data, in db/seeds.rb I have:
require_relative "seeds/#{Rails.env}/all"
and a directory structure such as:
db/seeds/development/all.rb
db/seeds/production/all.rb
In development/all.rb I do require_relative '../production/all'.
I run rake db:seed as part of deployment, this works well, just have sure that the seeds are idempotent, so check the record does not exist already, or use active record methods like find_or_create_by.

Alter Schema in Rails 2

I need to add some columns to a table in my schema. Can someone tell me the best way to do this?
The following seems incomplete or wrong since the schema.rb file did not update to include the new column and all of the corresponding view files (edit,index,new,show) did not update to include the new column. Not to mention the bloat of all of those migration classes that get generated. Thanks
ruby script/generate migration RecordLabelToAlbums record_label:string
exists db/migrate
create db/migrate/20121130125859_record_label_to_albums.rb
Creates this:
class RecordLabelToAlbums < ActiveRecord::Migration
def self.up
end
def self.down
end
end
I then added this:
class RecordLabelToAlbums < ActiveRecord::Migration
def self.up
add_column :albums, :record_label, :text
end
def self.down
remove_column :albums, :record_label
end
end
The I ran:
rake db:migrate
Got This:
Mysql::Error: Table 'albums' already exists: CREATE TABLE albums (id int(11) DEFAULT NULL auto_increment PRIMARY KEY, created_at datetime, updated_at datetime)
The code you added is correct.
The error suggests that for some reason your system appears to think it has not yet run the original migration that created the albums table. The state of migrations (in Rails 2) is specified in a table in the database called schema_migrations -- if this gets confused then it will try to re-run migrations. I am not sure what might cause it to get confused, but I do recall this happened a couple times back in 2008 when I was using Rails 2.x.
The table is simple -- you can see what's in it from a SQL prompt -- just the names of migrations it thinks it has run, I think.
If you don't mind losing some data, you can try rake db:rollback or even rake db:reset to get back to the beginning. rake db:rollback STEP=2 will rollback the last 2 migrations.
If you need the data, correct the contents of the table by adding one or more new records referencing the migrations in app/db/migrations that may have been missed. The order is important, I think (the format changed a little in Rails 3, I don't recall how).
Any time you want to add or change the database schema, use rails to generate a migration, and then run rake db:migrate once it's ready to go.
And just asking: is there any way you can move to Rails 3. It's been out for years now, and Rails 4 is coming soon. You'll find yourself in a backwater of incompatibilities, deprecations, security and performance issues and so on if you don't take the hit and upgrade.

Rails model validators break earlier migrations

I have a sequence of migrations in a rails app which includes the following steps:
Create basic version of the 'user' model
Create an instance of this model - there needs to be at least one initial user in my system so that you can log in and start using it
Update the 'user' model to add a new field / column.
Now I'm using "validates_inclusion_of" on this new field/column. This worked fine on my initial development machine, which already had a database with these migrations applied. However, if I go to a fresh machine and run all the migrations, step 2 fails, because validates_inclusion_of fails, because the field from migration 3 hasn't been added to the model class yet.
As a workaround, I can comment out the "validates_..." line, run the migrations, and uncomment it, but that's not nice.
Better would be to re-order my migrations so the user creation (step 2) comes last, after all columns have been added.
I'm a rails newbie though, so I thought I'd ask what the preferred way to handle this situation is :)
The easiest way to avoid this issue is to use rake db:schema:load on the second machine, instead of db:migrate. rake db:schema:load uses schema.rb to load the most current version of your schema, as opposed to migrating it up form scratch.
If you run into this issue when deploying to a production machine (where preserving data is important), you'll probably have to consolidate your migrations into a single file without conflicts.
You can declare a class with the same name inside the migration, it will override your app/models one:
class YourMigration < ActiveRecord::Migration
class User < ActiveRecord::Base; end
def self.up
# User.create(:name => 'admin')
end
end
Unfortunately, your IDE may try to autocomplete based on this class (Netbeans does) and you can't use your model logic in there (except if you duplicate it).
I'm having to do this right now. Building upon BiHi's advice, I'm loading the model manually then redefining methods where I need to.
load(File.join(RAILS_ROOT,"app/models/user.rb"))
class User < ActiveRecord::Base
def before_validation; nil; end # clear out the breaking before_validation
def column1; "hello"; end # satisfy validates_inclusion_of :column1
end
In your migration, you can save your user skipping ActiveRecord validation:
class YourMigration < ActiveRecord::Migration
def up
user = User.new(name: 'admin')
user.save(validate: false)
end
end

Resources