Multiple column foreign keys / associations in ActiveRecord/Rails - mysql
I have badges (sorta like StackOverflow).
Some of them can be attached to badgeable things (e.g. a badge for >X comments on a post is attached to the post). Almost all come in multiple levels (e.g. >20, >100, >200), and you can only have one level per badgeable x badge type (= badgeset_id).
To make it easier to enforce the one-level-per-badge constraint, I want badgings to specify their badge by a two-column foreign key - badgeset_id and level - rather than by primary key (badge_id), though badges does have a standard primary key too.
In code:
class Badge < ActiveRecord::Base
has_many :badgings, :dependent => :destroy
# integer: badgeset_id, level
validates_uniqueness_of :badgeset_id, :scope => :level
end
class Badging < ActiveRecord::Base
belongs_to :user
# integer: badgset_id, level instead of badge_id
#belongs_to :badge # <-- how to specify?
belongs_to :badgeable, :polymorphic => true
validates_uniqueness_of :badgeset_id, :scope => [:user_id, :badgeable_id]
validates_presence_of :badgeset_id, :level, :user_id
# instead of this:
def badge
Badge.first(:conditions => {:badgeset_id => self.badgeset_id, :level => self.level})
end
end
class User < ActiveRecord::Base
has_many :badgings, :dependent => :destroy do
def grant badgeset, level, badgeable = nil
b = Badging.first(:conditions => {:user_id => proxy_owner.id, :badgeset_id => badgeset,
:badgeable_id => badgeable.try(:id), :badgeable_type => badgeable.try(:class)}) ||
Badging.new(:user => proxy_owner, :badgeset_id => badgeset, :badgeable => badgeable)
b.level = level
b.save
end
end
has_many :badges, :through => :badgings
# ....
end
How I can specify a belongs_to association that does that (and doesn't try to use a badge_id), so that I can use the has_many :through?
ETA: This partially works (i.e. #badging.badge works), but feels dirty:
belongs_to :badge, :foreign_key => :badgeset_id, :primary_key => :badgeset_id, :conditions => 'badges.level = #{level}'
Note that the conditions is in single quotes, not double, which makes it interpreted at runtime rather than loadtime.
However, when trying to use this with the :through association, I get the error undefined local variable or method 'level' for #<User:0x3ab35a8>. And nothing obvious (e.g. 'badges.level = #{badgings.level}') seems to work...
ETA 2: Taking EmFi's code and cleaning it up a bit works. It requires adding badge_set_id to Badge, which is redundant, but oh well.
The code:
class Badge < ActiveRecord::Base
has_many :badgings
belongs_to :badge_set
has_friendly_id :name
validates_uniqueness_of :badge_set_id, :scope => :level
default_scope :order => 'badge_set_id, level DESC'
named_scope :with_level, lambda {|level| { :conditions => {:level => level}, :limit => 1 } }
def self.by_ids badge_set_id, level
first :conditions => {:badge_set_id => badge_set_id, :level => level}
end
def next_level
Badge.first :conditions => {:badge_set_id => badge_set_id, :level => level + 1}
end
end
class Badging < ActiveRecord::Base
belongs_to :user
belongs_to :badge
belongs_to :badge_set
belongs_to :badgeable, :polymorphic => true
validates_uniqueness_of :badge_set_id, :scope => [:user_id, :badgeable_id]
validates_presence_of :badge_set_id, :badge_id, :user_id
named_scope :with_badge_set, lambda {|badge_set|
{:conditions => {:badge_set_id => badge_set} }
}
def level_up level = nil
self.badge = level ? badge_set.badges.with_level(level).first : badge.next_level
end
def level_up! level = nil
level_up level
save
end
end
class User < ActiveRecord::Base
has_many :badgings, :dependent => :destroy do
def grant! badgeset_id, level, badgeable = nil
b = self.with_badge_set(badgeset_id).first ||
Badging.new(
:badge_set_id => badgeset_id,
:badge => Badge.by_ids(badgeset_id, level),
:badgeable => badgeable,
:user => proxy_owner
)
b.level_up(level) unless b.new_record?
b.save
end
def ungrant! badgeset_id, badgeable = nil
Badging.destroy_all({:user_id => proxy_owner.id, :badge_set_id => badgeset_id,
:badgeable_id => badgeable.try(:id), :badgeable_type => badgeable.try(:class)})
end
end
has_many :badges, :through => :badgings
end
While this works - and it's probably a better solution - I don't consider this an actual answer to the question of how to do a) multi-key foreign keys, or b) dynamic-condition associations that work with :through associations. So if anyone has a solution for that, please speak up.
Seems like it might workout best if you separate Badge into two models. Here's how I'd break it down to achieve the functionality you want. I threw in some named scopes to keep the code that actually does things clean.
class BadgeSet
has_many :badges
end
class Badge
belongs_to :badge_set
validates_uniqueness_of :badge_set_id, :scope => :level
named_scope :with_level, labmda {|level
{ :conditions => {:level => level} }
}
named_scope :next_levels, labmda {|level
{ :conditions => ["level > ?", level], :order => :level }
}
def next_level
Badge.next_levels(level).first
end
end
class Badging < ActiveRecord::Base
belongs_to :user
belongs_to :badge
belongs_to :badge_set
belongs_to :badgeable, :polymorphic => true
validates_uniqueness_of :badge_set_id, :scope => [:user_id, :badgeable_id]
validates_presence_of :badge_set_id, :badge_id, :user_id
named_scope :with_badge_set, lambda {|badge_set|
{:conditions => {:badge_set_id => badge_set} }
}
def level_up(level = nil)
self.badge = level ? badge_set.badges.with_level(level).first
: badge.next_level
save
end
end
class User < ActiveRecord::Base
has_many :badgings, :dependent => :destroy do
def grant badgeset, level, badgeable = nil
b = badgings.with_badgeset(badgeset).first() ||
badgings.build(
:badge_set => :badgeset,
:badge => badgeset.badges.level(level),
:badgeable => badgeable
)
b.level_up(level) unless b.new_record?
b.save
end
end
has_many :badges, :through => :badgings
# ....
end
Related
Efficently querying multi language categories and category items
So I have a bit of a server response time issue - which I think is caused due to obsolete queries. One major query chain that I have takes up to 370ms, which is obviously causing an issue. Here are the requirements: 5 Different languages There are several Product Categories (i.e. Cat 1, Cat 2, Cat 3, etc.) Categories displayed depend on language. For example whilst category 1 is displayed in all languages, category 2 is only displayed in Germany and France but not in the UK Each category contains x number of items (has_many belongs_to relationship). Again some items are displayed in certain languages others are not. For example even category 2 is displayed in France and Germany, only in Germany you can buy Item 1 and hence Item 1 should not be displayed in France but Germany. The categories and items do have boolean fields named after the locale. This way I can set via flag whether or not to display the category and item in a specific language. My solution: Building the solution is quiet easy. In controller I read out all the categories for the current locale: application_controller.rb (since it is used on every single page) #product_categories = ProductCategory.where("lang_" + I18n.locale.to_s + " = ?", true) And in the view (the navigation) I do the following: layouts/navs/productnav.html.haml - #product_categories.each do |category| ... - category.products.includes(:product_teasers).where("lang_" + I18n.locale.to_s + " = ? AND active = ?", true, true).in_groups_of(3).each do |group| ... The issue with this solution is that each time I fire a lot of queries towards the database. Using "includes" does not solve it as I can not specify what items to pull. Furthermore I require the in_groups_of(3) in my loop to display the items correctly on the page. I was also looking into memchached solutions to have the queries cached all together - i.e. Dalli however, this would require me to change a lot of code as I am guessing I would require to query all categories for each language and cache them. In addition to it I have to query each item for each langugage depending on language and store that somehow in an array ?! My question: How to approach this ? There must be a simpler and more efficient solution. How to efficiently query respectively cache this? Thank you! UPDATE: As requested here are my two Models: 1.) ProductCategory class ProductCategory < ActiveRecord::Base translates :name, :description, :slug, :meta_keywords, :meta_description, :meta_title, :header, :teaser, :fallbacks_for_empty_translations => true extend FriendlyId friendly_id :name, :use => [:globalize, :slugged] globalize_accessors :locales => [:at, :de, :ch_de, :ch_fr, :fr, :int_en, :int_fr], :attributes => [:slug] has_paper_trail has_many :products, :dependent => :destroy validates :name, presence: true, uniqueness: { case_sensitive: false } default_scope { includes(:translations) } private def slug_candidates [ [:name] ] end end 2.) Product And every product Category can have 0..n Products, and each Product must belongs to one category. class Product < ActiveRecord::Base translates :slug, :name, :meta_keywords, :meta_description, :meta_title, :teaser, :power_range, :product_page_teaser, :product_category_slider_teaser, :fallbacks_for_empty_translations => true extend FriendlyId friendly_id :name, :use => :globalize before_save :change_file_name searchable do text :name, :teaser, :product_page_teaser, :product_category_slider_teaser integer :product_category_id end belongs_to :product_category has_many :product_teasers, :dependent => :destroy has_many :product_videos, :dependent => :destroy has_many :product_banners, :dependent => :destroy has_many :product_documents, :dependent => :destroy has_many :product_tabs, :dependent => :destroy has_many :active_relationships, class_name: "Relationship", foreign_key: "follower_id", dependent: :destroy has_many :passive_relationships, class_name: "Relationship", foreign_key: "followed_id", dependent: :destroy has_many :following, through: :active_relationships, source: :followed has_many :followers, through: :passive_relationships, source: :follower has_many :references has_and_belongs_to_many :contacts accepts_nested_attributes_for :product_teasers, :reject_if => :all_blank, :allow_destroy => true accepts_nested_attributes_for :product_tabs, :reject_if => :all_blank, :allow_destroy => true accepts_nested_attributes_for :product_videos, :reject_if => :all_blank, :allow_destroy => true accepts_nested_attributes_for :product_banners, :reject_if => :all_blank, :allow_destroy => true accepts_nested_attributes_for :product_documents, :reject_if => :all_blank, :allow_destroy => true has_paper_trail validates :name, presence: true, uniqueness: { case_sensitive: false } default_scope {includes(:translations)} .... a lot more going on here ... end Please note: That category contains language flags (booleans), i.e lang_at, lang_de, lang_fr, etc. and if set then this category is displayed in that particualar language. SAME applies to products, as certain products are not displayed in all langauges altough the category might be. Examples: #product_categories = ProductCategory.where("product_categories.lang_" + I18n.locale.to_s + " = ?", true) #products = Product.where("product_categories.lang_" + I18n.locale.to_s + " = ?", true) I skipped any includes on purpose above - it is just to demonstrate the language logic.
UPDATE The system have spent a lot of times to loop data in nested loop. Avoid to fetch data in nested loop. You have to use join or includes to catch your data more effective. For example: Controller #products = Product.includes(:product_category).where("product_categories.lang_" + I18n.locale.to_s + " = ? AND product_categories.active = ?", true, true).group_by(&:category) View - #products.each do |category, products| <%= category.name %> - products.each do |product| <%= product.title %> It needs to fix with your necessary code. I just help the main query. For example: active = ? is for products field or product_categories field. I hope It can help you.
Rails Active Record query count multiple relations
I have a database structured like this: Groups has_many Packages Packages has_many Rooms Rooms has_and_belongs_to_many Clients Ok. Every room can be 'Quadruple', 'Triple', 'Double', etc. And I can access all the rooms belonging to a Group by doing Group.rooms. I want to be able to get how many clients per type of room the Group has. For example: Quad: 16 clients Double: 10 clients *etc. I managed to get the ammount of Rooms per type, like this: Group.rooms.group('type').count Any ideas? UPDATE 1 - Models Quarto.rb (Rooms) class QuartoContratado < ActiveRecord::Base belongs_to :pacote has_and_belongs_to_many :clientes, :join_table => :acomodacoes Pacote.rb (Packages) class Pacote < ActiveRecord::Base belongs_to :grupo has_many :passageiros has_many :clientes, :through => :passageiros has_many :quartos, :class_name => "QuartoContratado" Grupo.rb (Groups) class Grupo < ActiveRecord::Base has_many :pacotes has_many :clientes, :through => :pacotes, :conditions => { :pacotes => { :cancelado => false } } has_many :quartos, :class_name => "QuartoContratado", :through => :pacotes, :conditions => { :pacotes => { :cancelado => false } } Client.rb (Clients) class Cliente < ActiveRecord::Base has_many :passageiros has_many :pacotes, :class_name => "Pacote", :through => :passageiros has_many :grupos, :through => :pacotes, :conditions => { :pacotes => { :cancelado => false } }
This is how I managed to get what I wanted... it looks ugly! Any better way to write it? group = Grupo.first types = group.quartos.group_by { |t| t.type } types.each do |key, value| print "#{key} - " c=0 value.each do |v| c= c+v.clientes.count end puts "#{c} clients" end
Associate foreign key through another foreign key
I've these 3 Models class User < ActiveRecord::Base has_many :answers, :as => :owner end class Answer < ActiveRecord::Base belongs_to :owner, :polymorphic => true has_one :test end class Test < ActiveRecord::Base belongs_to :answer end so I want to associate the Test model with User model through Answer Model without need of creating new association between them, so I put the following into Test Model: has_one :owner, :through => :answer but it doesn't work and I got this error ActiveRecord::HasManyThroughAssociationPolymorphicSourceError: Cannot have a has_many :through association 'Test#owner' on the polymorphic object 'Owner#owner'. any help?
In Test: delegate :owner, :to => :answer
You have to specify source_type option as owner is a polymorphic association class Test < ActiveRecord::Base belongs_to :answer has_one :owner, :through => :answer, :source_type => "User" end
ActiveRecord Multi-association query
So I've got three models: User User Interest Interest Tags User Model class User < ActiveRecord::Base has_many :user_interests end InterestTag Model class InterestTag < ActiveRecord::Base has_many :user_interests, :dependent => :destroy validates :name, :uniqueness => true end UserInterest Model class UserInterest < ActiveRecord::Base belongs_to :interest_tag belongs_to :user end I'd like to use ActiveRecord to include the name of the user's interests when loading their profile using the following query: #user = User.find(current_user.id, :include => [{:user_interests => :interest_tags}]) Migrations for interest_tags + user_interests create_table :interest_tags do |t| t.string :name, :null => false, :size => 30 t.timestamp :created_at end create_table :user_interests do |t| t.integer :user_id t.integer :interest_tag_id end What am I doing wrong?
You have to add an has_many :through association on User model. class User < ActiveRecord::Base has_many :user_interests has_many :interest_tags, :through => :user_interests end class UserInterest < ActiveRecord::Base belongs_to :interest_tag belongs_to :user end class InterestTag < ActiveRecord::Base has_many :user_interests, :dependent => :destroy validates :name, :uniqueness => true end Now you can eager load the tags as follows: User.find(current_user.id, :include => :interest_tags) Note: You might want to look at the acts_as_taggable_on gem for your requirement.
I assume you are building a tagging system, like in stackoverflow: every user kann have multiple tags that they are interested in. In that case the user_interests table is only a join table and does not need a model. Just use has_and_belong_to_many on the two real models. See also this article on different ways to implement tagging with more or less normalized relational databases. You could also use a non-relational database like Mongodb, there you would need only one table to do tagging, see the cookbook.
how do I get this :through?
so I have these relationships: a location: has_many :services has_many :products, :through => :services a product: has_many :services has_many :locations, :through => :services has_many :add_ons and a service: belongs_to :product belongs_to :location has_many :service_add_ons has_many :add_ons, :through => :service_add_ons a service_add_on: belongs_to :service belongs_to :add_on How can I write a :through that will return a location with it's products and each products add_ons? so far I have: wants.json { render :json => #location.to_json(:include => {:products => {:add_ons}}) } which is obviously not working. What can I do to change this and make it work?
Try this: wants.json { render :json => #location.to_json(:include => {:products => {:include => :add_ons}}) }
I believe you should use :include => {:products => :add_ons}
Sorry for my english :S I think you have an error here: a service_add_on: belongs_to :service belongs_to :add_on add_on must belongs_to service_add_on and not contrary