ActiveRecord RoR - Saving only new associated objects - ruby-on-rails

How can I save (insert) only associated objects without saving (updating) the base object?
For example I just want to save the phone numbers, I don't wan to resave/update the person object.
def create_numbers
#params => person_id => 41, person => {:phone_number => '12343445, 1234566, 234886'}
#person = params[:person_id]
nums = params[:person][:phone_numbers].split(',')
nums.each do |num|
#person.phone_numbers.build(:number => num)
end
#person.save #here I just want to save the numbers, I don't want to save the person. It has read only attributes
end
Models:
Person < ...
# id, name
belongs_to :school, :class_name => :facility
has_many :phone_numbers
end
PhoneNumber < ...
# id, number
belongs_to :person
end
This is a bit of a dumb example, but it illustrates what I'm trying to accomplish

How about #person.phone_numbers.create(:number => num)
The downside is that you wont know whether it failed or not - you can handle that, but it depends on how exactly you want to handle it.

The simplest approach is to replace your build(:number => num) with create(:number => num), which will build and save the phone_number object immediately (assuming it passes validation).
If you need to save them all after creating the whole set (for some reason), you could just do something like
#person.phone_numbers.each{|num| num.save}

Related

Rails - extend attributes method

I have a group of input, for which I expect a large amount data (list of objects), so I want this input on create/update action to be wrapped inside of ActiveRecord transaction.
There is model Student, which has_one Account.
ACCOUNT_FIELDS=[:name, :surname, :email, :phone, :activated]
has_one :account, :as => :account_holder, autosave: true, :dependent => :destroy
validates_associated_extended :account
ACCOUNT_FIELDS.each do |action|
define_method action do
get_or_build_account.send(action)
end
setter_action = action.to_s + "="
define_method setter_action do |arg|
get_or_build_account.send(setter_action, arg)
end
end
here I made a reader/writer methods, so #student.name will return related data from account, also I can assign it through #student thanks to autosave.
Issue: as I said, I want it to be wrapped inside of transaction, so in my controller I don't save anything. Each student is assigned like this
student.attributes = #...data
Where student later on passed to transaction block.
But! For this specific model I want to student.attributes also return fields from ACCOUNT_FIELDS.
Normally it works with student.account_attributes but as I said, later student is processed in transaction, and it is made with module, which I want to be reusable for some other models (which doesn't need this logic).
So rather than modifying my module code with some conditions, I want instance of this model to return needed account fields when just called self.attributes
#student.attributes #=> :school_id => 1, :name => "John"...
where name is self.name from self.account.name
Try this:
def attributes
new_attributes={}
ACCOUNT_FIELDS.each do |field|
new_attributes[field]=self.send(field)
end
super.merge(new_attributes)
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

stale association data on ActiveRecord model instance?

I'm using Rails 2, and I have a one-to-many association between the Project model and the Schedule model in this app.
I have an observer check when attributes of various things change, and in response, it iterates over an array of hashes with which to populate new Schedule instances.
Each Schedule instance should have an attribute, disp_order, which eventually tells the front end where to display it when showing them in a list. The disp_order is populated upon adding the schedule to the project, so that it equals one more than the current highest disp_order.
The problem is in the iteration in the observer. When I iterate over the array of hashes filled with Schedule data, each time through the iteration should calculate the disp_order as one higher than the previous one, to account for the Schedule it just added. However, in practice, it doesn't--unless I refresh the Project object in the middle of the iteration with project = Project.find(project.id), or else it seems always to calculate the same max value of the disp_orders, and doesn't have an accurate list of those Schedule instances to go on.
Is there a better way to do this? (Note: I just mean is there a better way so that I don't have to tell project to re-find itself. There are a number of places I'm actively cleaning up the rest of the code which follows, but for this question I'm really only interested in this one line and things that impact it.)
project.rb
class Project < ActiveRecord::Base
# ...
has_many :schedules, :dependent => :destroy
# ...
belongs_to :data, :polymorphic => true, :dependent => :destroy
accepts_nested_attributes_for :data
# ...
schedule.rb
class Schedule < ActiveRecord::Base
belongs_to :project, :include => [:data]
# ...
project_event_observer.rb
class ProjectEventObserver < ActiveRecord::Observer
# ...
def perform_actions(project, actions)
# ...
actions.each { |action|
action.each { |a_type, action_list|
action_list.each { |action_data|
self.send(action_type.to_s.singularize, action_data, project)
project = Project.find(project.id) #My question is about this line.
}
}
}
# ...
sample actions for the iteration above
[
{:add_tasks => [
{:name => "Do a thing", :section => "General"},
{:name => "Do another thing", :section => "General"},
]
},
# More like this one.
]
the observer method that receives the rest of the action data
def add_task(hash, project)
# ...
sched = Schedule.new
hash.each { |key, val|
# ...
sched.write_attribute(key, val)
# ...
}
sched.add_to(project)
end
schedule.rb
def add_to(proj)
schedules = proj.schedules
return if schedules.map(&:name).include?(self.name)
section_tasks = schedules.select{|sched| sched.section == self.section}
if section_tasks.empty?
self.disp_order = 1
else
self.disp_order = section_tasks.map(&:disp_order).max + 1 #always display after previously existing schedules
end
self.project = proj
self.save
end
You're running into this issue because you're working off of a in-memory cached object.
Take a look at the association documentation.
You can pass the argument of true in your Schedule model's add_to function to reload the query.
Other options would include inverse_of but that's not available to you because you're using polymorphic associations, as per the docs.

How to handle rails enums?

How can I handle enums in rails? I have googled this, but not finding any clean solutions. So far I have come up with including the concern below on models that use interaction_type_id. This is my foreign key to my enum table in the database. Using this approach I don't have to use ids all over my code, but when saving an object that relates to an interact_type I can say
myobject.interaction_type = :file_download
This can then persist the the database with the correct id since the concern(see concern below - included on models that use the enum) will return the correct id.
module InteractionTypeEnum
extend ActiveSupport::Concern
included do
INTERACTION_TYPE = { file_download: 1, email: 2, telesales: 3, registration: 4, enrolment: 5 }
end
def interaction_type
INTERACTION_TYPE.key(read_attribute(:interaction_type_id)).to_s.gsub('_',' ').capitalize
end
def interaction_type=(s)
write_attribute(:interaction_type_id, INTERACTION_TYPE[s])
end
end
This just feels heavy. There must be an easier/cleaner way. Now when trying to write tests for this it gets even more messy.
Most of the reasons for wanting my enums in code and database are performance (code) and reporting (database).
Any help appreciated. Thanks.
I recommend the active_enum gem.
Example from their docs, if you have an integer column called sex on the class User:
class User < ActiveRecord::Base
enumerate :sex do
value :name => 'Male'
value :name => 'Female'
end
end
Or you can define the enum in a seperate class:
class Sex < ActiveEnum::Base
value 1 => 'Male'
value 2 => 'Female'
end
class User < ActiveRecord::Base
enumerate :sex, :with => Sex
end
I like the abstraction it provides, and it saves you from having to create an entire database table just to store your enum values.
I use the following method, say I have
class PersonInfo < ActiveRecord::Base
belongs_to :person_info_type
end
and PersonInfoType is a simple domain table, containing the possible types of information.
Then I code my model as follows:
class PersonInfoType < ActiveRecord::Base
PHONE = 1
EMAIL = 2
URL = 3
end
I have a seed fills the database with the corresponding data.
And so when assigning some person-information can do something like
person.person_infos << PersonInfo.create(:info => 'http://www.some-url.com', :person_info_type_id => PersonInfoType::URL)
This code can then be further cleaned up using relations:
class PersonInfo
belongs_to :person_info_type
def self.phones
PersonInfo.where(:person_info_type_id => PersonInfoType::PHONE)
end
end
person.person_infos << PersonInfo.phones.create(:info => '555 12345')

Updating an existing record just created in a Callback

Through a after_create callback I'm trying to assign a User a Program_id though a join model, and assign initial values for three additional parameters in that same join model.
Assigning the program_id works fine, I can't figure out how to add the additional parameters.
class User < ActiveRecord::Base
after_create :assign_user_program, :add_initial_programs
def assign_user_program
self.programs = Program.for_user(self)
end
class Program < ActiveRecord:Base
def self.for_user(user)
where(:goal_id => user.goal_id, :experience_id => user.experience_level_id, :gender => user.gender)
end
I tried creating another method in the after_create to assign the other three parameters, but that just creates another record in the join model with the three parameters assigned, and no program_id
def add_initial_programs
self.user_programs.create(:cycle_order => 1, :group_order => 1, :workout_order => 1)
end
Any ideas on how I can simply update the record that was just created?
EDIT:
To clarify, I'm trying to create one record in the join table with the program_id, user_id, group_order, cycle_order, and workout_order, as shown in the picture:
What is happening is that I'm getting two records, one with a program_id, and one with no program_id, but the other values assigned. See picture below:
You should probably run your callback before_create instead of after_create so the extra attributes get set on the model before it's persisted to the database. Doing it after_create means you'll end up sending two queries (one CREATE and one UPDATE) instead of one CREATE.
class User < ActiveRecord::Base
before_create :assign_user_program, :add_initial_programs
protected
def assign_user_program
self.programs = Program.for_user(self)
end
def add_initial_programs
self.user_programs.build(:cycle_order => 1, :group_order => 1, :workout_order => 1)
end
end
If you really want to do it in after_create, you'll have to call save or save! on the user model to actually persist the attribute changes to the database.
So the answer here was actually quite simple. I needed to put all the logic into one method like this:
def add_initial_program
prog_id = Program.for_user(self).first.id
self.user_programs.build( :cycle_order => 1, :group_order => 1, :workout_order => 1, :program_id => prog_id)
end
Which created the desired result of giving a user a program, and assigning the other values I needed.

Resources