Suppose we have a photography site. Any author can subscribe to receive updates from any other author. Obviously if author A is subscribed to author B that doesn't mean that B is subscribed to A. So we build models
class Author < ActiveRecord::Base
has_many :subscriptions
has_many :subscribed_by_author, :through => :subscriptions, :source => :subscribed_to
end
class Subscription < ActiveRecord::Base
belongs_to :author
belongs_to :subscribed_to, :class_name => "Author", :foreign_key => "subscribed_to"
end
This way we can use
some_author.subscribed_by_author -- the list of the authors to whom some_author is subscribed.
For any subscription we can know both ends (who is subscribed to whom)
But the question is how to get the list of people subscribed to some author using only rails (not using plain SQL) i.e get the answer to :"Who is subscribed to some_author?"
Question: is there any ability in Rails to get the relationship working both sides i.e. not only writing some_author.subscribed_BY_author but having some_author_subscribed_TO_author? If there is one, then what is it?
P.S. Obvious solution is to
Change the database design, adding a column named "direction"
Create 2 records each time a subscription is created
Add to the author model
has_many :subscribed_BY_author, :through => :subscriptions, :source => :subscribed_to, :conditions => "direction = 'by'"
has_many :subscribed_TO_author, :through => :subscriptions, :source => :subscribed_to, :conditions => "direction = 'to'"
But i wonder if there is a solution without changing the database design.
I'd use plain HABTM for something simple like this, but you're going to need a join table no matter what.
create_table :subscriptions do |t|
t.column :author_id, :integer
t.column :subscriber_id, :integer
end
Point Author to it:
class Author < ActiveRecord::Base
has_and_belongs_to_many :subscribers
:class_name => "Author",
:join_table => "subscriptions",
:association_foreign_key => "subscriber_id"
def subscriptions # "subscribers" is already included above
self.subscribers.find(:all, :subscriber_id=>author.id) # hopefully not too
end # much SQL
end
If you're really committed to your method names:
def subscribed_to_author
subscribers
end
def subscribed_by_author(author)
self.subscribers.find(:all, :subscriber_id=>author.id)
end
Create some connections (I'd make SubscriptionsController to be RESTy)
SubscriptionsController < ApplicationController
def create
#author = Author.find(params[:author_id] # author to be subscribed to
#user = current_user # user clicking the "subscribe" button
#author.subscribers << #user # assuming authors should only
#author.save # be able to subscribe themselves
end
end
Display names, or whatever
#author.subscribers.each do |s|
s.name
end
# ...or...and...also...
<%= render :partial => #author.subscribers -%>
<%= render :partial => #author.subscriptions -%>
# Author model
has_many :subscriptions_to, :class_name => "Subscription", :foreign_key => "subscribed_to"
has_many :subscribed_to_author, :through => :subscriptions_to, :source => :author
As far as I know - it works! :)
Related
I have this relation in my Product model:
has_many :features, :class_name => 'ProductFeature', :source => :product_feature, :include => :feature
So I can do Product.features
which works fine. But I want to be able to filter that by fields in the feature table, when and if necessary. For example in pseudo code:
find all product features where feature is comparable
compare is a bool field on the feature.
I have been trying for 2 hours solid and cannot figure it out (without writing a new query completely). I can't figure out how to access the feature table's fields from the Product.features relation, as it seems it can only filter on product_features fields.
This is what I have come up with so far:
def features_compare
features.feature.where(:compare => true)
end
But it just says feature is not a valid method, which I understand.
Edit
I have updated my model so the relationships are clearer:
product.rb:
class Product < ActiveRecord::Base
belongs_to :company
belongs_to :insurance_type
has_many :product_features
has_many :reviews
attr_accessible :description, :name, :company
end
product_feature.rb:
class ProductFeature < ActiveRecord::Base
belongs_to :product
belongs_to :feature
delegate :name, :to => :feature
attr_accessible :value
end
feature.rb
class Feature < ActiveRecord::Base
attr_accessible :name, :compare
end
I want to be able to query the product_features that belong to a product and feature where Feature.compare is true. Something like this:
product.rb
def features_compare
product_features.where(:compare => true)
end
This throws an error because compare in in the Feature model, not ProductFeature. I have tried the following in product_feature.rb:
delegate :compare, :to => :feature
but I didn't help.
I will adding a bounty to this in a few hours so please please help me!
find all product features where feature is comparable is just
ProductFeature.joins(:feature).where(:feature => {:compare => true})
You can make that a bit more reusable by introducing a scope:
#in product_feature.rb
scope :with_feature_like, lambda do |filter|
joins(:feature).where(:feature => filter)
end
#elsewhere
ProductFeature.with_feature_like(:compare => true)
#all the product features of a certain product with at comparable features
some_product.product_features.with_feature_like(:compare => true)
Finally, if you want all products with product features with comparable features, you want something like:
Product.joins(:product_features => :feature).where(:feature => {:compare => true})
which of course you can also turn into a scope on Product.
This seems like a has_many :through relationship. Try changing this:
has_many :features, :class_name => 'ProductFeature', :source => :product_feature, :include => :feature
to this:
has_many :product_features
has_many :features, :through => :product_features
As long as your ProductFeature model has this:
belongs_to :product
belongs_to :feature
And you have the appropriate columns on product_features (product_id, feature_id), then you should be able to access that product's features and all the attributes on both Product and ProductFeature.
See here:
http://guides.rubyonrails.org/association_basics.html#the-has_many-through-association
EDIT: Here's how to filter by feature fields.
Product.joins(:features).where(:features => {:name => "Size"})
#product.each |p| { p.features.where(:comparable => true) } is probably your best bet here, but I'm open to being enlightened.
Similar to this question, how do I set a property on the join model just before save in this context?
class Post < ActiveRecord::Base
has_many :post_assets
has_many :assets, :through => :post_assets
has_many :featured_images, :through => :post_assets, :class_name => "Asset", :source => :asset, :conditions => ['post_assets.context = ?', "featured"]
end
class PostAssets < ActiveRecord::Base
belongs_to :post
belongs_to :asset
# context is so we know the scope or role
# the join plays
validates_presences_of :context
end
class Asset < ActiveRecord::Base
has_many :post_assets
has_many :posts, :through => :post_assets
end
I just want to be able to do this:
#post = Post.create!(:title => "A Post")
#post.featured_images << Asset.create!(:title => "An Asset")
# ...
#post = Post.first
#featured = #post.featured_images.first
#=> #<Asset id: 1, title: "An Asset">
#featured.current_post_asset #=> #<PostAsset id: 1, context: "featured">
How would that work? I've been banging my head over it all day :).
What currently happens is when I do this:
#post.featured_images << Asset.create!(:title => "An Asset")
Then the join model PostAsset that gets created never gets a chance to set context. How do I set that context property? It looks like this:
PostAsset.first #=> #<PostAsset id: 1, context: nil>
Update:
I have created a test gem to try to isolate the problem. Is there an easier way to do this?!
This ActsAsJoinable::Core class makes it so you can have many to many relationships with a context between them in the join model. And it adds helper methods. The basic tests show basically what I'm trying to do. Any better ideas on how to do this properly?
Look at the has_many options in the ActiveRecord::Associations::ClassMethods API located here: http://rails.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#M001316
This is the most interesting quote:
:conditions
Specify the conditions that the associated object must meet in order to be included as a WHERE SQL fragment, such as authorized = 1. Record creations from the association are scoped if a hash is used. has_many :posts, :conditions => {:published => true} will create published posts with #blog.posts.create or #blog.posts.build.
So I believe your conditions must be specified as a hash, like so:
class Post < ActiveRecord::Base
has_many :post_assets
has_many :featured_post_assets, :conditions => { :context => 'featured' }
has_many :assets, :through => :post_assets
has_many :featured_images, :through => :featured_post_assets,
:class_name => "Asset", :source => :asset,
end
And you should also do the following:
#post.featured_images.build(:title => "An asset")
instead of:
#post.featured_images << Asset.create!(:title => "An Asset")
This should call the scoped asset build, as suggested in the quote above to add the context field to asset. It will also save both the join model object (post_asset) and the asset object to the database at the same time in one atomic transaction.
G'day guys,
I'm currently flitting through building a test "Auction" website to learn rails. I've set up my Auction and User models and have it so that only authenticated users can edit or delete auctions that are associated with them.
What I'm having difficulty doing is associating bid items with the Auction.
My models are as follows:
class Auction < ActiveRecord::Base
belongs_to :creator, :class_name => "User"
has_many :bids
validates_presence_of :title
validates_presence_of :description
validates_presence_of :curprice
validates_presence_of :finish_time
attr_reader :bids
def initialize
#bids = []
end
def add_bid(bid)
#bids << bid
end
end
class Bid < ActiveRecord::Base
belongs_to :auction, :class_name => "Auction", :foreign_key => "auction_id"
belongs_to :bidder, :class_name => "User", :foreign_key => "bidder_id"
validates_presence_of :amount
validates_numericality_of :amount
#retracted = false
end
class User < ActiveRecord::Base
has_many :auctions, :foreign_key => "owner_id"
has_many :bids, :foreign_key => "owner_id"
#auth stuff here
end
I'm attempting to add a bid record to an auction, but the auction_id simply will not add to the record.
I create a bid with a value from within a view of the auction, having the #auction as the local variable.
<% form_for :bid, :url => {:controller => "auction", :action => "add_bids"} do |f|%>
<p>Bid Amount <%= f.text_field :amount %></p>
<%= submit_tag "Add Bid", :auction_id => #auction %>
<% end %>
This is connected to the following code:
def add_bids
#bid = current_user.bids.create(params[:bid])
if #bid.save
flash[:notice] = "New Bid Added"
redirect_to :action => "view_auction", :id => #bid.auction_id
end
end
The problem I am getting is that the auction_id is not put into the bid element. I've tried setting it in the form HTML, but I think I'm missing something very simple.
My Data model, to recap is
Users have both bids and auctions
Auctions have a user and have many bids
Bids have a user and have a auction
I've been struggling with trying to fix this for the past 4 hours and I'm starting to get really downhearted about it all.
Any help would be really appreciated!
You're not quite doing things the Rails way, and that's causing you a bit of confusion.
Successful coding in Rails is all about convention over configuration. Meaning, Rails will guess at what you mean unless you tell it otherwise. There's usually a couple of things it will try if it guesses wrong. But in general stick to the deterministic names and you'll be fine.
There are so many errors in your code, so I'm going to clean it up and put comments every way to let you know what's wrong.
app/models/auction.rb
class Auction < ActiveRecord::Base
belongs_to :creator, :class_name => "User"
has_many :bids
# Given the nature of your relationships, you're going to want to add this
# to quickly find out who bid on an object.
has_many :bidders, :through => :bids
validates_presence_of :title
validates_presence_of :description
validates_presence_of :curprice
validates_presence_of :finish_time attr_reader :bids
#These two methods are unnecessary.
# Also don't override initialize in ActiveRecord. Instead use after_initialize
#def initialize Supplied by rails when you do has_many :bids
# #bids = [] #bids will be populated by what is picked up from
#end the database based on the has_many relationship
#def add_bid(bid) Supplied by rails when you do has_many :bids
# #bids << bid auction.bids << is a public method after has_many :bids
#end
end
app/models/bid.rb
class Bid < ActiveRecord::Base
# :class_name and :foreign_key are ony necessary when rails cannot guess from a
# association name. :class_name default is the association singularized and
# capitalized. :foreign_key default is association_id
belongs_to :auction #, :class_name => "Auction", :foreign_key => "auction_id"
# here we need :class_name because Rails is looking for a Bidder class.
# also there's an inconsistency. Later user refers to has_many bids with
# a foreign_key of owner_id, which one is it? bidder_id or owner_id?
# if it's owner_id? you will need the :foreign_key option.
belongs_to :bidder, :class_name => "User" #, :foreign_key => "bidder_id"
validates_presence_of :amount
validates_numericality_of :amount
# This will never get called in a useful way.
# It really should be done in the migration, setting default
# value for bids.retracted to false
# #retracted = false
end
app/models/user.rb
class User < ActiveRecord::Base
# This makes sense, because an auction can have many bidders, who are also users.
has_many :auctions, :foreign_key => "owner_id"
# This doesn't. A bid belongs to a user, there's no need to change the name.
# See above note re: owner_id vs. bidder_id
has_many :bids, :foreign_key => "owner_id"
# You could also use this to quickly get a list of auctions a user has bid on
has_many :bid_on_auctions, :through => :bids, :source => :auction
... auth stuff ...
end
So far so good, right?
The view isn't bad but it's missing the form parts for the bid amount. This code assumes that you store the value of the bid in an amount column. I also arbitrarily named it auctions/bid
app/views/auctions/bid.html.erb
<% form_for :bid, #auction.bids.new do |f|%>
<%= f.label_for :amount %>
<%= f.text_field :amount%>
<!-- Don't need to supply #auction.id, because form_for does it for you. -->
<%= submit_tag "Add Bid" %>
params hash generated by the form: that is passed to the controller:
params =
{
:bid =>
{
:auction_id => #auction.id
:amount => value of text_field
}
}
params hash generated by the from as you wrote it (note: I'm guessing at names because they were left out of the posted code):
params =
{
:id => #auction_id ,
:bid => { :amount => value of text_field }
}
However, your controller code is where all your problems are coming from this is almost entirely wrong. I'm guessing this is in the auction controller, which seems wrong because you're trying to create a bid. Lets see why:
app/controllers/auctions_controller.rb
...
def add_bids
# not bad, but... #bid will only fill in the owner_id/bidder_id. and bid amount.
#bid = current_user.bids.create(params[:bid])
# create calls save, so this next line is redundant. It still works though.
# because nothing's happening between them to alter the outcome of save.
if #bid.save
flash[:notice] = "New Bid Added"
# you should be using restful routes, this almost works, but is ugly and deprecated.
# it doesn't work becasue #bid.auction_id is never set. In fact you never use
# the auction_id any where, which was in your params_hash as params[:id]
redirect_to :action => "view_auction", :id => #bid.auction_id
end
end
...
Here's how your controller should work. First of all, this should be in the bids_controller, not auctions_controller
app/controllers/bids_controller.rb
...
def create
#bid = Bid.new(params[:bid]) # absorb values from form via params
#bid.bidder = current_user # link bid to current_user.
#auction = bid.auction based on.
# #auction is set, set because we added it to the #bid object the form was based on.
if #bid.save
flash[:notice] = "New Bid Added"
redirect_to #auction #assumes there is a show method in auctions_controller
else
render "auctions/show" # or what ever you called the above view
end
end
...
You'll also need to make sure the following is in your routes.rb (in addition to what may already be there. These few lines will set you up with RESTful routes.
config/routes.rb
ActionController::Routing::Routes.draw do |map|
...
map.resources :auctions
map.resources :bids
...
end
In any case you weren't far off. It seems you're off to a decent start, and could probably benefit from reading a book about rails. Just blindly following tutorials doesn't do you much good if you don't understand the underlying framework. It doesn't help that 90% of the tutorials out there are for older versions of rails and way out of date.
A lot of your code is the old way of doing things. Particularly redirect_to :action => "view_auction", :id => #bid.auction_id and <% form_for :bid, :url => {:controller => "auction", :action => "add_bids"} do |f|%>. With RESTful routing, they become redirect_to #auction and <% form_for #auction.bid.new do |f| %>`
Here's something resources you should read up on:
ActiveRecord::Associations: defines has_many, belongs_to, their options, and the convenience methods they add.
Understanding MVC: Provides a better understanding of the flow of information as it relates to Rails
RESTful resources: Understanding resources.
Form Helpers: In depth description of form_for and why the above code works.
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
I have a data model something like this:
# columns include collection_item_id, collection_id, item_id, position, etc
class CollectionItem < ActiveRecord::Base
self.primary_key = 'collection_item_id'
belongs_to :collection
belongs_to :item
end
class Item < ActiveRecord::Base
has_many :collection_items
has_many :collections, :through => :collection_items, :source => :collection
end
class Collection < ActiveRecord::Base
has_many :collection_items, :order => :position
has_many :items, :through => :collection_items, :source => :item, :order => :position
end
An Item can appear in multiple collections and also more than once in the same collection at different positions.
I'm trying to create a helper method that creates a menu containing every item in every collection. I want to use the collection_item_id to keep track of the currently selected item between requests, but I can't access any attributes of the join model via the Item class.
def helper_method( collection_id )
colls = Collection.find :all
colls.each do |coll|
coll.items.each do |item|
# !!! FAILS HERE ( undefined method `collection_item_id' )
do_something_with( item.collection_item_id )
end
end
end
I tried this as well but it also fails with ( undefined method `collection_item' )
do_something_with( item.collection_item.collection_item_id )
Edit: thanks to serioys sam for pointing out that the above is obviously wrong
I have also tried to access other attributes in the join model, like this:
do_something_with( item.position )
and:
do_something_with( item.collection_item.position )
Edit: thanks to serioys sam for pointing out that the above is obviously wrong
but they also fail.
Can anyone advise me how to proceed with this?
Edit: -------------------->
I found from online documentation that using has_and_belongs_to_many will attach the join table attributes to the retreived items, but apparently it is deprecated. I haven't tried it yet.
Currently I am working on amending my Collection model like this:
class Collection < ActiveRecord::Base
has_many :collection_items, :order => :position, :include => :item
...
end
and changing the helper to use coll.collection_items instead of coll.items
Edit: -------------------->
I've changed my helper to work as above and it works fine - (thankyou sam)
It's made a mess of my code - because of other factors not detailed here - but nothing that an hour or two of re-factoring wont sort out.
In your example you have defined in Item model relationship as has_many for collection_items and collections the generated association method is collection_items and collections respectively both of them returns an array so the way you are trying to access here is wrong. this is primarily case of mant to many relationship. just check this Asscociation Documentation for further reference.
do_something_with( item.collection_item_id )
This fails because item does not have a collection_item_id member.
do_something_with( item.collection_item.collection_item_id )
This fails because item does not have a collection_item member.
Remember that the relation between item and collection_items is a has_many. So item has collection_items, not just a single item. Also, each collection has a list of collection items. What you want to do is probably this:
colls = Collection.find :all
colls.each do |coll|
coll.collection_items.each do |collection_item|
do_something_with( collection_item.id )
end
end
A couple of other pieces of advice:
Have you read the documentation for has_many :through in the Rails Guides? It is pretty good.
You shouldn't need the :source parameters in the has_many declarations, since you have named your models and associations in a sensible way.
I found from online documentation that using has_and_belongs_to_many will attach the join table attributes to the retreived items, but apparently it is deprecated. I haven't tried it yet.
I recommend you stick with has_many :through, because has_and_belongs_to_many is more confusing and doesn't offer any real benefits.
I was able to get this working for one of my models:
class Group < ActiveRecord::Base
has_many :users, :through => :memberships, :source => :user do
def with_join
proxy_target.map do |user|
proxy_owner = proxy_owner()
user.metaclass.send(:define_method, :membership) do
memberships.detect {|_| _.group == proxy_owner}
end
user
end
end
end
end
In your case, something like this should work (haven't tested):
class Collection < ActiveRecord::Base
has_many :collection_items, :order => :position
has_many :items, :through => :collection_items, :source => :item, :order => :position do
def with_join
proxy_target.map do |items|
proxy_owner = proxy_owner()
item.metaclass.send(:define_method, :join) do
collection_items.detect {|_| _.collection == proxy_owner}
end
item
end
end
end
end
Now you should be able to access the CollectionItem from an Item as long as you access your items like this (items.with_join):
def helper_method( collection_id )
colls = Collection.find :all
colls.each do |coll|
coll.items.with_join.each do |item|
do_something_with( item.join.collection_item_id )
end
end
end
Here is a more general solution that you can use to add this behavior to any has_many :through association:
http://github.com/TylerRick/has_many_through_with_join_model
class Collection < ActiveRecord::Base
has_many :collection_items, :order => :position
has_many :items, :through => :collection_items, :source => :item, :order => :position, :extend => WithJoinModel
end