I want to build associated records for non-persisten record in rails.
Let's say I have these models:
class Post
has_many :comments
end
I want to do something like this:
p = Post.new(text: 'Some post')
p.build_comment(content: 'some comment')
It says
undefined method `build_comment' for #<Post
It works ok for has_one association though
Thanks in advance
In has_many association, you have to do it like this
p = Post.new(text: 'Some post')
p.comments.build(content: 'some comment')
Related
I essentially have multiple models with a belongs_to association to another model, but I don't want to define all the has_many relationships on the parent model. Is there a way to create and link this association in one call?
class Thing < ActiveRecord::Base
# does not have any has_many or has_one associations defined
end
class Comment < ActiveRecord::Base
belongs_to :thing
end
# in a controller...
thing = #comment.build_thing(thing_params)
if thing.save
# thing was created but #comment.thing_id was not updated
end
if thing.save && #comment.update(thing: thing)
# this works, but requires an extra call to update the comment
else
# now we would have to check which model failed to save
end
Is there a simple way to do this that I am missing?
has_many association wouldn't change anything (unless :inverse_of is set), you have to save comment either way:
# i assume `belongs_to :thing` is optional v
comment = Comment.create #=> #<Comment:0x00007fec1b612088 id: 1, thing_id: nil>
comment.build_thing #=> #<Thing:0x00007ff8ba420788 id: nil>
comment.save
comment.thing_id #=> 1
comment.thing #=> #<Thing:0x00007ff8ba420788 id: 1>
or like this:
Comment.create(thing: Thing.new) #=> #<Comment:0x00007ff8ba35c1d0 id: 2, thing_id: 2>
I have model with polymorphic association.
class Tag < ActiveRecord::Base
#attributes target_id, target_type
belongs_to :target, polymorphic: true
end
And Target model, which is user.
class User < ActiveRecord::Base
has_many :tags, as: :target
end
But method #user = User.find params[:id]; #user.tags returns #<ActiveRecord::Associations::CollectionProxy []> while Tag.where(target_id: #user.id, target_type: 'User') returns some objects I except.
What's wrong?
It's a wrong way because it gives error like NameError: undefined local variable or method user for main:Object, first you have to find user and it's tags like:
#user=User.find(1)
#user.tags
From an instance of the User model, you can retrieve a collection of tags like this:
#user = User.find(params[:id])
#user.tags
Similarly, if you have an instance of the Tag model, you can get to its parent:
#tag.target
Problem was solved!
I just add to model foreign_key.
class User < ActiveRecord::Base
has_many :tags, as: :target,
foreign_key: target_id
end
Thanks everyone, who try to help.
I have the following hierarchy of models where each one has_many of the one below it:
class AccountGroup < ActiveRecord::Base
has_many :accounts, :inverse_of=>:account_group
# name: string
class Account < ActiveRecord::Base
belongs_to :accountGroup, :inverse_of=>:account
has_many :positions, :inverse_of=>:account
class Position < ActiveRecord::Base
belongs_to :account, :inverse_of=>:positions
# net_position: integer
In other words, an AccountGroup contains a bunch of Accounts, and an Account contains a bunch of Positions.
Goal: I want an hash of AccountGroup => (sum of its net_positions). That means there's a GROUP BY involved.
I can do this with raw SQL, but I haven't cracked it with Rails functions. The raw SQL is:
SELECT account_groups.id,SUM(net_position),account_groups.name
FROM account_groups
LEFT JOIN accounts ON accounts.account_group_id = account_groups.id
LEFT JOIN positions ON positions.account_id = accounts.id
GROUP BY account_groups.id,account_groups.name;
Is this something that Rails just can't do?
Rails (4.0.0) can do this - we have two ways to do it currently:
1. SQL "Alias" Columns
Rails Scoping For has_many :through To Access Extra Data
#Images
has_many :image_messages, :class_name => 'ImageMessage'
has_many :images, -> { select("#{Image.table_name}.*, #{ImageMessage.table_name}.caption AS caption") }, :class_name => 'Image', :through => :image_messages, dependent: :destroy
2. ActiveRecord Association Extensions
This is a little-known feature of Rails, which allows you to play with the collection object. The way it does it is to extend the has_many relationship you have created:
class AccountGroup < ActiveRecord::Base
has_many :accounts do
def X
#your code here
end
end
end
We have only got this method working for collections, but you can do all sorts with it. You should look at this tutorial to see more about it
Update
We just got this working by using an extension module:
#app/models/message.rb
Class Message < ActiveRecord::Base
has_many :image_messages #-> join model
has_many :images, through: :image_messages, extend: ImageCaption
end
#app/models/concerns/image_caption.rb
module ImageCaption
#Load
def load
captions.each do |caption|
proxy_association.target << caption
end
end
#Private
private
#Captions
def captions
return_array = []
through_collection.each_with_index do |through,i|
associate = through.send(reflection_name)
associate.assign_attributes({caption: items[i]})
return_array.concat Array.new(1).fill( associate )
end
return return_array
end
#######################
# Variables #
#######################
#Association
def reflection_name
proxy_association.source_reflection.name
end
#Foreign Key
def through_source_key
proxy_association.reflection.source_reflection.foreign_key
end
#Primary Key
def through_primary_key
proxy_association.reflection.through_reflection.active_record_primary_key
end
#Through Name
def through_name
proxy_association.reflection.through_reflection.name
end
#Through
def through_collection
proxy_association.owner.send through_name
end
#Captions
def items
through_collection.map(&:caption)
end
#Target
def target_collection
#load_target
proxy_association.target
end
end
Props to this gist for the variable functions
This basically overrides the load ActiveRecord function in the CollectionProxy class, and uses it to create our own proxy_association.target array :)
If you need any information on how to implement, just ask in the comments
You can make this little bit more prettier than raw sql by using rails AR querying methods:
AccountGroup.
select("account_groups.id, SUM(net_position), account_groups.name").
joins("LEFT JOIN accounts ON accounts.account_group_id = account_groups.id").
joins("LEFT JOIN positions ON positions.account_id = accounts.id").
group("account_groups.id,account_groups.name")
This can be done with pure Arel as well.
AccountGroup.select(
AccountGroup.arel_table[:id], Arel::Nodes::NamedFunction.new('SUM', [:net_position]), AccountGroup.arel_table[:name]
).joins(
AccountGroup.arel_table.join(Account.arel_table).on(
Account.arel_table[:account_group_id].eq(AccountGroup.arel_table[:id])
).join_sources
).joins(
AccountGroup.arel_table.join(Position.arel_table).on(
Position.arel_table[:account_id].eq(Account.arel_table[:id])
).join_sources
).group(
AccountGroup.arel_table[:id], AccountGroup.arel_table[:name]
)
I'm not 100% sure this will work, I simply copied your SQL from above and put it into scuttle.io
Use include function, in example
ac = AccountGroup.all(:include => :account)
$ AccountGroup Load (0.6ms) SELECT `account_groups`.* FROM `groups`
$ Account Load (16.4ms) SELECT `accounts`.* FROM `accounts` WHERE `accounts`.`id` IN (1010, 3, 4, 202, 203, 204, 9999)
Then you can call ac.account.name or something like that
There are a great Railscast http://railscasts.com/episodes/22-eager-loading?view=asciicast
If you really want to use ActiveRecord for this (no SQL), it will be something like:
ags = AccountGroup.all(:include => {:accounts => :positions})
hash = Hash[ags.map { |ag| [ag, ag.map(&:accounts).flatten.map(&:positions).flatten.map(&:net_position).reduce(0,&:+)]}]
But it will be slower than your SQL, and not any prettier.
Is this something that Rails just can't do?
As this question has been open for about a month, I'm gonna to go ahead and assume the answer to this question is...
Yes.
EDIT: Yes, for Rails 3. But Rails 4 can do it! See accepted answer.
Rails can't do it, outside of using find_by_sql or ActiveRecord::Base.connection.execute(query), which are pretty kludgy and not rails-y.
I define a Post class which can link or be linked to multiple posts. To do this I added a PostLink class which specifies post_to and post_from.
I generated the PostLink class by rails g model post_link from_post:integer to_post:integer and of course rake db:migrate, and added
belongs_to :from_post, :class_name => 'Post'
belongs_to :to_post, :class_name => 'Post'
to the class.
And I also have has_many :post_links in my Post class.
I ran rails console and Post.new.post_links and got nil printed out, which is expected. However after I save a Post using
p = Post.new
p.save
and then run p.post_links, it prints out the following error message:
SQLite3::SQLException: no such column: post_links.post_id: SELECT "post_links".*
FROM "post_links" WHERE "post_links"."post_id" = 1
So anybody know why after saving it to the database post_link can not be accessed?
The has_many :post_links association in Post throws an error because it assumes the foreign key in post_links is post_id by default. Since you are using from_post_id and to_post_id, you will have to find a way to group the post_links for "from" posts and "to" posts to get the total set of post_links for a post.
One approach could be to define two associations on Post and an additional method to add the sets together:
class Post < ActiveRecord::Base
has_many :from_post_links, :class_name => 'PostLink', :foreign_key => :from_post_id
has_many :to_post_links, :class_name => 'PostLink', :foreign_key => :to_post_id'
def post_links
from_post_links + to_post_links
end
end
As another option, you could provide special sql to the association to group the sets in a single query:
class Post < ActiveRecord::Base
has_many :post_links, :finder_sql => Proc.new {
%Q{
SELECT *
FROM post_links pl
WHERE pl.to_post_id = #{id}
OR pl.from_post_id = #{id}
}
}
My model association is as follows:
#book model
class Book < ActiveRecord::Base
has_many :recommendations, :dependent => :destroy
has_many :similars, :through => :recommendations, :conditions => ['recommendation_type IS NULL'], :order => 'recommendations.created_at DESC'
#recommendation model
class Recommendation < ActiveRecord::Base
belongs_to :book
belongs_to :similar, :class_name => 'Book', :foreign_key => 'similar_id'
#Books_controller - injecting the recommendation_id
#book = Book.find(params[:id])
if params[:content_type]
#content_type = params[:content_type];
else
#content_type = "similars"
end
case #content_type
when "similars"
# get the similars
#book_content = #book.similars
#book_content.each do |similar|
#rec_id = Recommendation.where(:book_id=>similar.id, :recommendation_type=>'S').select('id').first.id
similar << {:rec_id => #rec_id}
# ^-- Above line gives NoMethodError (undefined method `<<' for #<Book:0x10de1f40>):
end
when "references"
# get the references
#book_content = #book.references
#book_content.each do |reference|
#rec_id = Recommendation.where(:book_id=>reference.id, :recommendation_type=>'R').select('id').first.id
reference << {:rec_id => #rec_id}
# ^-- Above line gives NoMethodError (undefined method `<<' for #<Book:0x10de1f40>):
end
end
So as noted above, A book has many similars through recommendations. My requirement is that while retrieving similars, I would also like to include the id of the corresponding record in the join table recommendations.
My questions are:
How can I include the field *recommendation_id* alongwith
similars?
If it cannot be included directly, then what is the correct way to
determine this field separately (as shown above) and then
inject it into the similars instance variable so that I can use
it directly in my views?
I recommend you read the Rails guide on associations, specifically about the has_many :through associations.
A lot of your code doesn't make sense - for example:
#book_similars = Book.similars
This means you have a class method on the Book model for similars, but you don't mention it being defined, or what it returns. Rails just doesn't work like this.