Can NOT insert data into database in migration? - ruby-on-rails

I am using Rails 3. I don't know if it is the rule of rails that inside migration, it seems I can not insert data into database table. If someone can confirm it is so.
I tried the following things:
I have two ActiveRecord model:
class Car < ActiveRecord::Base
has_many :users
...
end
class User < ActiveRecord::Base
belongs_to :car
...
end
I have generate a migration file, inside the migration I have:
def self.up
default_car = Car.new({:name => 'default_car'})
default_car.save() #I got 'false' here
User.create!(:car_id => default_car.id}) #I got default_car.id is null value
end
def self.down
default_car = Car.find({:name => 'default_car'})
default_user = User.find({:car_id=>default_car.id})
default_car.delete
default_user.delete
end
I got false when I trying to save the default_car to database, and my default_user have null car_id.
Is it because in migration, it is NOT allowed to store data into database??

You can create data in migrations, however it is probably best not to, use seeds.rb instead.
Think the above will be failing because your car is not saving, I'm guessing you have some validation in your Car model.

Ok, we've figgured out that there was some validation issues. So you would like to now, that you can skip validations:
default_car = Car.new({:name => 'default_car'})
default_car.save(false)
#=> true

Related

value not setting with '=' in rails

I have a model which looks something like this:
class At::ModalityCombo < Base
self.table_name = 'at_modalites_at_combos'
belongs_to :at_modality, :class_name => 'At::Modality', foreign_key: :modality_id
belongs_to :at_combo, :class_name => 'At::Combo', foreign_key: :combo_id
attr_reader :modality_day
attr_writer :modality_day
end
Migration for modality_day column is like:
class AddDayInModalityCombo < ActiveRecord::Migration[5.2]
def up
add_column :at_modalites_at_combos, :modality_day, :integer, default: 0
end
def down
remove_column :at_modalites_at_combos, :modality_day
end
end
On rails console,
abc = At::ModalityCombo.new
abc.modality_day = 4
abc
Output:
modality_id: nil, combo_id: nil, modality_day: 0
Why modality_day is still 0?
I think you are confusing Rails here.
You have a column named modality_day on an Active Record model. This lets you read/write that property on instances of At::ModalityCombo.
You also have attr_reader and attr_writer setup for modality_day.
It looks like attr_writer/attr_reader is overriding the methods that would normally let you manage the property defined in your database. Deleting those should fix this and make it work like you expect.
attr_reader :modality_day is basically equivalent to:
def modality_day
#modality_day
end
And attr_writer :modality_day is basically equivalent to:
def modality_day=(value)
#modality_day = value
end
These methods manage an instance variable, but Active Record does not store your database data in this same way.
attr_reader, attr_writer and attr_accessor should not be used in Rails models at all (at least not in Rails 5.0+). They are Rubys built in metaprogramming methods for creating setters and getters.
ActiveRecord actually reads the schema straight from the db and creates special setters and getters corresponding to the columns in the db table. These setters are light years from the basic attr_writer setter - they do type casting, dirty tracking etc.
If you use attr_accessor you overwrite the setter and your model will stop persisting the attribute among other things.
For "virtual attributes" that are not saved to the database use the attributes api which does default values and typecasting.

Prevent duplicate has_many records in Rails 5

Given the following models:
class Client < ApplicationRecord
has_many :preferences
validates_associated :preferences
accepts_nested_attributes_for :preferences
end
class Preference < ApplicationRecord
belongs_to :client
validates_uniqueness_of :block, scope: [:day, :client_id]
end
I'm still able to create preferences with duplicate days* when creating a batch of preferences during client creation. This is (seemingly) because the client_id foreign key isn't available when the validates_uniqueness_of validation is run. (*I have an index in place which prevents the duplicate from being saved, but I'd like to catch the error, and return a user friendly error message, before it hits the database.)
Is there any way to prevent this from happening via ActiveRecord validations?
EDIT: This appears to be a known issue.
There's not a super clean way to do this with AR validations when you're batch inserting, but you can do it manually with the following steps.
Make a single query to the database using a Postgresql VALUES list to load any potentially duplicate records.
Compare the records you are about to batch create and pull out any duplicates
Manually generate and return your error message
Step 1 looks a little like this
# Build array of uniq attribute pairs we want to check for
uniq_attrs = new_collection.map do |record|
[
record.day,
record.client_id,
]
end
# santize the values and create a tuple like ('Monday', 5)
values = uniq_attrs.map do |attrs|
safe = attrs.map {|v| ActiveRecord::Base.connection.quote(v)}
"( #{safe.join(",")} )"
end
existing = Preference.where(%{
(day, client_id) in
(#{values.join(",")})
})
# SQL Looks like
# select * from preferences where (day, client_id) in (('Monday',5), ('Tuesday', 3) ....)
Then you can take the collection existing and use it in steps 2 and 3 to pull out your duplicates and generate your error messages.
When I've needed this functionality, I've generally made it a self method off my class, so something like
class Preference < ApplicationRecord
def self.filter_duplicates(collection)
# blah blah blah from above
non_duplicates = collection.reject do |record|
existing.find do |exist|
exist.duplicate?(record)
end
end
[non_duplicates, existing]
end
def duplicate?(record)
record.day == self.day &&
record.client_id = self.client_id
end
end

Convert an Object to hash then save it to user's column

Could not find nothing close to what I'm trying to do. I want to store an object into a user's column. That column is in the form of an array:
#postgres
def change
add_column :users, :interest, :string, array: true, default: '{}'
end
I have another model called FooBar setup for other use. Each user has unique information inside as I've added a user_id key.
Im trying to make more sense:
def interest
#user = User.find(current_user.id ) # I need the logged in user's id
#support = Support.find(params[:id]) # I need the post's id they are on
u = FooBar.new
u.user_id = #user
u.support_id = #support
u.save # This saves a new Foo object..this is what I want
#user.interest.push(FooBar.find(#user)) # This just stores the object name itself ;)
end
So when I call u1 = FooBar.find(1) I get value return in hash. I want when I say u1.interest I get the same. The reason is, I need to target those keys on the user ie: u1.interest[0].support_id
Is this possible? I've looked over my basic ruby docs and nothing works. Oh..if I passed FooBar.find(#user).inspect I get the hash but not the way I want it.
Im trying to do something similar to stripe. Look at their data key. That's a hash.
Edit for Rich' answer:
I have, literally, a model called UserInterestSent model and table:
class UserInterestSent < ActiveRecord::Base
belongs_to :user
belongs_to :support # you can call this post
end
class CreateUserInterestSents < ActiveRecord::Migration
def change
create_table :user_interest_sents do |t|
t.integer :user_id # user's unique id to associate with post (support)
t.integer :interest_sent, :default => 0 # this will manually set to 1
t.integer :support_id, :default => 0 # id of the post they're on
t.timestamps # I need the time it was sent/requested for each user
end
end
end
I call interest interest_already_sent:
supports_controller.rb:
def interest_already_sent
support = Support.find(params[:id])
u = UserInterestSent.new(
{
'interest_sent' => 1, # they can only send one per support (post)
'user_id' => current_user.id, # here I add the current user
'support_id' => support.id, # and the post id they're on
})
current_user.interest << u # somewhere this inserts twice with different timestamps
end
And the interest not interests, column:
class AddInterestToUsers < ActiveRecord::Migration
def change
add_column :users, :interest, :text
end
end
HStore
I remembered there's a PGSQL datatype called hStore:
This module implements the hstore data type for storing sets of
key/value pairs within a single PostgreSQL value. This can be useful
in various scenarios, such as rows with many attributes that are
rarely examined, or semi-structured data. Keys and values are simply
text strings.
Heroku supports it and I've seen it used on another live application I was observing.
It won't store your object in the same way as Stripe's data attribute (for that, you'll just need to use text and save the object itself), but you can store a series of key:value pairs (JSON).
I've never used it before, but I'd imagine you can send a JSON object to the column, and it will allow you to to use the attributes you need. There's a good tutorial here, and Rails documentation here:
# app/models/profile.rb
class Profile < ActiveRecord::Base
end
Profile.create(settings: { "color" => "blue", "resolution" => "800x600" })
profile = Profile.first
profile.settings # => {"color"=>"blue", "resolution"=>"800x600"}
profile.settings = {"color" => "yellow", "resolution" => "1280x1024"}
profile.save!
--
This means you should be able to just pass JSON objects to your hstore column:
#app/controllers/profiles_controller.rb
class ProfilesController < ApplicationController
def update
#profile = current_user.profile
#profile.update profile_params
end
private
def profile_params
params.require(:profile).permit(:x, :y, :z) #-> z = {"color": "blue", "weight": "heavy"}
end
end
As per your comments, it seems to me that you're trying to store "interest" in a User from another model.
My first interpretation was that you wanted to store a hash of information in your #user.interests column. Maybe you'd have {name: "interest", type: "sport"} or something.
From your comments, it seems like you're wanting to store associated objects/data in this column. If this is the case, the way you're doing it should be to use an ActiveRecord association.
If you don't know what this is, it's essentially a way to connect two or more models together through foreign keys in your DB. The way you set it up will determine what you can store & how...
#app/models/user.rb
class User < ActiveRecord::Base
has_and_belongs_to_many :interests,
class_name: "Support",
join_table: :users_supports,
foreign_key: :user_id,
association_foreign_key: :support_id
end
#app/models/support.rb
class Support < ActiveRecord::Base
has_and_belongs_to_many :users,
class_name: "Support",
join_table: :users_supports,
foreign_key: :support_id,
association_foreign_key: :user_id
end
#join table = users_supports (user_id, support_id)
by using this, you can populate the .interests or .users methods respectively:
#config/routes.rb
resources :supports do
post :interest #-> url.com/supports/:support_id/interest
end
#app/controllers/supports_controller.rb
class SupportsController < ApplicationController
def interest
#support = Support.find params[:support_id] # I need the post's id they are on
current_user.interests << #support
end
end
This will allow you to call #user.interests and bring back a collection of Support objects.
Okay, look.
What I suggested was an alternative to using interest column.
You seem to want to store a series of hashes for an associated model. This is exactly what many-to-many relationships are for.
The reason your data is being populated twice is because you're invoking it twice (u= is creating a record directly on the join model, and then you're inserting more data with <<).
I must add that in both instances, the correct behaviour is occurring; the join model is being populated, allowing you to call the associated objects.
What you're going for is something like this:
def interest_already_sent
support = Support.find params[:id]
current_user.interests << support
end
When using the method I recommended, get rid of the interest column.
You can call .interests through your join table.
When using the code above, it's telling Rails to insert the support object (IE support_id into the current_user (IE user_id) interests association (populated with the UserInterestSelf table).
This will basically then add a new record to this table with the user_id of current_user and the support_id of support.
EDIT
To store Hash into column, I suggest you to use "text" instead
def change
add_column :users, :interest, :text
end
and then set "serialize" to attribute
class User < ActiveRecord::Base
serialize :interest
end
once it's done, you can save hash object properly
def interest
#user = User.find(current_user.id ) # I need the logged in user's id
#support = Support.find(params[:id]) # I need the post's id they are on
u = FooBar.new
u.user_id = #user
u.support_id = #support
u.save # This saves a new Foo object..this is what I want
#user.interest = u.attributes # store hash
#user.save
end
To convert AR object to hash use object.attributes.
To store a custom hash in a model field you can use serialize or ActiveRecord::Store
You can also use to_json method as object.to_json
User.find(current_user.id ).to_json # gives a json string

Creating a Rails change log

I am pretty new to rails (and development) and have a requirement to create a change log. Let's say you have an employees table. On that table you have an employee reference number, a first name, and a last name. When either the first name or last name changes, I need to log it to a table somewhere for later reporting. I only need to log the change, so if employee ref 1 changes from Bill to Bob, then I need to put the reference number and first name into a table. The change table can have all the columns that mnight change, but most only be populated with the reference number and the changed field. I don't need the previous value either, just the new one. hope that makes sense.
Looked at gems such as paper trail, but they seem very complicated for what I need. I don't ever need to manipulate the model or move versions etc, I just need to track which fields have changed, when, and by whom.
I'd appreciate your recommendations.
If you insist on building your own changelog, based on your requirements you can do so using a few callbacks. First create your log table:
def up
create_table :employee_change_logs do |t|
t.references :employee
# as per your spec - copy all column definitions from your employees table
end
end
In your Employee model:
class Employee < ActiveRecord::Base
has_many :employee_change_logs
before_update :capture_changed_columns
after_update :log_changed_columns
# capture the changes before the update occurs
def capture_changed_columns
#changed_columns = changed
end
def log_changed_columns
return if #changed_columns.empty?
log_entry = employee_change_logs.build
#changed_columns.each{|c| log_entry.send(:"#{c}=", self.send(c))}
log_entry.save!
end
end
I recommend the gem vestal_versions.
To version an ActiveRecord model, simply add versioned to your class like so:
class User < ActiveRecord::Base
versioned
validates_presence_of :first_name, :last_name
def name
"#{first_name} #{last_name}"
end
end
And use like this:
#user.update_attributes(:last_name => "Jobs", :updated_by => "Tyler")
#user.version # => 2
#user.versions.last.user # => "Tyler"
The first thing we did was put an around filter in the application controller. This was how I get the current_employee into the employee model, which was the challenge, especially for a newbie like me!
around_filter :set_employee_for_log, :if => Proc.new { #current_account &&
#current_account.log_employee_changes? && #current_employee }
def set_employee_for_log
Thread.current[:current_employee] = #current_employee.id
begin
yield
ensure
Thread.current[:current_employee ] = nil
end
end
end
Next, in the employee model I defined which fields I was interested in monitoring
CHECK_FIELDS = ['first_name', 'last_name', 'middle_name']
then I added some hooks to actually capture the changes IF logging is enabled at the account level
before_update :capture_changed_columns
after_update :log_changed_columns, :if => Proc.new { self.account.log_employee_changes? }
def capture_changed_columns
#changed_columns = changed
#changes = changes
end
def log_changed_columns
e = EmployeeChangeLog.new
Employee::CHECK_FIELDS.each do |field|
if self.send("#{field}_changed?")
e.send("#{field}=", self.send(field))
end
end
if e.changed?
e.update_attribute(:account_id, self.account.id)
e.update_attribute(:employee_id, self.id)
e.update_attribute(:employee_ref, self.employee_ref)
e.update_attribute(:user_id, Thread.current[:current_employee])
e.save
else return
end
end
And that;s it. If the account enables it, the app keeps an eye on specific fields and then all changes to those fields are logged to a table, creating an simple audit trail.

How to prevent has_and_belongs_to_many delete option from deleting records that are being used in Rails?

class Assembly < ActiveRecord::Base
has_and_belongs_to_many :parts
end
class Part < ActiveRecord::Base
has_and_belongs_to_many :assemblies
end
In console:
part1 = Part.new
assembly1 = Assembly.new
assembly1.parts << part1
part1.delete
Parts.all
=> []
Checking assembly1.parts shows that there is still a relationship.(!)
How is this possible when the record was deleted?
Also, how to prevent deletion of parts that are associated to assemblies?
Working in Rails 3.0.7.
Everything you were doing here was done from memory (nothing was stored in the database).
the ActiveRecord delete method will remove an object from the database but it doesn't look for other objects in memory that may have already been referencing that object. I think if you did assembly1.parts.delete(part1) that would likely do what you were expecting.
If you had saved the objects to the database:
part1 = Part.create
assembly1 = Assembly.create(:parts => [part1])
assembly1.parts
# => [part1]
part1.delete
assembly1.parts
# => [part1]
assembly1.reload
assembly1.parts
# => []
Note here how even if it's in the database part1.delete won't necessarily remove it from your assembly object until you refresh the in-memory collection or delete it using the method I mentioned earlier assembly1.parts.delete(part1)
UPDATE
I think you usually shouldn't use the delete() method. You should almost always use destroy(). delete() will just fire off a delete to the database and ignores all callbacks and I believe :dependent => :destroy-style declarations in your model. If you use the destroy() method then you can declare a before_destroy callback in your model:
class MyClass
has_and_belongs_to_many :foos
before_destroy :allow_destroy
def allow_destroy
foos.empty?
end
end
That should get your requirement of not destroying it if it is part of an assembly. You cannot stop delete() from executing because it ignores callbacks: ActiveRecord::Relation#delete documentation
More info about model callbacks (documentation)
You need to add :dependent => :destroy to the controller.
You can prevent myobject.delete in the database by adding some referential integrity with a foreign key.
I recommend looking at Adding foreign key to a rails model
in my project this did not prevent myobject.destroy. I think this is because it uses the awesome nested set gem tries really hard to handle cascading destroy for you.
to prevent myobject.destroy
I found How do I 'validate' on destroy in rails supper helpful
I ended up using an before_destroy as well as adding some referential integrity with add_foreign_key in a migration.
this will prevent deletion if myobject.delete is used or if myobject.destroy is used.
in my case myobject hads many of itself and belongs to one of itself. this is what act_as_nested_set handles.
Model
class Myobject
before_destroy :allow_destroy
# ^ this has to be above act_as_nested_set
acts_as_nested_set
def allow_destroy
return true if self.descendants.blank?
# the error is optional.
self.errors.add('Cannot_delete', 'myobject still has children')
throw(:abort)
end
end
Migration
class AddForignKeyToMyobject < ActiveRecord::Migration
def change
add_foreign_key :myobject, :myobject, column: :parent_id
end
end

Resources