I have three tables, one of which is a join table between the other two tables.
Jobs: id
Counties: id
Countyizations: job_ib, county_id
I want to create a list of counties a specific job has associations with. I'm trying to use something like:
<%= #counties.map { |county| county.id }.join(", ") %>
But this obviously is not using the countyizations table. How can I change the above code to accomplish what I need? Also, I'd like to list the counties alphabetically in ASC order.
P.S.
I suppose I should have added how I'm linking my tables in my models.
Jobs: has_many :countyizations & has_many :counties, :through => :countyizations
Counties: has_many :countyizations & has_many :jobs, :through => :countyizations
Countyizations: belongs_to :county & belongs_to :job
For a given job.id you can use this this return all the counties filtered by the given job.
<%= #counties.order('name asc').includes(:jobs).where('jobs.id = ?', job.id) %>
Replace job.id based on your requirement, you could set a #job instance variable in the controller and use in the view instead.
Or even better move this code to controller action:
# controller
def show
job_name = ...
#counties = ...
#county_jobs = #counties.order('name asc').includes(:jobs).where(jobs.name = ?', job_name)
end
Then in your view, to show all the counties that have the searched job:
<%= #counties.map(&:id).join.(',') %>
I am not sure if I understand you correctly. Is the following what you want?
class Job < ActiveRecord::Base
has_many :countries, :through => :countyizations
end
class County < ActiveRecord::Base
has_many :jobs, :through => :countyizations
end
<%= #job.counties.sort{|a, b| a.name <=> b.name}.map{ |county| county.name }.join(", ") %>
I think use "has_many_and_belongs_to" instead "of has_many" may work also.
Related
I am looking for a way to show a count of how many images there are for a category but obtained through a has_many association. I have been reading a little on counter_cache but as yet no joy on an implementation
class Category < ActiveRecord::Base
has_many :image_categories
has_many :images, through: :image_categories
end
class ImageCategory < ActiveRecord::Base
# Holds image_id and category_id to allow multiple categories to be saved per image, as opposed to storing an array of objects in one DB column
belongs_to :image
belongs_to :category
end
class Image < ActiveRecord::Base
# Categories
has_many :image_categories, dependent: :destroy
has_many :categories, through: :image_categories
end
Controller
#categories = Category.all
View
<% #categories.each do |c| %>
<li>
<%= link_to '#', data: { :filter => '.' + c.name.delete(' ') } do %>
<%= c.name %> (<%= #count here %>)
<% end %>
</li>
<% end %>
A couple important things to consider with counter_cache:
Certain Rails methods can update the database while bypassing callbacks (for instance update_column, update_all, increment, decrement, delete_all, etc.) and can cause inconsistent values for a counter cache. Same applies to any database changes outside of Rails.
Creating/deleting a child model always requires updating the parent. To ensure consistency of the counter cache Rails uses an additional DB transaction during this update. This usually isn't a problem but can cause database deadlocks if your child model is created/deleted frequently, or if the parent model is updated frequently. (http://building.wanelo.com/2014/06/20/counter-cache-a-story-of-counting.html)
These problems will be exacerbated since you're using a counter cache across a join table.
If you want to do an efficient dynamic count, that's always up to date, then you can use a custom select with a grouped join:
#categories = Category.select("categories.*, COUNT(DISTINCT images.id) AS images_count").joins(:images).group("categories.id")
<% #categories.find_each do |c| %>
<li>
<%= link_to '#', data: { :filter => '.' + c.name.delete(' ') } do %>
<%= c.name %> (<%= c.images_count # <- dynamic count column %>)
<% end %>
</li>
<% end %>
The cost of this grouped join should be very small provided your foreign keys are indexed, and I'd strongly consider taking this approach if you need images_count to always be consistent with the true value, or if images are frequently being created or destroyed. This approach may also be easier to maintain in the long run.
Since you are looking for an efficient way, i would suggest using counter_cache
Here is how your models should look like:
class Category < ActiveRecord::Base
has_many :image_categories
has_many :images, through: :image_categories
end
class ImageCategory < ActiveRecord::Base
# Holds image_id and category_id to allow multiple categories to be saved per image, as opposed to storing an array of objects in one DB column
belongs_to :image, counter_cache: :category_count
belongs_to :category, counter_cache: :image_count
end
class Image < ActiveRecord::Base
# Categories
has_many :image_categories, dependent: :destroy
has_many :categories, through: :image_categories
end
You'll need to add image_count field to your categories table and category_count in images table.
Once you are done adding the counters and fields, you'd need to reset the counters so that the fields are updated with the correct count values for the records already present in your db.
Category.find_each { |category| Category.reset_counters(category.id, :images) }
Image.find_each { |image| Image.reset_counters(image.id, :categories) }
I have a self nested category model: which has_many and belongs_to it self
class Category < ActiveRecord::Base
has_many :subcategories, class_name: "Category", foreign_key: "parent_id", dependent: :destroy
belongs_to :parent_category, class_name: "Category", foreign_key: "parent_id"
end
In the view I want to display not only the #category.subcategories.count but the count of all nested subcategories
how would I get that?
~~~ UPDATE: ~~~
In the categories controller I get the current category from the parameters like:
def show
#category = Category.find(params[:id])
end
now I want use in the view (but the following example doesn't give me all nested subcategories back)
<div>
<%= #category.name %> has <%= #category.subcategories.count %> subcategories in total
</div>
create a recursive model method...
def deep_count
count = subcategories.count
subcategories.each { |subcategory| count += subcategory.deep_count }
count
end
If in your design it's possible for a child to be the parent of an ancestor
(e.g. "4x4" -> "Jeep" - > "SUV" -> "4x4" -> ...)
Then you could end up with a stack overflow. To avoid that you can track categories to ensure you don't deep_count them twice...
def deep_count(seen_ids=[])
seen_ids << id
count = subcategories.count
subcategories.where("id NOT IN (?)", seen_ids).each do |subcategory|
count += subcategory.deep_count(seen_ids)
end
count
end
As an addition to #SteveTurczyn's epic answer, you may wish to look at using one of the hierarchy gems (we use acts_as_tree).
Not only will this extract your has_many :subcategories association, but provides a myriad of functionality to allow you to better handle nested objects.
#app/models/category.rb
class Category < ActiveRecord::Base
acts_as_tree order: "name"
end
This will allow you to use the following:
#category = Category.find x
#category.children #-> collection of subcategories
#category.parent #-> Category record for "parent"
#category.children.create name: "Test" #-> creates new subcategory called "test"
Because acts_as_tree uses parent_id, you wouldn't have to change anything in your database.
--
You'd still be able to use the deep_count method:
#app/models/category.rb
class Category < ActiveRecord::Base
acts_as_tree order: "name"
def deep_count
count = children.count
children.each {|child| count += child.deep_count }
count
end
end
I'm sure there must be a way to count the "children" but I've not got any code at hand for it.
The main benefit of it is the recursion with displaying your categories. For example, if a Post has_many :categories:
#app/views/posts/index.html.erb
<%= render #post.categories %>
#app/views/categories/_category.html.erb
<%= category.name %>
Subcategories:
<%= render category.children if category.children.any? %>
--
Just seems a lot cleaner than two ActiveRecord associations.
I am trying to refine my article and giving my user flexibility to decide what they want to view.
Here the models with relationship
Article
has_many :tags, through: :articletags
ArticleTags
belongs_to :article
belongs_to :tags
Tags
has_many :article, through: articletags
Now the idea is the use would go in article and on the side see the tags.title which then give refresh the pages with Article where tags = "world". Now i am trying to do this with scope but i am not to sure how to do it. Here my scope in my model
scope :by_tags, where(title => ?, "world news")
Here how i call it
<%= link_to (tag.title), articles_path(:scope => "test") %>
But obviously it doesn't work how can i fix it?
View
<%= link_to (tag.title), articles_path(:scope => tag.title) %>
Model(Article)
def self.by_tags(tag)
joins(:tags).where('tags.title = ?', tag)
end
Controller
def index
#articles = Article.by_tags(params[:scope])
end
i have a lessons table and a tags table. i associate both of the them using a has_many :through relationship and my middle table is tags_relationship.rb
class Lesson < ActiveRecord::Base
attr_accessible :title, :desc, :content, :tag_name
belongs_to :user
has_many :tag_relationships
has_many :tags, :through => :tag_relationships
end
class Tag < ActiveRecord::Base
attr_accessible :name
has_many :tag_relationships
has_many :lessons, :through => :tag_relationships
end
in one of my views, im trying to create a a virtual attribute. i have...
<div class="tags">
<%= f.label :tag_name, "Tags" %>
<%= f.text_field :tag_name, data: { autocomplete_source: tags_path} %>
</div>
but my lessons table doesn't have that attribute, tag_name, so it calls my method instead
def tag_name
????????
end
def tag_name=(name)
self.tag = Tag.find_or_initialize_by_name(name) if name.present?
end
however im not sure what to put inside the ????????. im trying to refer the :name attribute inside my tags table.
back then i used a has_many and belongs_to relationship. my lesson belonged to a tag (which was wrong) but i was able to write...
tag.name
and it worked. but since its a has_many :through now, im not sure. i tried using tags.name, Lessons.tags.name, etc but i cant seem to get it to work. how can i refer to the tags table name attribute? thank you
Apologize for my bad english.
When your Lesson was belonged to Tag lesson had only one tag, so your code was right. But now Lesson has many Tags, and it is collection (array in simple words). So, your setter must be more complex:
def tag_names=(names)
names = if names.kind_of? String
names.split(',').map{|name| name.strip!; name.length > 0 ? name : nil}.compact
else
names
end
current_names = self.tags.map(&:name) # names of current tags
not_added = names - current_names # names of new tags
for_remove = current_names - names # names of tags that well be removed
# remove tags
self.tags.delete(self.tags.where(:name => for_remove))
# adding new
not_added.each do |name|
self.tags << Tag.where(:name => name).first || Tag.new(:name => name)
end
end
And getter method should be like this:
def tag_names
self.tags.map(&:name)
end
BTW, finders like find_by_name are deprecated. You must use where.
I have a User model and Interest Model being joined by a join table called Choice (details below). I'm using the HABTM relationship with through since I have an attribute within the join table as well.
User.rb
has_many :choices
has_many :interests, :through => :choices
Interest.rb
has_many :choices
has_many :users, :through => :choices
Choice.rb
belongs_to :user
belongs_to :interest
So the question is how do I add records to this newly created choice table. For example =>
#user = User.find(1)
#interest = Interest.find(1)
????? Choice << user.id + interest.id + 4(score attribute) ??????
The last part is the part I'm having a problem with..I have these 3 parameters and didn't how to add them in and what the syntax was?
You have a couple options for adding a choice, but what probably makes the most sense would be to add choices by scoping to the user instance:
Assuming:
#user = User.find(1)
#interest = Interest.find(1)
You could add a choice like so:
#user.choices.create(:interest => #interest, :score => 4)
You could also do something like this in your controller:
def create
#choice = #user.choices.build(params[:choice])
if #choice.save
# saved
else
# not saved
end
end
This assumes your form has fields for choice[:interest_id] and choice[:score]