How to do bulk update in rails on habtm association - ruby-on-rails

I have two models:
#user.rb
has_and_belongs_to_many :groups
#group.rb
has_and_belongs_to_many :users
I want to associate groups to users such that many users can be associated to many groups.
Front end side of the application is sending the user_ids and group_ids to the update method.
So in the UsersController, i've found out each user and associated that user with the groups.
def update
users = User.where(id: [325, 326])
users.each { |user| user.update(group_ids: [1, 2])}
end
I need to know the standard way of updating the associated records and what is the most efficient way of updating associated records?

There is a method update_all
def update
users = User.where(id: [325, 326])
users.update_all group_ids: [1, 2]
end

The simplest ActiveRecord-native approach is to create a model class for your join table and use that, e.g.:
class GroupsUser < ActiveRecord::Base; end
(Note that the model class is GroupsUser so that by convention ActiveRecord will know it's for the groups_users table. It's a little goofy but it works.)
Then you can use insert_all on the join table:
user_ids = [325, 326]
group_ids = [1, 2]
values = user_ids.map do |uid|
group_ids.map do |gid|
{ user_id: uid, group_id: gid }
end
end.flatten
# => [{user_id: 325, group_id: 1}, {user_id: 325, group_id: 2}, {user_id=>326, group_id: 1}, {user_id: 326, group_id: 2}]
GroupsUser.insert_all(values)
Note though that this is is insert_all, not update_all -- update_all doesn't really make sense for a join table. We're adding values, not replacing them. (At least on PostgreSQL and SQLite, if there's a unique index on the join table, duplicates will be ignored -- on MySQL you might need to jump through some more hoops to exclude existing relationships, unless you're deleting those anyway as below.)
If you want to replace any existing associations, you'll need to delete those explicitly:
ActiveRecord::Base.transaction do
GroupsUser.delete_all(user_id: user_ids)
# and/or GroupsUser.delete_all(group_id: group_ids), depending
GroupsUser.insert_all(values)
end
Also worth noting: there's no need to put GroupsUser in app/models/groups_user.rb; if this is the only code that uses it, you can put it right in the file where it's used. This is especially useful in migrations.

Related

How to get serialized attribute in rails?

I have serialized column of Post model
serialize :user_ids
after save I can see in my console:
> p = Post.last
=> #<Post id: 30, title: "almost done2", created_at: "2017-05-08 15:09:40", updated_at: "2017-05-08 15:09:40", attachment: "LOGO_white.jpg", user_ids: [1, 2]>
I have permitted params :user_ids at my controller
def post_params
params.require(:post).permit(:title, :attachment, :user_ids)
end
All I need is to store #user_ids and copy to other model as atrribute,
but can't get instance like #user_ids = #Post.last.user_ids
Get error
ActiveRecord::StatementInvalid: SQLite3::SQLException: no such column: users.post_id: SELECT "users".id FROM "users" WHERE "users"."post_id" = ?
Thanks for help!
I think you've chosen really wrong name for your serialized attribute, unfortunately!
See, Rails is trying to be helpful and relation_ids method is allowing you to call ids of all related object. Consider those two models
class User
belongs_to :post
end
class Post
has_many :users
end
You can call something like
Post.last.user_ids
Which will then try to find all users that belong to this post and give you back their ids.
Look at query that you get:
SELECT "users".id FROM "users" WHERE "users"."post_id" = ?
This is exactly what Rails is trying to do. Select USERS that belong to the post and give you their ids. However User does not have post_id column so this fails.
You're better off changing the name of your serialized column to something that won't confuse Rails. However, if you want to keep your column, you can override the method by putting this on the Post model
def user_ids
self[:user_ids]
end
This is non-ideal however, but will leave it up for you to decide what to do. My preference would be column rename, really. Will save you a lot of headeache

Joining two ActiveRecord associations on common attribute

Let's say I have a User model. User has 2 has_many associations, that is User has many pencils and has many cars. Cars and Pencils table has same attribute, :date, and separate such as :speed(car) and :length(pencil). I want to join a user's pencils and cars on their common attribute, :date, so that I have an array/relation [:date, :speed, :length]. How do I achieve that, I tried joins and merge but they were no use.
I'd definitely recommend getting this into a query rather than a loop, for efficiency's sake. I think this will work:
Car.joins(:user => :pencils).where("pencils.date = cars.date")
And if you want to reduce it to the array immediately:
Car.joins(:user => :pencils).where("pencils.date = cars.date").pluck("cars.date", "cars.speed", "pencils.length")
If you need to include matches where date is nil, you might need to add:
Car.joins(:user => :pencils).where("(pencils.date = cars.date) OR (pencils.date IS NULL AND cars.date IS NULL)")
Many more efficient options exist, but here is one possible approach:
class User < ActiveRecord::Base
def get_merged_array
dates = (cars.map(&:date) & pencils.map(&:date))
results = []
dates.each do |date|
cars.where(date: date).each do |car|
pencils.where(date: date).each do |pencil|
results << [date, car.speed, pencil.length]
end
end
end
results
end
end

Rails 4 - Is there a performance win between using self. vs Model call in after_create callbacks

When creating a model Deal, I use an after_create to create 10 prizes on the prize table.
Is there a performance difference or any performance-related (like garbage colleciton maybe) that would help me decide between a and B
A
after_create :create_prizes
def create_prizes
300000.times do
prizes = self.prizes.create(:deal_id => self.id, :admin_user_id => self.admin_user_id)
end
end
B
after_create :create_prizes
def create_prizes
300000.times do
prizes = Prize.create(:deal_id => self.id, :admin_user_id => self.admin_user_id)
end
end
B
Note that when the Admin creates a deal, it will create a very large number of prizes (up to 300,000).
Thanks for any help,
Mathieu
Option B should be slightly faster as AR does't need to traverse the relations to find the foreign key. However, inserting 300,000 records will be slow either way.
Consider generating a SQL INSERT statement or passing an array to create.
Prize.create([{deal_id: 1}, {deal_id: 2}])
https://www.coffeepowered.net/2009/01/23/mass-inserting-data-in-rails-without-killing-your-performance/

Rails: validate unique friendship relationships

I'm following http://railscasts.com/episodes/163-self-referential-association
to make a friendship system.
#Friendship model
id | user_id | friend_id
The problem is
How can I make the combination of user_id and friend_id unique?
prevent creating user_id = 1, friend_id = 2 from user_id = 2. friend_id = 1
I find this model design is really bad. Can I model it to actually store the combine of 2 ids? Like friendship_id_combination = [1, 2], and then validate it.
You can use the following:
validate :users_are_not_already_friends
def users_are_not_already_friends
combinations = ["user_id = #{user_id} AND friend_id = #{friend_id}",
"user_id = #{friend_id} AND friend_id = #{user_id}"]
if User.where(combinations.join(' OR ')).exists?
self.errors.add(:user_id, 'Already friends!')
end
end
Update 2019:
Revised suggestion:
def users_are_not_already_friends
if User.where(user_id: friend_id, friend_id: user_id).exist?
|| User.where(user_id: user_id, friend_id: friend_id).exist?
self.errors.add(:user_id, 'Already friends!')
end
end
And I would strongly advise to add a DB constraint (unique index) based on the composite user_id and friend_id:
CREATE UNIQUE INDEX users_user_id_friend_id_key ON users (user_id, friend_id);
Yes. You are thinking along the right path. Perhaps the simplest solution is to use the database table to create the validation by making the combination of IDs a unique index.
class CreateFriendshipsUnq < ActiveRecord::Migration
def change
add_index :friendships, [:user_id, :friend_id], unique: true
end
end
I think this will do what you want.
Why you need this unique combination. Consider if you want to fetch friends of single user then in current scenario you can find it using simple association.
#user.friends
And if you don't want to add two keys then you need to fire two queries to find same details. One for your friends and other for who requested you as a friend.

Get ID of Rails Model before saving...?

How do you get the id of a rails model before it is saved?
For example, if I create a new model instance, how can I get its ID before it is saved?
I know that the id is created onsave and according to the database but is there a workaround?
I was looking for this too, and I found an answer:
Let's suppose model name is "Model" and table name is "models"
model.rb
before_save {
next_id=Model.connection.select_value("Select nextval('models_id_seq')")
}
This will output the value your record will take for id IF it gets saved
Usually when people think they need to do this they actually do not need to do it. Like John says, explain what you are trying to do and someone can probably suggest a way to do it without having to know the id in advance.
This is less a Rails question and more a database question. This is a problem that will present itself in any web application framework, and the solution is the same in all places. You have to use a database transaction.
The basic flow will work like this.
Open a transaction
Save your model
Use the ID assigned by the database
If it turns out you actually don't want to keep this model in the database, roll back the transaction.
If it turns out you want to keep the model in the database, commit the transaction.
The main thing you will notice from this approach is that there will be gaps in your IDs where you rolled back the transaction.
Using the default Rails convention of an auto-incrementing integer primary key, there's no way to get the ID of a model before it's saved because it's generated by the RDBMS when the new row is inserted in the relevant table.
What problem are you actually trying to solve?
Most of the time when I needed an id can be grouped into a short list.
When creating nested associations or connectin of the associations through.
Let's assume we have: :user that have :pets through :user_pets association, where we will save their type.
If we have a properly configured "has_many: through Association" we can just
User.pets.create(name: "Rex") but this is too simplistic, as we want to creat :pet type in :user_pets.
u = User.create(name: "Cesar")
u.id # => 1 # works fine
p = u.pets.create(name: 'Rex')
# => rails will create UserPets => {id: 1, user_id: 1, pet_id: 1} for us
# But now we have a problem, how do we find :user_pets of our :pet?
# remember we still need to update the :type, the ugly (wrong) way:
up = p.user_pets.first
up.type = 'dog'
up.save! # working but wrong
# Do you see the problems here? We could use an id
P = Pet.new( name: "Destroyer" )
p.id # will not work, as the pet not saved yet to receive an id
up = UserPet.new( user_id: U.id, pet_id: p.id )
# => UserPet {id: 2, user_id: 1, pet_id: nil} # as we expected there is no id.
# What solutions do we have? Use nested creation!
# Good
up = UserPet.new(user_id: u.id, type: "dog")
# even better
up = u.user_pets.new(type: "dog")
# it's just a shortcut for the code above,
# it will add :user_id for us, so let's just remember it.
# Now lets add another 'new' from our creatd 'user_pet'
p = up.pets.new(name: "Millan")
user.save!
# => UserPet: {id: 3, user_id: 1, pet_id: 2, type: 'dog'} # => Pet: {id: 2, name: "Sam"}
# everything is working! YEY!
# we can even better, than writing in the beginning "User.create",
# we can write "User.new" and save it with all the nested elements.
You saw how this created all the ids for us? Let's move to something even more complex!
Now we have an additional table :shampoo that exactly as :user_pet, belongs to a :pet and a :user
We need to create it without knowing the id of the :user and :pet
u = User.new('Mike')
up = u.user_pets.new(type: "cat")
p = up.pets.new(name: "Rowe")
# But what are we doing now?
# If we do:
s = u.shampoos.new(name: "Dirty Job")
# => Shampoo: {user_id: 2, pet_id: nil, name: "..."}
# We get "pet_id: nil", not what we want.
# Or if we do:
s = p.shampoos.new(name: "Schneewittchen")
# => Shampoo: {user_id: nil, pet_id: 3, name: "..."}
# We get "user_id: nil", in both cases we do not get what we want.
# So we need to get the id of not created record, again.
# For this we can just do as in the first example (order is not important)
s = u.shampoos.new(name: "Mission Impossible")
# => Shampoo: {id: 3, user_id: 2, pet_id: nil, name: "..."}
s.pet = p # this will give the missing id, to the shampoo on save.
# Finish with save of the object:
u.save! #=> Shampoo: {id: 3, user_id: 2, pet_id: 3, name: '...'} # => Pet: ...
# Done!
I tried to cover most common causes when you need element id, and how to overcome this. I hope it will be useful.
I don't believe there are any workarounds since the id is actually generated by the database itself. The id should not be available until after the object has been saved to the database.
Consider doing what you want right after the instance is created.
after_create do
print self.id
end
First understand the structure of database.
Id is generated using sequence
increment done by 1 (specified while creating sequence)
Last entry to database will have highest value of id
If you wanted to get id of record which is going to be saved,
Then you can use following:
1. id = Model.last.id + 1
model = Model.new(id: id)
model.save
# But if data can be delete from that dataabse this will not work correctly.
2. id = Model.connection.select_value("Select nextval('models_id_seq')")
model = Model.new(id: id)
model.save
# Here in this case if you did not specified 'id' while creating new object, record will saved with next id.
e.g.
id
=> 2234
model = Model.new(id: id)
model.save
# Record will be created using 'id' as 2234
model = Model.new()
model.save
# Record will be created using next value of 'id' as 2235
Hope this will help you.
I just ran into a similar situation when creating a data importer. I was creating a bunch of records of different types and associating them before saving. When saving, some of the records threw validation errors because they had validate_presence_of a record that was not yet saved.
If you are using postgres, active record increments the id it assigns to a Model by keeping a sequence named models_id_seq (sales_id_seq for Sale etc.) in the database. You can get the next id in this sequence and increment it with the following function.
def next_model_id
ActiveRecord::Base.connection.execute("SELECT NEXTVAL('models_id_seq')").first["nextval"].to_i
end
However, this solution is not good practice as there is no guarantee that active record will keep id sequences in this way in the future. I would only use this if it was used only once in my project, saved me a lot of work and was well documented in terms of why it should not be used frequently.
I know it's an old question, but might as well throw my answer in case anyone needs to reference it
UserModel
class User < ActiveRecord::Base
before_create :set_default_value
def set_default_value
self.value ||= "#{User.last.id+1}"
end

Resources