Issue constructing a has_many relation with scope using multiple join tables - ruby-on-rails

I have a tag system on multiple models that are linked together.
The system works like this:
A Top has many Middles
A Middle has many Lows
Tops, Middles and Lows have many Tags
A tag associated to Top level is supposed to qualify every Middle and Low associated to it.
Same goes for a tag that would be associated to a Middle, every Low associated to it would 'inherit' from the tags.
This mechanic is not on a database level, in the end in what concerns the database, Tops, Middles and Lows all have their own tag collection, and i initially implemented instance methods on each model so that when you call, for example, low_instance.all_tags, it concatenates the tag collections of it's parent Middles, and the one of its Top.
Here is what the models look like:
# ______________________________
# / \
# (1) (*)
# [Top] (1) __ (*) [Middle] (*) __ (*) [Low]
# (*) (*) (*)
# \_______________ | ______________/
# |
# *
# [Tags]
class Low < ApplicationRecord
has_many :low_tags, dependent: :destroy
has_many :tags, through: :low_tags
has_many :middle_foos, dependent: :destroy
has_many :middles, through: :middle_foos
end
class Middle < ApplicationRecord
belongs_to :top
has_many :middle_tags, dependent: :destroy
has_many :tags, through: :middle_tags
has_many :middle_lows, dependent: :destroy
has_many :lows, through: :middle_lows
end
class Top < ApplicationRecord
has_many :middles, dependent: :destroy
has_many :lows, dependent: :destroy
has_many :top_tags, dependent: :destroy
has_many :tags, through: :top_tags
end
### Join tables
class MiddleLow < ApplicationRecord
belongs_to :middle
belongs_to :low
end
class LowTag < ApplicationRecord
belongs_to :low
belongs_to :tag
end
class MiddleTag < ApplicationRecord
belongs_to :middle
belongs_to :tag
end
class TopTag < ApplicationRecord
belongs_to :top
belongs_to :tag
end
That actually works like a charm. The issue is that i want to be able to search my Lows with the awesome Ransack gem and using the full tag collection of a Low (its self tags, plus the ones inherited from the parent Middles and Top)
Problem: Ransack only works with ActiveRecord::Relations. So from Ransack's point of view, i can only search my Lows using their self-tags and not the full inherited collection as this does not exist on the database level.
The initial solution to this problem i wanted to implement is to add a "copy" full tag collection on the database level that updates with the rest and that i could use to search with Ransack.
But I'm sure i don't have to add anything to the database as all the info is already here in the join tables and i kind of don't want to duplicate that info which is not super cool i think and would make the code base less understandable.
I have seen potential solutions using has many with scopes like so:
has_many :all_tags, ->(low) {
unscope(.........).
left_joins(..........).
where(.........)
# Returs self tags (Low) + tags from associated Middles + tags from the Top
}
I'm pretty sure this would be the best solution, but I'm really not good when it comes to database querying especially with so much models and join tables. I get confused and can't seem to find what to put in that scope so that i get this full collection of tags.
So if anybody has a clue about that, any help would be greatly appreciated!
By the way, using Rails 6.1 and Ruby 2.7

So found the solution to the query I wanted to construct in the end.
The scope looks like following:
has_many :full_tags, lambda { |low|
where_clause = 'top_tags.top_id = ? or low_tags.low_id = ?'
where_args = [low.top_id, low.id]
if low.middles.any?
where_clause += ' or middle_tags.zone_id IN ?'
where_args << low.middle_ids
end
unscope(where: :low_id)
.left_joins(:middle_tags, :top_tags, :low_tags)
.where(where_clause, *where_args).distinct
}, class_name: 'Tag'
Calling xxx.full_tags on any instance of low returns the whole collection of tags from every middle it belongs to , plus those from the top it belongs to, plus its own tags, and the distinct makes it a unique collection.
That being said, that didn't fully fixed my problem because the whole purpose was to pass this scoped has_many relation as an attribute used by the Ransack gem to filter out my Low models, out of their full inherited collection of tags.
Big disappointment it was when i discovered that Ransack performs eager loading when it comes to search on associations:
Rails does not support Eager Loading on scoped associations
So i ended up implementing a whole other solution for my tagging system. But hey, i learned a lot.

Related

Rails multiple joins into one method

I have a user in my application that can have multiple assessments, plans, and materials. There is already a relationship between these in my database. I would like to show all these in a single tab without querying the database too many times.
I tried to do a method that joins them all in a single table but was unsuccessful. The return was the following error: undefined method 'joins' for #<User:0x007fcec9e91368>
def library
self.joins(:materials, :assessments, :plans)
end
My end goal is to just itterate over all objects returned from the join so they can be displayed rather than having three different variables that need to be queried slowing down my load times. Any idea how this is possible?
class User < ApplicationRecord
has_many :materials, dependent: :destroy
has_many :plans, dependent: :destroy
has_many :assessments, dependent: :destroy
end
class Material < ApplicationRecord
belongs_to :user
end
class Assessment < ApplicationRecord
belongs_to :user
end
class Plan < ApplicationRecord
belongs_to :user
end
If all you want to do is preload associations, use includes:
class User < ApplicationRecord
# ...
scope :with_library, -> { includes(:materials, :assessments, :plans) }
end
Use it like this:
User.with_library.find(1)
User.where(:name => "Trenton").with_library
User.all.with_library
# etc.
Once the associations are preloaded, you could use this for your library method to populate a single array with all the materials, assessments and plans of a particular user:
class User < ApplicationRecord
# ...
def library
[materials, assessments, plans].map(&:to_a).flatten(1)
end
end
Example use case:
users = User.all.with_library
users.first.library
# => [ ... ]
More info: https://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations
Prefer includes over joins unless you have a specific reason to do otherwise. includes will eliminate N+1 queries, while still constructing usable records in the associations: you can then loop through everything just as you would otherwise.
However, in this case, it sounds like you're working from a single User instance: in that case, includes (or joins) can't really help -- there are no N+1 queries to eliminate.
While it's important to avoid running queries per row you're displaying (N+1), the difference between one query and three is negligible. (It'd cost more in overhead to try to squish everything together.) For this usage, it's just unnecessary.

Ruby on Rails: Is it possible to link a column with a column on another table?

I have models with deep associations in my Ruby on Rails API, sometimes 4 associations deep. For example:
Group has_many Subgroups has_many: Posts has_many: Comments
If I want to return Group.title with my comments, I need to say:
#comment.post.subgroup.group.title
Since this is way too many queries per Comment, I have added a column to the Comment table called group_title. This property is assigned when the Comment is created. Then every time the associated Group.title is updated, I have an after_update method on the Group model to update all associated Comment group_titles.
This seems like a lot of code to me and I find myself doing this often in this large scale app. Is there a way to link these 2 properties together to automatically update Comment.group_title every time its associated Group.title is updated?
I also had a similar relation hierarchy, and solved it (maybe there are better solutions) with joins.
Quarter belongs_to Detour belongs_to Forestry belongs_to Region
For a given detour, I find region name with one query.
Quarter.select("regions.name AS region_name, forestries.name as forestry_name, \
detours.name AS detour_name, quarters.*")
.joins(detour: [forestry: :region])
Sure, you can encapsulate it in a scope.
class Quarter
...
scope :with_all_parents, -> {
select("regions.name AS region_name, forestries.name as forestry_name, \
detours.name AS detour_name, quarters.*")
.joins(detour: [forestry: :region])
}
end
You can also use same approach.
class Comment
...
scope :with_group_titles, -> {
select("groups.title AS group_title, comments.*").joins(post: [subgroup: :group])
}
end
You can build hierarchies by using indirect associations:
class Group
has_many :subgroups
has_many :posts, through: :subgroups
has_many :comments, through: :posts
end
class Subgroup
belongs_to :group
has_many :posts
has_many :comments, through: :posts
end
class Post
belongs_to :subgroup
has_one :group, through: :subgroup
has_many :comments
end
class Comment
belongs_to :post
has_one :subgroup, through: :post
has_one :group, through: :post
end
The has_many :through Association
The has_one :through Association
This allows you to go from any end and rails will handle joining for you.
For example you can do:
#comment.group.title
Or do eager loading without passing a nested hash:
#comment = Comment.eager_load(:group).find(params[:id])
This however does not completely solve the performance issues related to joining deep nested hierarchies. This will still produce a monster of a join across four tables.
If you want to cache the title on the comments table you can use an ActiveRecord callback or you can define a database trigger procedure.
class Group
after_save :update_comments!
def update_comments!
self.comments.update_all(group_title: self.title)
end
end
You can do this by updating all the Comments from one side.
class Group
after_update do
Comment.joins(post: [subgroup: :group]).where("groups.title=?", self.title).update_all(group_title: self.title)
end
end

Rails ActiveRecord includes with run-time parameter

I have a few models...
class Game < ActiveRecord::Base
belongs_to :manager, class_name: 'User'
has_many :votes
end
class Vote < ActiveRecord::Base
belongs_to :game
belongs_to :voter, class_name: 'User'
end
class User < ActiveRecord::Base
has_many :games, dependent: :destroy
has_many :votes, dependent: :destroy
end
In my controller, I have the following code...
user = User.find(params[:userId])
games = Game.includes(:manager, :votes)
I would like to add an attribute/method voted_on_by_user to game that takes a user_id parameter and returns true/false. I'm relatively new to Rails and Ruby in general so I haven't been able to come up with a clean way of accomplishing this. Ideally I'd like to avoid the N+1 queries problem of just adding something like this on my Game model...
def voted_on_by_user(user)
votes.where(voter: user).exists?
end
but I'm not savvy enough with Ruby/Rails to figure out a way to do it with just one database roundtrip. Any suggestions?
Some things I've tried/researched
Specifying conditions on Eager Loaded Associations
I'm not sure how to specify this or give the includes a different name like voted_on_by_user. This doesn't give me what I want...
Game.includes(:manager, :votes).includes(:votes).where(votes: {voter: user})
Getting clever with joins. So maybe something like...
Game.includes(:manager, :votes).joins("as voted_on_by_user LEFT OUTER JOIN votes ON votes.voter_id = #{userId}")
Since you are already includeing votes, you can just count votes using non-db operations: game.votes.select{|vote| vote.user_id == user_id}.present? does not perform any additional queries if votes is preloaded.
If you necessarily want to put the field in the query, you might try to do a LEFT JOIN and a GROUP BY in a very similar vein to your second idea (though you omitted game_id from the joins):
Game.includes(:manager, :votes).joins("LEFT OUTER JOIN votes ON votes.voter_id = #{userId} AND votes.game_id = games.id").group("games.id").select("games.*, count(votes.id) > 0 as voted_on_by_user")

Is there a Rails way or a gem to get related entries from HABTM to the same object?

Well, I have a table with images, each image is tagged with a HABTM relation through a join table.
What I want to do is show related images in the page the image is being shown, by matching images with the same tags.
Can this be done efficiently with the current join table or I should generate joins between images in another table each time a new image is created?
I recommend you consider switching to using has_many :items, :through => :link_table
It's become the new standard and it's a great approach because you get more flexibility 'baked in' and the structure is also ready for easy expansion and growth without major rework, e.g. adding new attributes is very easy. 'new attributes' are frequently date fields.
So you can add the regular timestamps fields (created and updated) and people also find other date fields like 'completed_on', 'authorized_on', 'terminated_on', 'activated_on', 'sold_on', etc. useful, depending on the application use cases.
class Image < ActiveRecord::Base
has_many :image_tags
has_many :tags, :through => :image_tags
end
class ImageTag < ActiveRecord::Base
belongs_to :image
belongs_to :tag
end
class Tag < ActiveRecord::Base
has_many :image_tags
has_many :images, :through => :image_tags
end
You will still see a lot of examples of HABTM and it does still work. There are certainly cases where it may still make sense to use it but as HMT does them anyway, KISS says use 'one way'.

What is the relationship between these two tables in RoR?

I am developing an application like the stackoverflow, which questions or articles have at less one tag. And one tags must have one or more articles.
So, I am doing this in migration in RoR. I am consider which relationship is suitable for both table. In article table, should use a "has_many", and in the tag table, should use "has_many".
But I am thinking is it necessary to add one more table in the middle, something like....
So, the first one is like that:
class Article < ActiveRecord::Base
has_many :tags
end
class Tag < ActiveRecord::Base
has_many :articles
end
or something like this:
class Article < ActiveRecord::Base
has_many :articleTagList
has_many :tags, :through => : articleTagLists
end
class Tag < ActiveRecord::Base
has_many :articleTagList
has_many :articles, :through => :articleTagLists
end
class ArticleTagList < ActiveRecord::Base
belongs_to :article
belongs_to :tag
end
Many-to-Many relationships in a normalized database will always need a third "look-up table."
If you denormalize you can get away with just having the tag id's in one field with a delimiter between them. But you also have to provide the logic to handle retrieval.
I'd personally just go with the normalized option.
If you don't want to store any information on the middle table (for example the name of the user who added tag X to the question Y), you can use the has_and_belongs_to_many:
http://apidock.com/rails/ActiveRecord/Associations/ClassMethods/has_and_belongs_to_many
If you want to store something, you need to create the middle model, as your example. In your example, the ArticleTagList model should be called ArticlesTag and the database table should be articles_tags, by convention.

Resources