Rails concatenate sql columns and create one to many relationship - sql

I've got two models: Building and BuildingInfo. I want to relate the two tables using two columns townhall_level and name.
Ideally it will work like the following: Building.first.building_info For instance Building.first.townhall_level => 5 and Building.first.name => cannon, Building.first.building_info would access BuildingInfo.where(townhall_level: 5, name:"cannon".
What's the best way to do this? Can I create a third column which concatenates name and townhall_level? Could I also use that column to create the belongs_to and has_many relationship?

Simple and straightforward:
class Building < ActiveRecord::Base
def building_info
BuildingInfo.find_by(townhall_level: townhall_level, name: name)
end
end
It will be nil if nothing is found, and will return only the first record even if multiples are found. I also highly suggest that you add an index to the two columns through a migration:
add_index :building_infos, [:townhall_level, :name], name: 'building_infos_level_and_name'
Which will speed up searching, if you were concerned about performance.

mmm...I'm not sure this will work but you can do something like
class Building < ActiveRecord::Base
def self.bulding_info
BuildingInfo.find_by(townhall_level: townhall_level, name: name)
end
end
but I would really suggest you to put a building_info_id in the Building model and have a
class Building < ActiveRecord::Base
belongs_to :bulding_info
end

Related

How do you query 3 different models in Rails w/ PostgreSQL?

I need to write a query in Rails that involves 3 different models. I need to know which Subscriptions are delivereable. But delivereable is not a column in Subscription but in BasePlan.
class BasePlan
has_many :plans
end
class Plan
has_many :subscriptions
end
class Subscription
belongs_to :plan
end
I've tried joining all three models together to no success:
Subscription.joins(:plans).joins(:base_plans).where(queried_column: true)
What would be the right way to write the query?
Subscription needs to know about it's relationship to :base_plans for your code to work.
class Subscription
belongs_to :plan
has_one :base_plan, through: :plan
end
Using #SteveTurczyn 's answer, you could query like this:
Subscription.joins(:base_plan).where(base_plans: { queried_column: true }). You need to first define the through relationship as Steve suggests, then you can traverse the relationship to get the column you need to check on BasePlan.

Rails query with multiple conditions on the same field

I have two models, Recipes and Skills. In this scenario, a skill is a cooking technique, like baking, frying, etc. So each recipe has a certain set of associated skills.
I want to search all the recipes like this:
Find all recipes that use any given set of skills (e.g. Baking OR Frying OR both)
EDIT: This should NOT return recipes that require a skill that wasn't in the search query - e.g. If I search for skills [1, 2] I don't want a recipe that uses skills [1, 2, 4] or any other superset.
If you add a new skill to the search, return just the additional recipes (e.g. if you add Boiling to the previous query of Baking or Frying, how many NEW recipes can you now cook?)
I currently have this working in Rails using plain old Ruby methods:
class Recipe < ActiveRecord::Base
has_many :practices
has_many :skills, through: :practices
def self.find_recipes_that_require_any_of_these_skills(*known_skill_ids)
self.select do |recipe|
recipe.skill_ids.all? do |skill_id|
known_skill_ids.include?(skill_id)
end
end
end
# calls the above method twice, once with the new skill and once without
# and subtracts one result from the other
def self.find_newly_unlocked_recipes(*prior_skill_ids, new_skill_id)
self.find_recipes_that_require_any_of_these_skills(*(prior_skill_ids + [new_skill_id])) - self.find_recipes_that_require_any_of_these_skills(*prior_skill_ids)
end
end
In Rails console: Recipe.find_recipes_that_require_any_of_these_skills(1,4)
returns all the Recipe objects for skill 1, skill 4, or skills 1 & 4.
But this is inefficient because it generates a SQL query for every single recipe in my database.
How can I write these queries the ActiveRecord/SQL way?
def self.find_recipes_that_require_any_of_these_skills(*known_skill_ids)
self.includes(:skills).where(skills: { id: known_skill_ids })
end
Two queries to DB:
def self.find_recipes_that_require_any_of_these_skills(*known_skill_ids)
Recipe.joins(:skills)
.merge(Skill.where(id: known_skill_ids))
.where("recipes.id NOT IN (?)", Recipe.joins(:skills).merge(Skill.where("skills.id NOT IN (?)", known_skill_ids)).uniq.pluck(:id)).uniq
end
Since you're using has_many :skills, through: :practices - your Practices table should have both recipe_id and skills_id columns.
previous_recipe_ids = Recipe.joins(:practices).where('practices.skill_id in (?)', prior_skills_ids).map(&:id)
Recipe.joins(:practices).where('practices.skill_id = (?) and recipes.recipe_id not in (?)', new_skill_id, previous_recipe_ids)
The following method uses just three sql queries in total to create a collection of recipies
array_of_skills=["cooking","frying",...]
skills=Skill.where('name in (?)',array_of_skills).map(&:id)
Up to here you already have, so you might need just this:
practices=Practice.where('skill_id in (?)',skills).map(&:recipe_id)
recipes=Recipe.where('id in (?)', practices)
Maybe there is a better way, but I don't think there would be something with much less sql
It seems to be that your find_recipes_that_require_any_of_these_skills method isn't correct. It returns recipes that have all of the known_skill, not any.
So, the ActiveRecord/SQL way:
Known skills:
class Skill < ActiveRecord::Base
#known skills
scope :known_skills, -> { where(id: known_skill_ids) }
#not known skills
scope :not_known_skills, -> { where("skills.id NOT IN (?)", known_skill_ids) }
Recipes that have any of known skills:
Recipe.joins(:skills).merge(Skill.known_skills)
Newly_unlocked_recipes:
Recipe.joins(:skills).merge(Skill.not_known_skills).where("skills.id = ?", new_skill_id)

Rails4 query help, find unique records with has_many though and a joining model

I have the following table structure
manufacturers --> products ---> available_sizes_products <-- sizes
and the following models
class Manufacturer < ActiveRecord::Base
has_many :products
end
class Product < ActiveRecord::Base
has_many :sizes, :through => :available_sizes_products
has_many :available_sizes_products
end
class AvailableProductSize < ActiveRecord::Base
belongs_to :sizes
belongs_to :products
end
class Size < ActiveRecord::Base
has_many :products, :through => :available_sizes_products
has_many :available_sizes_products
end
I need to get a unique list of manufacturers, that have products in size "XL" or "L" for example.I'm getting lost in the chaining of joins etc.
class Manufacturer < ActiveRecord::Base
def self.with_sizes(sizes=[])
#sizes = Sizes.find(sizes)
...
end
end
Can someone help me with that ? Trying to do the Rails 4 way rather than drop down to SQL, since I need the query to run on several DBS
Thanks
First of all you have to use single form of noun in belongs_to expression.
And for the query try this one:
Manufacturer.includes(:products).where(products: (size: "XL"))
I use "includes" to avoid N+1 query. Otherwise it will send two queries: one for Manufacturers and one for products. Write back, if this one doesn't fit your need.
EDIT
BTW, if you want to use exactly joining, write joins instead of includes.
Everything is here:
http://guides.rubyonrails.org/active_record_querying.html#joining-tables
and here:
http://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations
After going through the docs for joins
this is what worked :
Manufacturer.joins(products: :sizes).where(sizes: {id:ids}).distinct
This Rails way returns the model correctly.

Retrieving sublist 3 level deep in Rails

I have a datamodel that contains a Project, which contains a list of Suggestions, and each Suggestion is created by a User. Is there a way that I can create a list of all distinct Users that made Suggestions within a Project?
I'm using Mongoid 3. I was thinking something like this, but it doesn't work:
#project = Project.find(params[:id])
#users = Array.new
#users.push(#project.suggestions.user) <-- this doesn't work
Any ideas? Here's my model structure:
class Project
include Mongoid::Document
has_many :suggestions, :dependent => :destroy
...
end
class Suggestion
include Mongoid::Document
belongs_to :author, class_name: "User", :inverse_of => :suggestions
belongs_to :project
...
end
class User
include Mongoid::Document
has_many :suggestions, :inverse_of => :author
...
end
While Mongoid can give MongoDB the semblance of relationships, and MongoDB can hold foreign key fields, there's no underlying support for these relationships. Here are a few options that might help you get the solution you were looking for:
Option 1: Denormalize the data relevant to your patterns of access
In other words, duplicate some of the data to help you make your frequent types of queries efficient. You could do this in one of a few ways.
One way would be to add a new array field to User perhaps called suggested_project_ids. You could alternatively add a new array field to Project called suggesting_user_ids. In either case, you would have to make sure you update this array of ObjectIds whenever a Suggestion is made. MongoDB makes this easier with $addToSet. Querying from Mongoid then looks something like this:
User.where(suggested_project_ids: some_project_id)
Option 2: Denormalize the data (similar to Option 1), but let Mongoid manage the relationships
class Project
has_and_belongs_to_many :suggesting_users, class_name: "User", inverse_of: :suggested_projects
end
class User
has_and_belongs_to_many :suggested_projects, class_name: "Project", inverse_of: :suggesting_users
end
From here, you would still need to manage the addition of suggesting users to the projects when new suggestions are made, but you can do so with the objects themselves. Mongoid will handle the set logic under the hood. Afterwards, finding the unique set of users making suggestions on projects looks like this:
some_project.suggesting_users
Option 3: Perform two queries to get your result
Depending on the number of users that make suggestions on each project, you might be able to get away without performing any denormalization, but instead just make two queries.
First, get the list of user ids that made suggestions on a project.
author_ids = some_project.suggestions.map(&:author_id)
users = User.find(author_ids)
In your Project class add this :
has_many :users, :through => :suggestions
You'll then be able to do :
#users.push(#project.users)
More info on :through here :
http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#method-i-has_many
For mongoid, take a look at this answer :
How to implement has_many :through relationships with Mongoid and mongodb?

Help with Rails find_by queries

Say if #news_writers is an array of records. I then want to use #news_writers to find all news items that are written by all the news writers contained in #news_writers.
So I want something like this (but this is syntactically incorrect):
#news = News.find_all_by_role_id(#news_writers.id)
Note that
class Role < ActiveRecord::Base
has_many :news
end
and
class News < ActiveRecord::Base
belongs_to :role
end
Like ennen, I'm unsure what relationships your models are supposed to have. But in general, you can find all models with a column value from a given set like this:
News.all(:conditions => {:role_id => #news_writers.map(&:id)})
This will create a SQL query with a where condition like:
WHERE role_id IN (1, 10, 13, ...)
where the integers are the ids of the #news_writers.
I'm not sure if I understand you - #news_writers is a collection of Role models? If that assumption is correct, your association appears to be backwards - if these represent authors of news items, shouldn't News belong_to Role (being the author)?
At any rate, I would assume the most direct approach would be to use an iterator over #news_writers, calling on the association for each news_writer (like news_writer.news) in turn and pushing it into a separate variable.
Edit: Daniel Lucraft's suggestion is a much more elegant solution than the above.