In Rails, to automatically count associations, you do:
class Script
has_many :chapters
end
class Chapter
belongs_to :script
end
and you add a chapters_count column into the Script model.
Now, what if you want to count the number of paragraphs in a Script without having a script_id key in the paragraph model ?
class Script
has_many :chapters
has_many :paragraphs # not complete
end
class Chapter
has_many :paragraphs
belongs_to :script
end
class Paragraph
belongs_to :chapter
end
How do you automatically associate script to paragraph and count them using the automatic count of Rails ?
You're on the right track. But first you've got to address a small error. Rails won't update a counter cache unless you instruct it to.
class Chapter
belongs_to :script, :counter_cache => true
end
Will automatically update #script.chapter_count before creation and after destruction of all associated Chapters.
Unfortunately things aren't so simply when dealing :through relationships. You will need to update the associated script's paragraph counter through callbacks in the Paragraph model.
N.B.: The following assumes you want to keep a paragraph counter in Chapter as well.
Start by applying the same theory to the Chapter model, and a paragraphs count column to the Script table.
class PrepareForCounterCache < ActiveRecord::Migration
def self.up
add_column :scripts, :paragraphs_count, :integer, :default => 0
add_column :chapters, :paragraphs_count, :integer, :default => 0
Chapter.reset_column_information
Script.reset_column_information
Chapter.find(:all).each do |c|
paragraphs_count = c.paragraphs.length
Chapter.update_counters c.id, :paragraphs_count => paragraphs_count
Script.update_counters c.script_id, :paragraphs_count => paragraphs_count
end
end
def self.down
remove_column :scripts, :paragraphs_count
remove_column :chapters, :paragraphs_count
end
end
Now to set up the relationships:
class Script
has_many: chapters
has_many: paragraphs, :through => :chapters
end
class Chapter
has_many: paragraphs
belongs_to :script, :counter_cache => true
end
class Paragraph
belongs_to :chapter, :counter_cache => true
end
All that's left is to tell Paragraph to update the paragraph counters in script as a callback.
class Paragraph < ActiveRecord::Base
belongs_to :chapter, :counter_cache => true
before_save :increment_script_paragraph_count
after_destroy, :decrement_script_paragraph_count
protected
def increment_script_paragraph_count
Script.update_counters chapter.script_id, :paragaraphs_count => 1
end
def decrement_script_paragraph_count
Script.update_counters chapter.script_id, :paragaraphs_count => -1
end
end
The quick and simple way, without using a cache is to do:
class Script
has_many :chapters
has_many :paragraphs, :through => :chapters
end
script = Script.find(1)
puts script.paragraphs.size #get the count
Related
I'm trying to model items that has_many tags. Items can have multiple tags, but are required to have at least 3 predefined ones.
Here's what I have so far:
class Item < ActiveRecord::Base
has_one :type, :through => :item_tags, :source => :tag
has_one :material, :through => :item_tags, :source => :tag
has_one :use, :through => :item_tags, :source => :tag
has_many :tag, :through => :item_tags
has_many :item_tags
end
This is giving me an ActiveRecord::HasOneThroughCantAssociateThroughCollection when I try to do Item.find(1).type.
I'm not sure how to do this. Can anyone help?
EDIT: I also want to be able to find the three predefined tags by doing item.type and item.use etc.
It's easier to consider this first by seeing how you want your database set up. You want:
Table: Tag
id
tag_name
Table: ItemTag
id
item_id
tag_id
Table: Item
id
type_id
material_id
use_id
So, your model would be more like:
class Item < ActiveRecord::Base
belongs_to :type, :class_name => 'Tag'
belongs_to :material, :class_name => 'Tag'
belongs_to :use, :class_name => 'Tag'
# Require these tags
validates_presence_of :type, :material, :use
has_many :item_tags
has_many :tags, :through => :item_tags
def list_tags
[type, material, use] + tags
end
end
So, your database will have three columns directly in the item table, which link to the tag table. These are required via validations, but you can also set up in your migrations to make these columns not null as well if you wish. The other optional tags keep their same relationship.
You want belongs_to and not has_one, because that pushes the relationship to the Item, where you want it. Has_one puts an item_id column in the Tag table, which is not what you want.
To get the three required tags to appear with the rest of the tags via this method, I'd recommend adding a function just for this use, defined as list_tags above.
Hope that helps!
I think you may want to use custom validations to check that Item.tags includes the ones you require, then use either scopes and class methods to get item.use, item.type, etc. to work as you want.
Item model:
class Item < ActiveRecord::Base
has_many :tags, :class_name => 'ItemTag'
validate :has_type, :has_use, :has_material
# Access methods
def types
self.tags.types
end
def uses
self.tags.uses
end
def materials
self.tags.materials
end
private
# Custom validation methods
def has_type
unless tags.present? and tags.include?(ItemTag.types)
errors.add("Tags must include a type.")
end
end
def has_material
unless tags.present? and tags.include?(ItemTag.materials)
errors.add("Tags must include a material.")
end
end
def has_use
unless tags.present? and tags.include?(ItemTag.use)
errors.add("Tags must include a use.")
end
end
end
ItemTag model:
class ItemTag < ActiveRecord::Base
scope :types, lambda { where(...) }
scope :materials, lambda { where(...) }
scope :uses, lambda { where(...) }
end
You could grab single occurances if preferred using .first in the access methods. You'll need to adjust the where(...) queries based on how you are determining what constitutes a type/material/use.
I have the following models:
class Section < ActiveRecord::Base
belongs_to :course
has_one :term, :through => :course
end
class Course < ActiveRecord::Base
belongs_to :term
has_many :sections
end
class Term < ActiveRecord::Base
has_many :courses
has_many :sections, :through => :courses
end
I would like to be able to do the following in my Section model (call_number is a field in Section):
validates_uniqueness_of :call_number, :scope => :term_id
This obviously doesn't work because Section doesn't have term_id, so how can I limit the scope to a relationship's model?
I tried creating a custom validator for Section to no avail (doesn't work when I create a new Section with the error "undefined method 'sections' for nil:NilClass"):
def validate_call_number
if self.term.sections.all(:conditions => ["call_number = ? AND sections.id <> ?", self.call_number, self.id]).count > 0
self.errors[:base] << "Call number exists for term"
false
end
true
end
Thanks a lot!
Assuming your validation code is correct, why don't you simply add a check for term existence?
def validate_call_number
return true if self.term.nil? # add this line
if self.term.sections.all(:conditions => ["call_number = ? AND sections.id <> ?", self.call_number, self.id]).count > 0
self.errors[:base] << "Call number exists for term"
false
end
true
end
I have two models
Post
has_many :comments
Comment
belongs_to :post
When I want display a list of posts and it's comment count. I usually include comments in the post like this .
Post.find(:all,:include => :comments)
To display a number of comment for post.
post.comments.size
Can I create a has_many relation which return count of comments ?
has_one :comments_count
Can I include this relationship like this ?
Post.find(:all,:include => :comments_count)
Rails has a counter cache which will automatically update a columns value based on the count of associated items. This will allow you to include the count when you return the posts object. Just add it to the belongs_to on comment.
Comment
belongs_to :post, :counter_cache => true
You'll need to add a new integer column to posts for the counter:
class AddCounterCacheToPosts < ActiveRecord::Migration
def self.up
add_column :posts, :comments_count, :integer
end
end
To answer you question, yes you can; but this is not the most efficient way to do it. Normally you add a column to Post called comments_count and you updated that column every Comment CRUD action.
Add the column:
rails g migration add_comment_count_to_post
Then in that migration add the following line:
add_column :posts, :comments_count, :integer, :default => 0
Then there are two way to handle it from here.
The first is a custom before_save and before_destroy in Comment model.
app/models/comment.rb
class Comment << ActiveRecord::Base
belongs_to :post
before_save :update_comments_count
before_destroy :update_comments_count
def update_comment_count
post.update_attribute(:comment_count, post.comments.count)
end
end
The second way is to use Rails custom helper for this:
app/models/comment.rb
class Comment << ActiveRecord::Base
belongs_to :post, :counter_cache => true
end
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).
I am trying to create a model for a ruby on rails project that builds relationships between different words. Think of it as a dictionary where the "Links" between two words shows that they can be used synonymously. My DB looks something like this:
Words
----
id
Links
-----
id
word1_id
word2_id
How do I create a relationship between two words, using the link-table. I've tried to create the model but was not sure how to get the link-table into play:
class Word < ActiveRecord::Base
has_many :synonyms, :class_name => 'Word', :foreign_key => 'word1_id'
end
In general, if your association has suffixes such as 1 and 2, it's not set up properly. Try this for the Word model:
class Word < ActiveRecord::Base
has_many :links, :dependent => :destroy
has_many :synonyms, :through => :links
end
Link model:
class Link < ActiveRecord::Base
belongs_to :word
belongs_to :synonym, :class_name => 'Word'
# Creates the complementary link automatically - this means all synonymous
# relationships are represented in #word.synonyms
def after_save_on_create
if find_complement.nil?
Link.new(:word => synonym, :synonym => word).save
end
end
# Deletes the complementary link automatically.
def after_destroy
if complement = find_complement
complement.destroy
end
end
protected
def find_complement
Link.find(:first, :conditions =>
["word_id = ? and synonym_id = ?", synonym.id, word.id])
end
end
Tables:
Words
----
id
Links
-----
id
word_id
synonym_id
Hmm, this is a tricky one. That is because synonyms can be from either the word1 id or the word2 id or both.
Anyway, when using a Model for the link table, you must use the :through option on the Models that use the Link Table
class Word < ActiveRecord::Base
has_many :links1, :class_name => 'Link', :foreign_key => 'word1_id'
has_many :synonyms1, :through => :links1, :source => :word
has_many :links2, :class_name => 'Link', :foreign_key => 'word2_id'
has_many :synonyms2, :through => :links2, :source => :word
end
That should do it, but now you must check two places to get all the synonyms. I would add a method that joined these, inside class Word.
def synonyms
return synonyms1 || synonyms2
end
||ing the results together will join the arrays and eliminate duplicates between them.
*This code is untested.
Word model:
class Word < ActiveRecord::Base
has_many :links, :dependent => :destroy
has_many :synonyms, :through => :links
def link_to(word)
synonyms << word
word.synonyms << self
end
end
Setting :dependent => :destroy on the has_many :links will remove all the links associated with that word before destroying the word record.
Link Model:
class Link < ActiveRecord::Base
belongs_to :word
belongs_to :synonym, :class_name => "Word"
end
Assuming you're using the latest Rails, you won't have to specify the foreign key for the belongs_to :synonym. If I recall correctly, this was introduced as a standard in Rails 2.
Word table:
name
Link table:
word_id
synonym_id
To link an existing word as a synonym to another word:
word = Word.find_by_name("feline")
word.link_to(Word.find_by_name("cat"))
To create a new word as a synonym to another word:
word = Word.find_by_name("canine")
word.link_to(Word.create(:name => "dog"))
I'd view it from a different angle; since all the words are synonymous, you shouldn't promote any one of them to be the "best". Try something like this:
class Concept < ActiveRecord::Base
has_many :words
end
class Word < ActiveRecord::Base
belongs_to :concept
validates_presence_of :text
validates_uniqueness_of :text, :scope => :concept_id
# A sophisticated association would be better than this.
def synonyms
concept.words - [self]
end
end
Now you can do
word = Word.find_by_text("epiphany")
word.synonyms
Trying to implement Sarah's solution I came across 2 issues:
Firstly, the solution doesn't work when wanting to assign synonyms by doing
word.synonyms << s1 or word.synonyms = [s1,s2]
Also deleting synonyms indirectly doesn't work properly. This is because Rails doesn't trigger the after_save_on_create and after_destroy callbacks when it automatically creates or deletes the Link records. At least not in Rails 2.3.5 where I tried it on.
This can be fixed by using :after_add and :after_remove callbacks in the Word model:
has_many :synonyms, :through => :links,
:after_add => :after_add_synonym,
:after_remove => :after_remove_synonym
Where the callbacks are Sarah's methods, slightly adjusted:
def after_add_synonym synonym
if find_synonym_complement(synonym).nil?
Link.new(:word => synonym, :synonym => self).save
end
end
def after_remove_synonym synonym
if complement = find_synonym_complement(synonym)
complement.destroy
end
end
protected
def find_synonym_complement synonym
Link.find(:first, :conditions => ["word_id = ? and synonym_id = ?", synonym.id, self.id])
end
The second issue of Sarah's solution is that synonyms that other words already have when linked together with a new word are not added to the new word and vice versa.
Here is a small modification that fixes this problem and ensures that all synonyms of a group are always linked to all other synonyms in that group:
def after_add_synonym synonym
for other_synonym in self.synonyms
synonym.synonyms << other_synonym if other_synonym != synonym and !synonym.synonyms.include?(other_synonym)
end
if find_synonym_complement(synonym).nil?
Link.new(:word => synonym, :synonym => self).save
end
end