ruby on rails fill association table - ruby-on-rails

I have a table of recipes, a table of ingredients and an association table (a recipe "has and belong to many" ingredient and an ingredient "has and belongs to many" recipe).
I do not have a controller or a model for the association table.
I want to write a task that fills the association table with random (yet valid) data.
I wrote a code that generates the valid ids for the association table, but I couldn't figure how to get it to go in the association table (since I don't have its model).
Can I somehow iterate over the recipes, and add the data to the recipe.ingredients list? Will it automatically fill the association table?
My code so far:
namespace :FillRandomAssociationData do
desc "Fills the recipes - ingredients association table with random data"
task :Recipe_Ingredients_Association => :environment do
Recipe.all.each do |rec|
numOfIngredientsPerRecipe = rand(3)
ingredientIDLimit = Ingredient.count
for i in 0..numOfIngredientsPerRecipe
ingRandId = rand(ingredientIDLimit)
.... This is where I got stuck...
end
end
end
end
Thanks,
Li

You just have to fill the recipe object with its ingredients, and save it, Rails will fill the association table:
desc "Fills the recipes - ingredients association table with random data"
task :Recipe_Ingredients_Association => :environment do
Recipe.all.each do |rec|
numOfIngredientsPerRecipe = rand(3)
ingredientIDLimit = Ingredient.count
for i in 0..numOfIngredientsPerRecipe
ingRandId = rand(ingredientIDLimit)
rec.ingredients << Ingredient.find(ingRandId)
end
rec.save!
end
end
Be careful, with this algorithm you can add several time the same ingredient to the recipe.

Related

How to get all records that also "own" specific records performantly

I have the following two models in Rails application
class Students < ApplicationRecord
has_many :courses
attr_accessor :name
end
class Courses < ApplicationRecord
has_many :students
attr_accessor :name, :course_id
end
I would like to get a a list of all courses shared for each student who has been in the same class as a selected student in an efficient manner.
Given the following students:
Jerry's courses ["math", "english", "spanish"],
Bob's courses ["math", "english", "french"],
Frank's courses ["math", "basketweaving"]
Harry's courses ["basketweaving"]
If the selected student was Jerry, I would like the following object returned
{ Bob: ["math", "english"], Frank: ["math"] }
I know this will be an expensive operation, but my gut tells me there's a better way than what I'm doing. Here's what I've tried:
# student with id:1 is selected student
courses = Student.find(1).courses
students_with_shared_classes = {}
courses.each do |course|
students_in_course = Course.find(course.id).students
students_in_course.each do |s|
if students_with_shared_classes.key?(s.name)
students_with_shared_classes[s.name].append(course.name)
else
students_with_shared_classes[s.name] = [course.name]
end
end
end
Are there any ActiveRecord or SQL tricks for a situation like this?
I think you're looking to do something like this:
student_id = 1
courses = Student.find(student_id).courses
other_students = Student
.join(:courses)
.eager_load(:courses)
.where(courses: courses)
.not.where(id: student_id)
This would give a collection of other students that took courses with only two db queries, and then you'd need to narrow down to the collection you're trying to create:
course_names = courses.map(&:name)
other_students.each_with_object({}) do |other_student, collection|
course_names = other_student.courses.map(&:name)
collection[other_student.name] = course_names.select { |course_name| course_names.include?(course_name) }
end
The above would build out collection where the key is the student names, and the values are the array of courses that match what student_id took.
Provided you setup a join model (as required unless you use has_and_belongs_to_many) you could query it directly and use an array aggregate:
# given the student 'jerry'
enrollments = Enrollment.joins(:course)
.joins(:student)
.select(
'students.name AS student_name',
'array_agg(courses.name) AS course_names'
)
.where(course_id: jerry.courses)
.where.not(student_id: jerry.id)
.group(:student_id)
array_agg is a Postgres specific function. On MySQL and I belive Oracle you can use JSON_ARRAYAGG to the same end. SQLite only has group_concat which returns a comma separated string.
If you want to get a hash from there you can do:
enrollments.each_with_object({}) do |e, hash|
hash[e.student_name] = e.course_names
end
This option is not as database independent as Gavin Millers excellent answer but does all the work on the database side so that you don't have to iterate through the records in ruby and sort out the courses that they don't have in common.

Can i cache a parent in rails?

I have this code:
Business.all.limit(50).each do |business|
card = {name: business.name, logo: business.logo, category: business.category.name}
feed << card
end
In my models, Business belongs to Category, and Category has many Business
My problem is that this will query the DB 50 times, each time I want to retrieve each business' category name.
I have seen Rails cache effectively by using :include, but all examples I have seen are for child records, for example:
Category.all :include => [:businesses]
but in this case I want to cache the parent's data.
Its the same you can do by using singular model name
Business.includes(:category)

Ruby on Rails: Activerecord collection associated models

I have two models as follows:
class Bookshelf < ActiveRecord::Base
has_many :books
scope :in_stock, -> { where(in_stock: true) }
end
class Book < ActiveRecord::Base
belongs_to :bookshelf
end
I would like to find all the books in a collection of bookshelves based on a column in the bookshelf table efficiently.
At the moment I have to loop through each member as follows:
available_bookshelves = Bookshelf.in_stock
This returns an activerecord relation
To retrieve all the books in the relation, i am looping through the relation as follows:
available_bookshelves.each do |this_bookshelf|
this_bookshelf.books.each do |this_book|
process_isbn this_book
end
end
I would like all the books from the query so that I don't have to loop through each "bookshelf" from the collection returned individually. This works but feels verbose. I have other parts of the app where similar queries-loops are being performed.
EDIT:
Some clarification: Is there a way to get all books in all bookshelves that fit a certain criteria?
For example, if there are 5 brown bookshelves, can we retrieve all the books in those bookshelves?
something like (this is not valid code)
brown_books = books where bookshelf is brown
You can use the following query to get the books in the in stock book shelves
available_books = Book.where(bookshelf_id: Bookshelf.in_stock.select(:id))
That will run a single query which will look like:
SELECT books.*
FROM books
WHERE books.bookshelf_id IN (SELECT id FROM bookshelves WHERE in_stock = true)

How to select from a table that has been joined with same model/class/table?

I'm trying to get a count of how many subcontacts every contact has.
Class Contacts
has_many :subcontacts, class_name: "Contact",foreign_key: "supercontact_id"
belongs_to :supercontact, class_name:"Contact"
And here's the activerecord part i have so far that's roughly what i'm trying to do.
Contact.joins{subcontacts.outer}.select(subcontacts.count as subcontact_count)
I think the problem is that the joins portion is looking for a association name and the select part is looking for a table name. The trouble is that the table name is the same table... What's the best way to do this so that it stays as a relation or using SQL so that we can minimize the number of queries so that it isn't an N+1 problem?
Try using
results = Contact.joins(:subcontacts).select("count(subcontacts.id) as count, contacts.id").group("contacts.id")
and count can be fetched as
results.map do |result|
"Contact ID: #{result.id} - Subcontacts Count: #{result['count']}"
end
Contacts.all.each do |contact|
puts contact.name => contact.subcontacts.count
end
OR
Contacts.all.map{|contact| [contact.name => contact.subcontacts.count]}
The above will provide you the hash like answer{contact_name => subcontacts.count}

Mass creation and association of habtm

I have 3 models (Genre, Mood, Tempo) each with a has_and_belongs_to_many association with another model (Track). The user can upload an Excel file with info for each track and I want to be able to create the respective records and add the association all at once in my update method.
I'm reading each row from the spreadsheet and creating arrays holding the genres, moods, and tempos that are to be associated with each track, but I'm not sure how to correctly create the association.
Something like:
1.upto #worksheet.last_row_index do |index|
row = #worksheet.row(index)
genres = row[6].split(", ")
genres.each do |g|
Genre.find_or_create_by_name(g) // not sure what to do here exactly
end
end
The Track ID is row[0].
I need to create each Genre if it doesn't exist, and associate that track with each. If the genre does exist, then I just need to create the association. Same for moods and tempos.
Thanks for any assistance.
This seems to do it.
1.upto #worksheet.last_row_index do |index|
row = #worksheet.row(index)
track = Track.find(row[0])
genres = row[6].split(", ")
genres.each do |g|
track.genres << Genre.find_or_create_by_name(g)
end
end

Resources