Rails default to a defined value - ruby-on-rails

Hello I was wondering what the correct way to a defined default value as in
class SetDefault < ActiveRecord::Migration
def change
change_column :table_name, :column_name, :type, default: "Your value"
end
end
but instead of "Your Value" it was #yourvalue which is stored in a #settings model
class SetDefault < ActiveRecord::Migration
def change
change_column :table_name, :column_name, :type, default: #settings.yourvalue
end
end
class CreateSettings < ActiveRecord::Migration[5.2]
def change
create_table
t.string :yourvalue
t.timestamps
end
end
end
#yourvalue would be defined and updated by a user

Migrations are normal Ruby classes. You can write Rails code within them. Eg. you can access your models.
class SetDefault < ActiveRecord::Migration
def change
change_column :table_name, :column_name, :string, default: Setting.find(1).yourvalue
end
end
Replace Setting.find(1) with whatever logic you need to filter.

I would set it on the model so your migrations remain non-reliant on previous states of the data therein.
class TableName < ApplicationRecord
before_create :add_defaults
# whatever
private
def add_defaults
self.column_name = Setting.find(1).default_value unless column_name
end
end
This way you won't be invoking ActiveRecord from the migration, and your app will still work if you deploy it somewhere and run the migrations before adding a Setting record. You'll likely need a fully-migrated database before adding any data.

Related

Migrate from two string yes/no variable to a single boolean variable in rails?

The question is about rails database migration.
The current database contains two entries for a supposedly boolean variable as in the database scheme as follows:
create_table "table_name", force: :cascade do |t|
...
t.string "yes_boolvar"
t.string "no_boolvar"
...
end
I need to convert it to one single boolean variable as following:
t.boolean "boolvar"
I considered about renaming the 'yes_boolvar', changing its type from string to boolean, and then removing 'no_boolvar' column, based on some readings, like the following:
t.rename :yes_boolvar,
:boolvar
t.change :boolvar,
:boolean
t.remove :no_boolvar
However, this will only consider the truth value of 'yes_*' and not 'no_*' while copying the value of the variable. Is there a way to successfully migrate the var so that the truth (or nil) values of the both the vars are taken into account.
It depends on your app.
If no one can update those values (i.e. it is not a field in a user profile), then you can:
make a database dump
run your migration
execute a code that will fill boolvar
Another solution is to migrate data in 3 steps:
first migration renames column
second migration migrates data
third migration removes no_boolvar column
I guess it is possible to merge the first two actions into a single migration (but I prefer to keep them separated).
I recommends you to handle with 3 migrations.
For first, create a migration adding a boolean: :boolvar
class AddBoolvarToTableName < ActiveRecord::Migration
def up
add_column :table, :boolvar, :boolean
end
def down
remove_column :table, :boolvar
end
end
After, create a new migration to handle data:
class RepopulateBooleanValues < ActiveRecord::Migration[5.0]
def change
YourClass.all.each do |record|
# put the logic here like:
record.boolvar = record.yes_boolvar == 'true'
# or
record.boolvar = record.not_boolvar == 'false'
# I'am not sure whats the content of yes_boolvar and not_boolvar, elaborate the logic here
record.save
end
end
end
To finish it, just create a new migration removing yes_boolvar and no_boolvar.
This is roughly the migration I'd write (I didn't run the code, but it should work):
# This ensures the migration to work
# regardless the customizations on your original model
class TempModel < ActiveRecord::Base
self.table_name = 'table_name'
end
class MyMigration < ActiveRecord::Migration[5.0]
def up
add_column :table_name, :boolvar, :boolean
TempModel.reset_column_information
TempModel.find_each do |record|
# Decide some logic here about how to migrate values from yes_boolvar
# and no_boolvar columns to boolvar column
boolvar_value = record.yes_boolvar || !record.no_boolvar
record.update_column :boolvar, boolvar_value
end
remove_column :table_name, :yes_boolvar
remove_column :table_name, :no_boolvar
end
def down
add_column :table_name, :yes_boolvar, :string
add_column :table_name, :no_boolvar, :string
TempModel.reset_column_information
TempModel.find_each do |record|
# Decide some logic here about how to handle yes_boolvar
# and no_boolvar values
record.update_columns yes_boolvar: record.boolvar,
no_boolvar: !record.boolvar
end
remove_column :table_name, :boolvar
end
end

Rails migration to change column type from text to json (Postgresql)

I've been trying unsuccessfully to change a column type in my Postgres database from text to json. Here's what I've tried...
class ChangeNotesTypeInPlaces < ActiveRecord::Migration[5.0]
def up
execute 'ALTER TABLE places ALTER COLUMN notes TYPE json USING (notes::json)'
end
def down
execute 'ALTER TABLE places ALTER COLUMN notes TYPE text USING (notes::text)'
end
end
Also...
class ChangeNotesTypeInPlaces < ActiveRecord::Migration[5.0]
def up
change_column :places, :notes, 'json USING CAST(notes AS json)'
end
def down
change_column :places, :notes, 'text USING CAST(notes AS text)'
end
end
Both of these return the same error...
PG::InvalidTextRepresentation: ERROR: invalid input syntax for type json
Using Rails 5.1.x and PostgreSQL 9.4, here is what worked for me when converting text columns (containing valid json) to jsonb columns :
class ChangeTextColumnsToJson < ActiveRecord::Migration[5.1]
def change
change_column :table_name, :column_name, :jsonb, using: 'column_name::text::jsonb'
end
end
I was able to accomplish it using:
def change
change_column :places, :notes, :json, using: 'notes::JSON'
end
This won't be reversible though; I imagine you can split it out into separate up and down definitions, but I didn't feel the need to.
I just solved a similar problem. Trying to adapt it for your question, it would look (mostly?) like this.
class ChangeNotesTypeInPlaces < ActiveRecord::Migration[5.0]
def up
change_column :places, :notes, :jsonb, using: 'CAST(value AS JSON)'
end
def down
change_column :places, :notes, :text
end
end

making the default value for a migration based upon a calculation using two other columns

I'm adding a column to a model and I want the new column's default to be the sum of two of the existing columns in that table. Not sure if this is possible. Here's the (invalid) migration I tried:
class AddRoundsWithSpeaksToCases < ActiveRecord::Migration
def change
add_column :cases, :rounds_with_speaks, :integer, default: :wins + :losses
end
end
Is there a way to do this via a migration, or do I have to run rails console and update the attributes through there?
You wouldn't want to set the migration up that way because :wins + :losses is irrelevant for a new Case record. The default should be 0, since by "default" the value of the field will be 0 for new records.
Additionally, you don't have to rely on the console to run the update, you can put it in the migration:
class AddRoundsWithSpeaksToCases < ActiveRecord::Migration
def up
add_column :cases, :rounds_with_speaks, :integer, default: 0
Case.find_each(batch_size: 200) { |c| c.update_attribute!(:rounds_with_speaks, c.wins + c.losses) }
end
def down
remove_column :cases, :rounds_with_speaks
end
end
As a one time migration, to the historical records, you can do it like this:
class AddRoundsWithSpeaksToCases < ActiveRecord::Migration
def change
add_column :cases, :rounds_with_speaks, :integer
update "UPDATE cases SET rounds_with_speaks = wins+losses"
end
end
This won't affect any future records, you could write something like these as a SQL trigger/procedure..

Rails Globalize3 gem: How do I add an additional field to the translation table using a migration?

The docs for the Globalize3 gem are clear about how to create a translation table, but I don't see any information about how to add a field to a translation table during a later migration. For example, I initially included Category.create_translation_table! :name => :string when I created my Category model. Now, however, I need to add a translated field to the model.
How do I do that with a Rails migration? I don't see any docs for an alter_translation_table! method or anything similar...
You can do it by hand, something like the following:
class AddNewFieldToYourTable < ActiveRecord::Migration
def self.up
change_table(:your_tables) do |t|
t.string :new_field
end
change_table(:your_table_translations) do |t|
t.string :new_field
end
end
def self.down
remove_column :your_tables, :new_field
remove_column :your_table_translations, :new_field
end
end
With Globalize4, just :
class AddHintToCategory < ActiveRecord::Migration
def up
Category.add_translation_fields! hint: :text
end
def down
remove_column :category_translations, :hint
end
end
Don't forget to add the new field in your model :
translate :name, :hint
https://github.com/globalize/globalize/blob/master/lib/globalize/active_record/migration.rb:34 line (globalize 4)
add_translation_fields!(fields, options)
P.S. Just a typo in a previous comment, 'add_transaction_fields' isn't defined.

How to write conditional migrations in rails?

I am looking for ways to write migrations in rails that can be executed against the database many times without failing.
For instance let say I have this migration:
class AddUrlToProfile < ActiveRecord::Migration
def self.up
add_column :profile, :url, :string
end
def self.down
remove_column :profile, :url
end
end
If the url column already exists in the Profile table (if the schema.rb has been modified unexpectedly for instance), my migration will fail saying that it's a duplicate!
So how to execute this migration only if it has to?
Thanks
You can do something like this:
class AddUrlToProfile < ActiveRecord::Migration
def self.up
Profile.reset_column_information
add_column(:profile, :url, :string) unless Profile.column_names.include?('url')
end
def self.down
Profile.reset_column_information
remove_column(:profile, :url) if Profile.column_names.include?('url')
end
end
This will reset the column information before it begins - making sure that the Profile model has the up-to-date column information from the actual table. It will then only add the column if it doesn't exist. The same thing happens for the down function, but it only removes the column if it exists.
If you have multiple use cases for this you could factor the code out into a function and re-use that in your migrations.
For Rails 3.X, there's the column_exists?(:table_name, :column_name) method.
For Rails 2.X, you can check the existence of columns with the following:
columns("<table name>").index {|col| col.name == "<column name>"}
...or if you're not in a migration file:
ActiveRecord::Base.connection.columns("<table name>").index {|col| col.name == "<column name>"}
If it returns nil, no such column exists. If it returns a Fixnum, then the column does exist. Naturally, you can put more selective parameters between the {...} if you want to identify a column by more than just its name, for example:
{ |col| col.name == "foo" and col.sql_type == "tinyint(1)" and col.primary == nil }
This should work
def self.table_exists?(name)
ActiveRecord::Base.connection.tables.include?(name)
end
if table_exists?(:profile) && !Profile.column_names.include?("url")
add_column :profile, :url, :string
end
Wrapping my migration in a conditional worked for me.
Rails 4.X
class AddUrlToProfile < ActiveRecord::Migration
unless Profile.column_names.include?("url")
def self.up
add_column :profile, :url, :string
end
def self.down
remove_column :profile, :url
end
end
end

Resources