Rails Migration Column Added But Not Populated - ruby-on-rails

I'm trying to write a very simple Rails migration that adds a new column to the table and populates it with a combination of data from other columns. However, the column gets added but the column value is nil for every record. What am I doing wrong?
class AddNameToPermissions < ActiveRecord::Migration
def change
add_column :auth_permissions, :name, :string
Auth::Permission.reset_column_information
Auth::Permission.all.each do |permission|
target_name = permission.target_symbol || permission.target_class
permission.name = permission.action << ", " << target_name
permission.save
end
end
end

Calling permission.save will execute all the callbacks including validators and maybe a validator is preventing the record to be saved. To skip validators you can use update_column instead of save
permission.update_column(:name, permission.action << ", " << target_name)
Also I would recommend you a couple of tips:
Use Auth::Permission.find_each instead of Auth::Permission.all.each (this loads all the records at once in memory)
Try to update the permission in a rake task and use only the migration to modify the table (it is the recommended good practice way)

Unless you are using the default option in a migration, it is usually advisable to do that in a custom rake task. Content Migrations are a bit of an anti pattern. The main Logic you want there is anything that has to deal with the schema of your database. Run your migration with only the add_column line, and then write a rake task to transfer the values from one table to the other.

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.

Can you create a database backed rails model without a table?

I have a complicated SQL query that aggregates columns using a group_by.
IndividualOrder.select(SUM(...) as amount, other_fields).group_by("organization.id")
The problem is: I get an ActiveRecord::Relation from IndividualOrder, which isn't really what the result is conceptually anymore. I'm not entirely sure how to cast it to a new method. If I use arel to handle it, I still usually would have to go IndividualOrder.arel_table, and it would still cast to whatever I select.
I just want to take the fields [:amount, :organization, :other] and be able to interact with them as I would a database backed model that had those values as a table.
So, it's not quite a tableless model (which usually aren't database backed), and it's not an actual model because it's a generated query.
Is this the use case for scenic? I'm stuck with having to navigate around 2 variables that exist within the query that I'm doing via ActiveRecord.
Hi there hope this answer helps you, this should be a comment not a answer( not enough karma for doing that yet)
what you can do is to create a view in your database,
rails g migration my_new_view
ActiveRecord does not give us any methods to create views so you will have to do "manually" or you can use specific gems for doing those.
class MyNewView < ActiveRecord::Migration[5.2]
def change
reversible do |dir|
dir.up do
execute <<-SQL
CREATE OR REPLACE VIEW public.my_new_view AS
# your sql query here SELECT ....
SQL
end
dir.down do
execute <<-SQL
DROP VIEW IF EXISTS public.my_new_view;
SQL
end
end
end
end
now you can refer in your model.
Beware view tables will not show in schema by default, for seing them you have to put in your application.rb:
config.active_record.schema_format = :sql
cheers.

How to add a new column to every model?

with what migration can I add a column to every model at once? This would be handy almost every time you forgot to add a field anyway because most of the time a field you want to add is not limited to one model only. Like "email" In this case I forgot to add an "order" field.
Thanks
You can do as follows -
def change
tables = [:users, :products, :addresses]
tables.each do |table_name|
add_column table_name, :order, :integer
end
end
It's called a group migration
Answering your first question "With what migration can I add a column to every model at once?". Answer: None. Rails migrations are a way to alter database schemas over time in a consistent way.
Rails migrations are Ruby classes using Rails methods as instructions to modify your database as needed. So your question could be better formulated as "How can I create a migration to add a column to every model at once?"
IMHO I don't think there's going to be a specific method to do this, as the requeriment is pretty custom, but, depending in your Rails version you can get all the ApplicationRecord.descendants:
Zeitwerk::Loader.eager_load_all
ApplicationRecord.descendants.map { |table| table.name.downcase.pluralize }.each do |table|
add_column table, :logdate, :datetime
end
Or all those tables from the database that can be safe_constantized:
ActiveRecord::Base.connection.tables.map do |table|
table.classify.safe_constantize
end.reject(&:nil?).each do |table|
add_column table, :logdate, :datetime
end
That way you get the name of each table/model and use it as the first argument for add_column.
The difference is that in Rails 6 the default code loader is Zeitwerk, so you can eager load all the project dependencies. In other versions you could do the same but using Rails.application.eager_load!.
The second version would work without having to load the models as dependencies as it makes a query asking for their tables and then maps them as constants.

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(...)

Can Rails Migrations be used to convert data?

I'm trying to convert a column in my Rails app, for arguments sake let's pretend I'm trying to change the age column in my users table to a string representation rather than an int.
In my migration I have this;
def.self up
add_column :users, :age_text, :string
users = User.find(:all)
users.each do |u|
u.age_text = convert_to_text(u.age)
u.save
end
end
def self.convert_to_text(number)
#code here to convert 1 to 'one' etc
end
But it doesn't seem to be working, is what I'm attempting here even possible with migrations?
What you're trying to do is possible, and I would say the correct thing to do.
You need, though, to reload the column info for the model classes you're updating in the migration, so that Rails knows about the new columns. Try this:
def.self up
add_column :users, :age_text, :string
User.reset_column_information
users = User.find(:all)
users.each do |u|
u.age_text = convert_to_text(u.age)
u.save
end
end
On a separate note, please note that if your table is large, doing updates one by one will take a looong time.. Be careful with that.
Since I'm new here I can't comment on the above so I'll add my own answer.
GENERALLY manipulating data in migrations is a BAD idea. Migrations with direct model access can get stuck if the model logic changes.
Imagine in your second migration you've added a new column. You want to seed that column with new data.
Let's also say a few weeks later you add a new validation to the model - a validation that operates on a field that does not yet exist in your second migration. if you ever were to construct the database from migration 0, you'd have some problems.
I strongly suggest using migrations to alter columns and other means to manage database data, especially when moving to production.
Here is an example migration I ran to convert data. You can easily convert it to use integers instead of strings. Making the conversion in SQL is much faster than loading each row in Rails.
class ConvertCommentTextToText < ActiveRecord::Migration
def up
add_column :comments, :text_tmp, :text
# copy text column
execute <<-SQL
update comments set text_tmp = text
SQL
remove_column :comments, :text
rename_column :comments, :text_tmp, :text
end
def down
add_column :comments, :text_tmp, :string
# copy text column
execute <<-SQL
update comments set text_tmp = text
SQL
remove_column :comments, :text
rename_column :comments, :text_tmp, :text
end
end
And to test it:
rake db:migrate
rake db:rollback
rake db:migrate
I would say that if you can "undo" the imported data when rolling back the migration version, then it's appropriate to put imports into the migration.
For example, I have a migration which sets up a lot of lookup tables and other meta-data. The data for these tables are populated during this phase. As the data for these lookup tables changes, I create new YAML files storing the meta-data and load those files in subsequent migrations (and un-do those YAMLS, re-loading the previous YAML file when backing out of a migration version). This is pretty clean. I have files (in different well-defined folders in my case) with these files:
002_setup_meta_data.rb
002_meta_data.yaml
007_change_meta_data.rb
007_meta_data.yaml
If you're importing "production" data from another system into transactional (non-static) tables, then I would say using migrations is not appropriate. Then I would follow Brian Hogan's advice of using rake tasks.

Resources