Rails complex association - ruby-on-rails

I have this models:
class Product < ActiveRecord::Base
has_many :feature_products, -> { where("taxonomy_slug = feature_taxonomy_slug") }
has_many :features, through: :feature_products, class_name: "Feature", source: :feature
end
class Feature < ActiveRecord::Base
has_many :feature_products, -> (object){ where(" feature_taxonomy_slug = ?", object.taxonomy_slug) }, primary_key: :external_id
has_many :products, through: :feature_products
end
class FeatureProduct < ActiveRecord::Base
belongs_to :product
belongs_to :feature, primary_key: :external_id
end
and the tables are like this:
create_table "feature_products", force: :cascade do |t|
t.integer "product_id"
t.integer "feature_id"
t.string "feature_taxonomy_slug"
end
create_table "features", force: :cascade do |t|
t.integer "external_id"
t.string "name"
t.string "taxonomy_slug"
end
create_table "products", force: :cascade do |t|
t.integer "company_id"
end
I want to be able to create feature products association like this:
feature = Feature.create(external_id: 1234, name: 'WS', taxonomy_slug: 'prop')
Product.create(name: 'XXX', features: [feature])
The problem is the table feature_products, it stores the ids but it doesn't store the feature_taxonomy_slug from the feature. Is there any way to store it?

I think if you want to save the slug in the FeatureProduct model then you are going to have to create one explicitly, i.e.
FeatureProduct.create(product: product, feature: feature, feature_taxonomy_slug: feature.taxonomy_slug)
I do not understand why you want to save the slug in the FeatureProduct model.
Is there a reason why you do not want to access it through the product.features association?

Related

What is the best way to have an index action return all records, and all record's foreign key associations?

I have a recipes_controller that has an index action that fetches all the recipes in my database.
The problem is each recipe has_many ingredients and has_many recipe_ingredients.
The schema looks like:
create_table "recipes", force: :cascade do |t|
t.string "name"
t.string "genre"
t.bigint "user_id"
t.index ["user_id"], name: "index_recipes_on_user_id"
end
create_table "ingredients", force: :cascade do |t|
t.string "name"
t.string "food_group"
end
create_table "recipe_ingredients", force: :cascade do |t|
t.bigint "recipe_id"
t.bigint "ingredient_id"
t.integer "quantity"
t.string "measurement_unit"
t.index ["ingredient_id"], name: "index_recipe_ingredients_on_ingredient_id"
t.index ["recipe_id"], name: "index_recipe_ingredients_on_recipe_id"
end
When the index action for recipes_controller is called I would like to return a hash with all of the associations, eg:
{<Recipe id: 1, name: "rice", genre: "staples", user_id: 1, ingredients: [...ingredients], recipe_ingredients: [...recipe_ingredients]>, <Recipe id: 2, name: "toast", genre: "breakfast", user_id: 2, ingredients: [...ingredients], recipe_ingredients: [...recipe_ingredients]>}
I can essentially get this in SQL with
SELECT recipes.*,
ingredients.name,
recipe_ingredients.quantity,
recipe_ingredients.measurement_unit
FROM recipes
JOIN recipe_ingredients ON recipe_ingredients.recipe_id = recipes.id
JOIN ingredients on recipe_ingredients.ingredient_id = ingredients.id
But I'm struggling to understand how I can condense each recipe to only include it's discrete associations like in the example above, as one object that I can then display easily on the front end.
It may be important to know that the recipe_ingredients table is a join model
so my recipe and ingredient models look like:
class Recipe < ApplicationRecord
belongs_to :user
has_many :recipe_ingredients
has_many :ingredients
end
class Ingredient < ApplicationRecord
has_many :recipe_ingredients
has_many :recipes, through: :recipe_ingredients
end
The usual way to do this is use preloading, you don't have to worry about it being in a single object.
#recipes = Recipe.includes(:ingredients).where(...).order(...)
In view
#recipes.each do |recipe|
recipe.ingredients.each do |ingredient|
#...
end
end
You'll also need to do has_many :ingredients, through: :recipe_ingredients on the Recipe model.

How to set an instance of a model as an attribute of another model in Rails?

I am fairly new to Rails and working on an app that will allow a user to make a List containing their top 5 Items of a certain category. The main issue I'm having is how to keep track of the List order (which should be allowed to change and will be different for each User)?
My Items can belong to many Lists and my Lists should have many Items so, as of now, I am using a has_and_belongs_to_many association for both my Item and List models.
My idea to keep track of the list order right now is to have my #list have 5 attributes: one for each ranking on the list (ie. :first, :second, :third, :fourth, :fifth) and I am attempting to associate the #item instance to the #list attribute (ie. #list.first = #item1, #list.second = #item2 , etc...). Right now I am saving the #list attribute to the #item ID (#list.first = 1), but I would prefer to be able to call the method .first or .second etc and have that point directly at the specific Item instance.
Here is my current schema for lists, items, and the join table list_nominations required for the has_and_belongs_to_many association-which I'm pretty sure I am not utilizing correctly (the :points attribute in items will be a way of keeping track of popularity of an item:
create_table "lists", force: :cascade do |t|
t.integer "user_id"
t.integer "category_id"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.string "first"
t.string "second"
t.string "third"
t.string "fourth"
t.string "fifth"
end
create_table "items", force: :cascade do |t|
t.string "name"
t.integer "category_id"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.integer "points", default: 0
end
and here is the code currently in my List and Item models:
class List < ApplicationRecord
belongs_to :user
belongs_to :category
has_and_belongs_to_many :items
end
class Item < ApplicationRecord
belongs_to :category
has_and_belongs_to_many :lists
end
Is there a way to do this or any suggestions on a better way to keep track of the List order without creating multiple instances of the same Item?
I'm afraid your tables don't fit any known approach, you can achieve what you want but this is not a perfect nor a recommended solution, you could specify the primary key on many has_one associations inside lists but in items it's not very possible to have all lists in one association but you can have an instance method which query lists and returns the matched ones
the hacky solution:
class List < ApplicationRecord
belongs_to :user
belongs_to :category
has_one :first_item, primary_key: :first, class_name: "Item"
has_one :second_item, primary_key: :second, class_name: "Item"
has_one :third_item, primary_key: :third, class_name: "Item"
has_one :fourth_item, primary_key: :fourth, class_name: "Item"
has_one :fifth_item, primary_key: :fifth, class_name: "Item"
end
class Item < ApplicationRecord
belongs_to :category
def lists
List.where(
"first = ? OR second = ? OR third = ? OR fourth = ? OR fifth = ?", self.id, self.id, self.id, self.id, self.id
)
end
end
you can read about how to create a many-to-many relationship via has_and_belongs_to_many associations here: https://guides.rubyonrails.org/association_basics.html#the-has-and-belongs-to-many-association (your tables will need a field to properly point to each other)
What I recommend doing is following a many-to-many through relationship guide (mono-transitive association) :
you will need 1 extra table because you want to track the order(first,second, etc)
DB:
create_table "lists", force: :cascade do |t|
.. all your other fields without first,second, etc..
end
create_table "items", force: :cascade do |t|
.. all your other fields
end
create_table "lists_items", force: :cascade do |t|
t.integer "list_id"
t.integer "item_id"
t.integer "rank" there is where you will store your order (first, second ..) bas as an integer
end
Models:
class ListsItem < ApplicationRecord
belongs_to :list
belongs_to :item
end
class List < ApplicationRecord
belongs_to :user
belongs_to :category
has_many :lists_items, -> { order(:rank) }, limit: 5
has_many :items, through: :lists_items
end
class Item < ApplicationRecord
belongs_to :category
has_many :lists_items
has_many :lists, through: :lists_items
end
you can read more about many-to-many via has_many through here https://guides.rubyonrails.org/association_basics.html#the-has-many-through-association
and the difference between the 2 approaches here https://guides.rubyonrails.org/association_basics.html#choosing-between-has-many-through-and-has-and-belongs-to-many

Find all records whose association has a nil association in turn

Given the models below:
class Score < ApplicationRecord
belongs_to :composition
end
class Composition < ApplicationRecord
has_many :scores
has_one :invoice, dependent: :destroy
end
class Invoice < ApplicationRecord
belongs_to :composition
end
what's the best way to find the scores, whose composition has a nil invoice?
I tried:
Score.joins(:composition).where(composition: {invoice: nil})
csn = Composition.includes(:invoice).where(invoices:{id:nil})
Score.where(csn.include? composition)
Score.where(csn.map(&:id).include? composition_id)
Score.where(Composition.left_outer_joins(:invoice).where(invoices:{id:nil}).includes? composition)
all with errors. Any ideas?
EDIT: here are the corresponding tables as per schema.rb:
create_table "compositions", force: :cascade do |t|
...
end
create_table "invoices", force: :cascade do |t|
t.integer "composition_id"
...
t.index ["composition_id"], name: "index_invoices_on_composition_id", using: :btree
end
create_table "scores", force: :cascade do |t|
t.integer "composition_id", null: false
...
end
Please try following query:
Score.joins(:composition).includes(composition: : invoice).where(invoices: { id: nil })
Try following
Score.joins(:composition).where('compositions.invoice_id IS NULL')
above should work, Have a good luck!!!
Please try following query,
Score.joins(:composition).where('compositions.id NOT IN (?)', Invoice.pluck(:composition_id))
If this not work then let me know the columns present in compositions and scores tables

rails admin has_many through relationship leading to errors

I am using rails_admin gem for admin interface.
I have a has_many through relationship which doesn't seem to work with rails admin.
class Company < ActiveRecord::Base
has_many :talent_infos, class_name: 'CompanyTalentInfo'
has_many :talents, through: :talent_infos
end
class CompanyTalentInfo < ActiveRecord::Base
belongs_to :company
belongs_to :talent
end
class Talent < ActiveRecord::Base
has_many :talent_infos, class_name: 'CompanyTalentInfo'
has_many :companies, through: :talent_infos
end
I get error every time I try to create a new company and my guess is that its the first time when rails_admin tries to check the relationships and it doesn't accept my current associations.
The error I get is this file gems/rails_admin-0.7.0/app/views/rails_admin/main/_form_filtering_multiselect.html.haml:21
21 controller.list_entries(config, :index, field.associated_collection_scope, false).map { |o| [o.send(field.associated _object_label_method), o.send(field.associated_primary_key)] }.sort_by {|a| [selected_ids.index(a[1]) || selected_ids.si ze, i+=1] }
I get this error
undefined method `klass' for nil:NilClass`
Can anyone help me with this association how can I fix it.
perhaps a late response, but could you compare the relevant part of your database schema with the following and let me know the difference? I believe the relations are setup correctly so that should be the problem.
create_table "companies", force: :cascade do |t|
t.string "name"
end
create_table "company_talent_infos", force: :cascade do |t|
t.string "metadata"
t.integer "company_id"
t.integer "talent_id"
end
create_table "talents", force: :cascade do |t|
t.string "name"
end

has_and_belongs_to_many UndefinedTable: ERROR

I have two models, Clinician and Patient. A clinician has_many: patients and a patient belongs_to :clinician. A join table, shared_patients is meant to store additional associations between patients and clinicians as a patient can be shared by many other clinicians besides the one it belongs_to. This is done using a has_and_belongs_to_many relationship.
See models:
class Clinician < ActiveRecord::Base
has_many :patients
has_and_belongs_to_many :shared_patients, join_table: 'shared_patients', class_name: 'Patient'
end
class Patient < ActiveRecord::Base
belongs_to :clinician
has_and_belongs_to_many :shared_clinicians, join_table: 'shared_patients', class_name: 'Clinician'
end
This is how my tables are set out in the db schema:
create_table "clinicians", force: true do |t|
t.string "first_name"
t.string "last_name"
t.integer "user_id"
end
create_table "patients", force: true do |t|
t.integer "clinician_id"
t.string "first_name"
t.string "last_name"
t.integer "user_id"
end
create_table "shared_patients", id: false, force: true do |t|
t.integer "clinician_id"
t.integer "patient_id"
end
Using these I would like to show the list of clinicians that a patient is shared with.
Right now I am getting an error:
PG::UndefinedTable: ERROR: relation "shared_patients" does not exist
LINE 1: INSERT INTO "shared_patients" ("clinician_id", "id", "patien...
If I try and create a relationship in console:
#shared = SharedPatient.new("id"=>1, "clinician_id"=>2526, "patient_id"=>1307)
=> #1, "clinician_id"=>2526, "patient_id"=>1307}>
#shared.save
Any advice on solving this error or on structuring the models to get the associations I want would be great. Thanks
When you have a has_and_belongs_to_many then you cannot have a class for the join table, ie. you can't have SharedPatient and you can't try using it as you've done.

Resources