Need help constructing a query - ruby-on-rails

I've a model that has a nested model of skills. Its a common has_many example. Elastic search is indexing the skills as an array of strings.
My question is, I am attempting to match on those skills by way of two different inputs.
Required skills and bonus skills.
So if I have two query terms one for required and one for bonus, I want to query the skills attribute with required input, if none found, query with the bonus input.
I'm using elasticsearch-rails gem. Didn't think I needed to post any code as this is more theory.
UPDATE
class Profile
has_many :skills
...
end
class Skill
belongs_to :profile
end
Mappings
settings index: { number_of_shards: 1, number_of_replicas: 0 } do
...
mapping dynamic: 'false' do
indexes :skills, analyzer: 'keyword'
end
...
end
Overriden as_json
def as_indexed_json(options={})
hash = self.as_json(
include: {location: { methods: [:coordinates], only: [:coordinates] },
locations_of_interest: { methods: [:coordinates], only: [:coordinates]}
})
hash['skills'] = self.skills.map(&:name)
hash['interests'] = self.interests.map(&:name)
hash
end
I guess in essence i'm looking to perform the reverse of a multi_match on multiple fields and boosting one but instead searching one field with multiple inputs (required and bonus) and depending no the results of required search with bonus input. Does this makes things more clear?
This is my query so far, first attempt.
if options[:required_skills].present? && options[:bonus_skills].present?
bool do
must do
term skills: options[:required_skills]
end
should do
term skills: options[:bonus_skills]
end
end
end

class SkillContainer < ActiveRecord::Base
has_many :skill_links, dependent: :destroy, inverse_of: :skill_container
has_many :skills, through: :skill_links
has_many
end
##################################
create_table :skill_link do |t|
t.references :skill
t.references :skill_container
t.boolean :required
t.boolean :bonus
end
##################################
class SkillLink
belongs_to :skill_container
belongs_to :skill
scope :required, -> {
where(required: true)
}
scope :bonus, -> {
where(bonus: true)
}
end
class Skill < ActiveRecord::Base
has_many :skill_links, dependent: :destroy, inverse_of: :skill
has_many :skills, through: :skill_links
end
#required skills from any skill container
SkillContainer.last.skills.merge(SkillLink.required)
#bonus skills from any skill container
SkillContainer.last.skills.merge(SkillLink.bonus)
scopes can be combined with your elastic search

Related

has_many through => find not matching records

I want to be able to find unpopulated hives, but don't find any solution.
Can you help me please ?
The goal is to be able to do Hive.unpopulated
The main problem is the most_recent, butins ok for me to work with a raw SQL, but I don't find the right query.
Here are my classes :
class Hive < ApplicationRecord
has_many :moves, dependent: :destroy
has_many :yards, through: :moves
has_many :populations, -> { where(:most_recent => true) }
has_many :colonies, through: :populations
validates :name, uniqueness: true
def hive_with_colony
"#{name} (colony #{if self.colonies.count > 0 then self.colonies.last.id end})"
end
def self.populated
Hive.joins(:populations)
end
def self.unpopulated
end
end
class Population < ApplicationRecord
belongs_to :hive
belongs_to :colony
after_create :mark_most_recent
before_create :mark_end
class Colony < ApplicationRecord
has_many :populations, -> { where(:most_recent => true) }
has_many :hives, through: :populations
has_many :visits
has_many :varroas
has_many :most_recents_populations, -> { where(:most_recent => true) }, :class_name => 'Population'
scope :last_population_completed, -> { joins(:populations).where('populations.most_recent=?', true)}
I think you can do a simple query to select Hives which are not in populated list, so:
def self.unpopulated
where.not(id: populated.select(:id))
end
Another option is a LEFT OUTER JOIN and picking the lines that have no population id set on the right side.
def self.unpopulated
left_outer_joins(:populations).where(populations: { id: nil })
end
It depends on your data if Thanh's version (which compares a potentially huge list of ids) or this version (which makes a sightly more complex join but doesn't need to compare against a list of ids) is more performant.

How to remove N+1 queries

I have a rails API that currently has quite a few N+1 queries that I'd like to reduce.
As you can see it's going through quite a few loops before returning the data.
The relationships are as follows:
Company Model
class Company < ApplicationRecord
has_many :jobs, dependent: :destroy
has_many :contacts, dependent: :destroy
has_many :listings
end
Job Model
class Job < ApplicationRecord
belongs_to :company
has_many :listings
has_and_belongs_to_many :technologies
has_and_belongs_to_many :tools
scope :category, -> ( category ) { where category: category }
end
Listing Modal
class Listing < ApplicationRecord
belongs_to :job, dependent: :destroy
belongs_to :company, dependent: :destroy
scope :is_active, -> ( active ) { where is_active: active }
end
Job Serializer
class SimpleJobSerializer < ActiveModel::Serializer
attributes :id,
:title,
:company_name,
attribute :technology_list, if: :technologies_exist
attribute :tool_list, if: :tools_exist
def technology_list
custom_technologies = []
object.technologies.each do |technology|
custom_technology = { label: technology.label, icon: technology.icon }
custom_technologies.push(custom_technology)
end
return custom_technologies
end
def tool_list
custom_tools = []
object.tools.each do |tool|
custom_tool = { label: tool.label, icon: tool.icon }
custom_tools.push(custom_tool)
end
return custom_tools
end
def tools_exist
return object.tools.any?
end
def technologies_exist
return object.technologies.any?
end
def company_name
object.company.name
end
end
Current query in controller
Job.eager_load(:listings).order("listings.live_date DESC").where(category: "developer", listings: { is_active: true }).first(90)
I've tried to use eager_load to join the listings to the Jobs to make the request more efficient but i'm unsure how to handle this when some of the n+1 queries are coming from inside the serializer as it tries to look at tools and technologies.
Any help would be much appreciated!
You might was well eager load tools and technologies since you know that the serializer is going to use them:
Job.eager_load(:listings, :tools, :technologies)
.order("listings.live_date DESC")
.where(category: "developer", listings: { is_active: true })
.first(90)
After that you really need to refactor that serializer. #each should only be used when you are only interested in the side effects of the iteration and not the return value. Use #map, #each_with_object, #inject etc. These calls can be optimized. return is implicit in ruby so you only explicitly return if you are bailing early.
class SimpleJobSerializer < ActiveModel::Serializer
# ...
def tool_list
object.tools.map { |t| { label: tool.label, icon: tool.icon } }
end
# ...
end
Try nested preload:
Job.preload(:technologies, :tools, company: :listings).order(...).where(...)

Rails 5: How to allow model create through when polymorphic reference also carries distinct association

I have model with polymorhphic reference to two other models. I've also included distinct references per this article eager load polymorphic so I can still do model-specific queries as part of my .where clause. My queries work so I can search for scores doing Score.where(athlete: {foo}), however, when I try to do a .create, I get an error because the distinct reference alias seems to be blinding Rails of my polymorphic reference during validation.
Given that athletes can compete individually and as part of a team:
class Athlete < ApplicationRecord
has_many :scores, as: :scoreable, dependent: :destroy
end
class Team < ApplicationRecord
has_many :scores, as: :scoreable, dependent: :destroy
end
class Score < ApplicationRecord
belongs_to :scoreable, polymorphic: true
belongs_to :athlete, -> { where(scores: {scoreable_type: 'Athlete'}) }, foreign_key: 'scoreable_id'
belongs_to :team, -> { where(scores: {scoreable_type: 'Team'}) }, foreign_key: 'scoreable_id'
def athlete
return unless scoreable_type == "Athlete"
super
end
def team
return unless scoreable_type == "Team"
super
end
end
When I try to do:
Athlete.first.scores.create(score: 5)
...or...
Score.create(score: 5, scoreable_id: Athlete.first.id, scoreable_type: "Athlete")
I get the error:
ActiveRecord::StatementInvalid (SQLite3::SQLException: no such column: scores.scoreable_type
Thanks!
#blazpie, using your scoping suggestion worked for me.
"those scoped belongs_to can be easily substituted by scopes in Score: scope :for_teams, -> { where(scorable_type: 'Team') }

How can I name the relationship between two records?

Let's say I have a model Movie. Movies can have_many of each other through an intermediary model AssociatedMovie.
How can I specify the nature of the relationship between two Movies? For any given pair of Movies, the relationship may be prequel/sequel, or remake/original, or inspired/inspired by, or related/related, etc. Right now, I can't give the relationships names.
Here's my schema and associations:
create_table "movies", force: true do |t|
t.string "title"
end
create_table "associated_movies", force: true do |t|
t.integer "movie_a_id"
t.integer "movie_b_id"
end
class Movie < ActiveRecord::Base
has_many :movies, :through => :associated_movies
end
class AssociatedMovie < ActiveRecord::Base
has_many :movies
end
And here's the query for setting each Movie's associated Movies:
def movie_associated_movies
associated_movie_ids = AssociatedMovie.
where("movie_a_id = ? OR movie_b_id = ?", self.id, self.id).
map { |r| [r.movie_a_id, r.movie_b_id] }.
flatten - [self.id]
Movie.where(id: associated_movie_ids)
end
I think I'd probably have to add movie_a_type and movie_b_type attributes to AssociatedMovie. But I'm not sure how I could specify which Movie is attached to which type.
Anyone have any ideas?
You're already half-way there with has_many :through (using an intermediary model) - this allows you to add as many extra attributes as you like.
I think your problem is down to your relationships, which I'll explain below:
#app/models/movie.rb
class Movie < ActiveRecord::Base
has_many :associated_movies, foreign_key: :movie_a_id
has_many :movies, through: :associated_movies, foreign_key: :movie_b_id
end
#app/models/associated_movie.rb
class AssociatedMovie < ActiveRecord::Base
belongs_to :movie_a, class_name: "Movie"
belongs_to :movie_b, class_name: "Movie"
end
The above will give you access to:
#movie = Movie.find params[:id]
#movie.associated_movies #-> collection of records with movie_a and movie_b
#movie.movies #-> all the movie_b objects
--
Because you're using has_many :through, rather than has_and_belongs_to_many, you'll be at liberty to add as many attributes to your join model as you need:
To do this, you just have to add a migration:
$ rails g migration AddNewAttributes
#db/migrate/add_new_attributes_________.rb
class AddNewAttributes < ActiveRecord::Migration
def change
add_column :associated_movies, :relationship_id, :id
end
end
$ rake db:migrate
-
... I apologize if this is a little off-course; however I would actually add a separate model for your relationships (considering you have them predefined):
#app/models/relationship.rb
class Relationship < ActiveRecord::Base
#columns id | movie_a_type | movie_b_type | created_at | updated_at
has_many :associated_movies
end
#app/models/associated_movie.rb
class AssociatedMovie < ActiveRecord::Base
belongs_to :movie_a, class_name: "Movie"
belongs_to :movie_b, class_name: "Movie"
belongs_to :relationship
delegate :movie_a_type, :movie_b_type, to: :relationship
end
This may seem a little bloated (it is), but it will provide extensibility.
You'll have to add another table, but it will ultimately provide you with the ability to call the following:
#movie.associated_movies.each do |associated|
associated.movie_a #-> current movie
associated.movie_b #-> related movie
associated.movie_a_type #-> "Original"
associated.movie_b_type #-> "Sequel"
end
You'd then be able to pre-populate the Relationship model with the various relationships you'll have.
I can add to the answer as required.

Multiple counter_cache in Rails model

I'm learning Rails, and got into a little problem. I'm writing dead simple app with lists of tasks, so models look something like that:
class List < ActiveRecord::Base
has_many :tasks
has_many :undone_tasks, :class_name => 'Task',
:foreign_key => 'task_id',
:conditions => 'done = false'
# ... some validations
end
Table for List model has columns tasks_counter and undone_tasks_counter.
class Task < ActiveRecord::Base
belongs_to :list, :counter_cache => true
# .. some validations
end
With such code there is attr_readonly :tasks_counter for List instances but I would like to have a counter for undone tasks as well. Is there any way of having multiple counter cached automagically by Rails.
So far, I've managed to create TasksObserver that increments or decrements Task#undone_tasks_counter, but maybe there is a simpler way.
Have you tried it with a custom-counter-cache column?
The doc here:
http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html
It suggests that you can pass a column-name to the counter_cache option, which you may well be able to call twice eg
belongs_to :list, :counter_cache => true # will setup tasks_count
belongs_to :list, :counter_cache => :undone_tasks_count
Note: not actually tested.
ez way.
1) first counter - will do automatically
2) Manually "correct"
AnotherModelHere
belongs_to :user, counter_cache: :first_friends_count
after_create :provide_correct_create_counter_2
after_destroy :provide_correct_destroy_counter_2
def provide_correct_create_counter_2
User.increment_counter(:second_friends_count, another_user.id)
end
def provide_correct_destroy_counter_2
User.decrement_counter(:second_friends_count, another_user.id)
end
Most probably you will need counter_culture gem, as it can handle counters with custom conditions and will update counter value not only on create and destroy, but for updates too:
class CreateContainers < ActiveRecord::Migration[5.0]
create_table :containers, comment: 'Our awesome containers' do |t|
t.integer :items_count, default: 0, null: false, comment: 'Caching counter for total items'
t.integer :loaded_items_count, default: 0, null: false, comment: 'Caching counter for loaded items'
end
end
class Container < ApplicationRecord
has_many :items, inverse_of: :container
has_many :loaded_items, -> { where.not(loaded_at: nil) },
class_name: 'Item',
counter_cache: :loaded_items_count
# Notice that you can specify custom counter cache column name
# in has_many definition and AR will use it!
end
class Item < ApplicationRecord
belongs_to :container, inverse_of: :items, counter_cache: true
counter_culture :container, column_name: proc { |model| model.loaded_at.present? ? 'loaded_items_count' : nil }
# But this column value will be handled by counter_culture gem
end
I'm not aware of any "automagical" method for this. Observers seems good for this, but I personally prefer using callbacks in model (before_save, after_save).

Resources