rails associations :autosave doesn't seem to working as expected - ruby-on-rails

I made a real basic github project here that demonstrates the issue. Basically, when I create a new comment, it is saved as expected; when I update an existing comment, it isn't saved. However, that isn't what the docs for :autosave => true say ... they say the opposite. Here's the code:
class Post < ActiveRecord::Base
has_many :comments,
:autosave => true,
:inverse_of => :post,
:dependent => :destroy
def comment=(val)
obj=comments.find_or_initialize_by(:posted_at=>Date.today)
obj.text=val
end
end
class Comment < ActiveRecord::Base
belongs_to :post, :inverse_of=>:comments
end
Now in the console, I test:
p=Post.create(:name=>'How to groom your unicorn')
p.comment="That's cool!"
p.save!
p.comments # returns value as expected. Now we try the update case ...
p.comment="But how to you polish the rainbow?"
p.save!
p.comments # oops ... it wasn't updated
Why not? What am I missing?
Note if you don't use "find_or_initialize", it works as ActiveRecord respects the association cache - otherwise it reloads the comments too often, throwing out the change. ie, this implementation works
def comment=(val)
obj=comments.detect {|obj| obj.posted_at==Date.today}
obj = comments.build(:posted_at=>Date.today) if(obj.nil?)
obj.text=val
end
But of course, I don't want to walk through the collection in memory if I could just do it with the database. Plus, it seems inconsistent that it works with new object but not an existing object.

Here is another option. You can explicitly add the record returned by find_or_initialize_by to the collection if it is not a new record.
def comment=(val)
obj=comments.find_or_initialize_by(:posted_at=>Date.today)
unless obj.new_record?
association(:comments).add_to_target(obj)
end
obj.text=val
end

I don't think you can make this work. When you use find_or_initialize_by it looks like the collection is not used - just the scoping. So you are getting back a different object.
If you change your method:
def comment=(val)
obj = comments.find_or_initialize_by(:posted_at => Date.today)
obj.text = val
puts "obj.object_id: #{obj.object_id} (#{obj.text})"
puts "comments[0].object_id: #{comments[0].object_id} (#{comments[0].text})"
obj.text
end
You'll see this:
p.comment="But how to you polish the rainbow?"
obj.object_id: 70287116773300 (But how to you polish the rainbow?)
comments[0].object_id: 70287100595240 (That's cool!)
So the comment from find_or_initialize_by is not in the collection, it outside of it. If you want this to work, I think you need to use detect and build as you have in the question:
def comment=(val)
obj = comments.detect {|c| c.posted_at == Date.today } || comments.build(:posted_at => Date.today)
obj.text = val
end

John Naegle is right. But you can still do what you want without using detect. Since you are updating only today's comment you can order the association by posted_date and simply access the first member of the comments collection to updated it. Rails will autosave for you from there:
class Post < ActiveRecord::Base
has_many :comments, ->{order "posted_at DESC"}, :autosave=>true, :inverse_of=>:post,:dependent=>:destroy
def comment=(val)
if comments.empty? || comments[0].posted_at != Date.today
comments.build(:posted_at=>Date.today, :text => val)
else
comments[0].text=val
end
end
end

Related

Mongoid destroy method not working with has_and_belongs_to_many relationship

I'm using Mongoid version 2.8.1. I've noticed that the destroy method does not work when the model has a has_and_belongs_to_many relationship.
For example, I have two models
class Article
include Mongoid::Document
has_and_belongs_to_many :subjects
...
end
and
class Subject
include Mongoid::Document
has_and_belongs_to_many :articles
...
end
Now I want to delete an article document. So I tried
a = Article.find('someid1234')
this returns a valid object, then I do
>> a.destroy
=> true
>> a.errors.any?
=> false
>> a.errors.count
=> 0
But when I do
a.reload
I still get the object back!
If I use
a.delete
instead, it would work, but delete doesn't run callbacks, and I want to run callbacks.
I have nailed this down to the has_and_belongs_to_many relationship. Because of this relationship, destroy invokes a callback method.
Article._destroy_callbacks
=> [#<ActiveSupport::Callbacks::Callback:0x007fc4a0e71258 #klass=Shortlist, #kind=:after, #chain=[...], #per_key={:if=>[], :unless=>[]}, #options={:if=>[], :unless=>[]}, #raw_filter=#<Proc:0x007fc4a0e714d8#/opt/boxen/rbenv/versions/1.9.3-p392/lib/ruby/gems/1.9.1/gems/mongoid-2.4.12/lib/mongoid/relations/synchronization.rb:160>, #filter="_callback_after_31(self)", #compiled_options="true", #callback_id=32>]
The method in question in /opt/boxen/rbenv/versions/1.9.3-p392/lib/ruby/gems/1.9.1/gems/mongoid-2.4.12/lib/mongoid/relations/synchronization.rb:160
is
def synced_destroy(metadata)
tap do
set_callback(
:destroy,
:after
) do |doc|
doc.remove_inverse_keys(metadata)
end
end
end
After I call a.destroy, a's id is removed from its subjects' article_ids array fields. But article a is not destroyed. So it seems the callback executes correctly, but the object is not destroyed afterwards.
I debugged this problem by looking at mongoid's source code. The destroy method looks like
def destroy(options = {})
self.flagged_for_destroy = true
run_callbacks(:destroy) do
remove(options) # THIS IS NOT EXECUTED!
end.tap do
self.flagged_for_destroy = false
end
end
def remove(options = {})
Operations.remove(self, options).persist
end
alias :delete :remove
The comment is mine. This seems to be a bug with Mongoid, that destroy only executes the callbacks, and does not destroy the object itself.
However, when there's no callback methods for destroy (for example, on a model without
the has_and_belongs_to_many relationship), the object is destroyed correctly. Strange
Has anyone has experienced the same problem and if there's any workaround.
Thanks.
It could be that :article must be referenced instead of Article. Keep in mind that doing Article.new doesn't automatically give a relation. The mongoid relation doc says you need something like the following
class Person
include Mongoid::Document
embeds_many :addresses
end
person.addresses = [ address ]
a work around is add manual deletes to the modals
after_destroy :delete_self!
def delete_self!
if persisted?
self.delete
end

Using a method within model, calling it from view

I have an Update model which belongs to users.
To show all of one user's friends' Updates, I am doing something like:
Update.where("user_id" => [array_of_friend_ids])
I know the "right" way of doing things is to create a method to create the above array. I started writing the method but it's only half-working. Currently I have this in my user model:
def self.findfriends(id)
#friendarray = []
#registered_friends = Friend.where("user_id" => id)
#registered_friends.each do |x|
#friendarray << x.friend_id
end
return #friendarray
end
I am doing the entire action in the view with:
<% #friendinsert = User.findfriends(current_user.id) %>
<% #friendarray = [] %>
<% #friendarray << #friendinsert %>
<%= #friendarray.flatten! %>
Then I'm calling Update.where("user_id" => #friendarray) which works. But obviously I'm doing things in a very hacky way here. I'm a bit confused as to when Rails can "see" certain variables from models and methods in the view. What's the best way to go about inserting an array of IDs to find their Updates, since I'm not supposed to use much logic in the view itself?
Mattharick is right about using associations. You should use associations for the question you mentioned in description of your question. If we come to the question at the title of your question;
let's say you have a User model.
These two methods are different:
def self.testing
puts "I'm testing"
end
and the other one is:
def testing
puts "I'm testing"
end
Pay attention to the self keyword. self keyword makes method a Class method. Which you can call it from your controllers or views like: User.testing.
But the one with out testing is a instance method. Which can be called like:
u = User.last
u.testing
Second one gives you possibility to use attributes of the 'instance' inside your model.
For example, you can show name of your instance in that method just like this?
def testing
puts "Look, I'm showing this instance's name which is: #{name}"
end
These are powerful stuff.
Practise on them.
Simple add another association to your project.
class User < ActiveRecord::Base
has_many :friendship
has_many :friends, :through => :friendship, :class_name => User, :foreign_key => :friend_id
has_many :friendship
has_many :users, :through => :friendship
end
class Friendship < ActiveRecord::Base
belongs_to :user
belongs_to :friend, :class_name => User
end
I don't know if my synrax is correct, please try out.
Friendship has the attributes user_id and friend_id.
After that you should be able to do something like following to get the updates of a friend:
User.last.friends.last.updates
You can work with normal active record queries instead of hacky arrays..

as_json not calling as_json on associations

I have a model with data that should never be included when it is rendered as json. So I implemented the class' as_json method to behave appropriately. The problem is when other models with associations with this model render json, my custom as_json is not being called.
class Owner < ActiveRecord::Base
has_one :dog
def as_json(options={})
puts "Owner::as_json"
super(options)
end
end
class Dog < ActiveRecord::Base
belongs_to :owner
def as_json(options={})
puts "Dog::as_json"
options[:except] = :secret
super(options)
end
end
Loading development environment (Rails 3.0.3)
ruby-1.9.2-p136 :001 > d = Dog.first
=> #<Dog id: 1, owner_id: 1, name: "Scooby", secret: "I enjoy crapping everwhere">
ruby-1.9.2-p136 :002 > d.as_json
Dog::as_json
=> {"dog"=>{"id"=>1, "name"=>"Scooby", "owner_id"=>1}}
ruby-1.9.2-p136 :004 > d.owner.as_json(:include => :dog)
Owner::as_json
=> {"owner"=>{"id"=>1, "name"=>"Shaggy", :dog=>{"id"=>1, "name"=>"Scooby", "owner_id"=>1, "secret"=>"I enjoy crapping everwhere"}}}
Thanks for the help
This is a known bug in Rails. (The issue is marked closed due to the migration to Github issues from the previous bug tracker, but it's still a problem as of Rails 3.1.)
As acknowledged above, this is an issue with the Rails base. The rails patch here is not yet applied and seems at least slightly controversial, so I'm hesitant to apply it locally. Even if applied as a monkey patch it could potentially complicate future rails upgrades.
I'm still considering RABL suggested above, it looks useful. For the moment, I'd rather not add another view templating language into my app. My current needs are very small.
So here's a workaround which doesn't require a patch and work for most simple cases. This works where the association's as_json method you'd like to have called looks like
def as_json(options={})
super( <... custom options ...> )
end
In my case I've got Schedule model which has many Events
class Event < ActiveRecord::Base
# define json options as constant, or you could return them from a method
EVENT_JSON_OPTS = { :include => { :locations => { :only => [:id], :methods => [:name] } } }
def as_json(options={})
super(EVENT_JSON_OPTS)
end
end
class Schedule < ActiveRecord::Base
has_many :events
def as_json(options={})
super(:include => { :events => { Event::EVENT_JSON_OPTS } })
end
end
If you followed the guideline that anytime you :include an association in your as_json() methods, you define any options you need as a constant in the model to be referenced, this would work for arbitrary levels of associations. NOTE I only needed the first level of association customized in the above example.
I've found that serializable_hash works just as you'd expect as_json to work, and is always called:
def serializable_hash(options = {})
result = super(options)
result[:url] = "http://.."
result
end
I ran into the same issue. I wanted this to work:
render :json => #favorites.as_json(:include => :location)
But it didn't so I ended up adding an index.json.erb with the following:
<% favs = #favorites.as_json.each do |fav| %>
<% fav["location"] = Location.find(fav["location_id"]).as_json %>
<% end %>
<%= favs.to_json.html_safe %>
Not a fix - just a work around. I imagine you did the same thing.
Update #John pointed out this is a known bug in Rails. A patch to fix it appears to be: at https://github.com/rails/rails/pull/2200. Nevertheless, you might try RABL, because its sweet.
I've always been frustrated with passing a complex set of options to create the JSON views I want. Your problem, which I experienced with Mongoid in Rails 3.0.9, prompted me to write JSON templates. But actually, if you're dealing with relations or custom api properties, it turns out that templates are way nicer.
Besides, dealing with different outputs seems like the View layer to me, so I settled on using RABL, the API templating language. It makes it super easy to build valid JSON and include any associations or fields.
Not a solution to the problem, but a better solution for the use case.
This was reported as a bug: http://ternarylabs.com/2010/09/07/migrating-to-rails-3-0-gotchas-as_json-bug/

How to check if a record exists before creating a new one in rails3?

Heres what I'm trying to accomplish:
I have a tagging system in place.
Tags are created, when Posts are created (posts has_many :tags, :through => :tag_joins.
A tag join is automatically created when a post is created with tags).
I want to check if the tag already exists. If it does I want to use the existing tag for the tag_join record, rather than creating a new tag record.
Here is my current code, which isn't working.
class Tag < ActiveRecord :: Base
belongs_to :user
belongs_to :tag_join
belongs_to :post
before_create :check_exists
def check_exists
tag = Tag.where(:name => self.name, :user_id => current_user.id)
if tag.nil?
tag = Tag.create(:name => self.name, :user_id => current_user.id)
end
end
end
This doesn't work though, I'm getting an error upon task creation...(the server is actually just timing out - I don't receive a specific error).
Any ideas?
Tokland said I was creating an infinite loop by telling it to create tag again - so I tried this:
def check_exists
tag = Tag.find_by_name_and_user_id(:name => self.name, :user_id => current_user.id)
if tag != nil
self.id = tag.id
end
end
And still get the server timeout
Edit: I'm not sure if this matters, but the way the tags are being added is similar to "http://railscasts.com/episodes/73-complex-forms-part-1
they're nested in the post form, and use something like this:
def tag_attributes=(tag_attributes)
tag_attributes.each do |attributes|
tags.build(attributes)
end
end
I'm wondering if this is stopping this whole thing from working? Also, using current_user.id in the model definitely seems to be an issue...
EDIT:
Something I have figured out:
this had to change, the format we were using before was incorrect syntax - generally used for a .where method.
def check_exists
#tag = Tag.find_by_name_and_user_id(self.name, self.user_id)
if #tag != nil
#return false
#self=#tag
end
end
The problem now is this, I can learn if it the tag already exists. But then what? If I go with the return false option, there is an error upon post creation, and the join record isn't created... The other option "self=#tag" obviously just doesn't work.
You're going to find it hard to to this from within the Tag model. It seems like what you want is to update the Post using nested attributes, like so:
post = Post.create
post.update_attributes(:tags_attributes=>{"0"=>{:name=>"fish",:user_id=>"37"}})
This is actually pretty simple to do by using a virtual attribute setter method:
class Post < AR::Base
has_many :tags
def tags_attributes=(hash)
hash.each do |sequence,tag_values|
tags << Tag.find_or_create_by_name_and_user_id(tag_values[:name],\
tag_values[:user_id])
end
end
> post = Post.create
> post.update_attributes(:tags_attributes=>{"0"=>{:name=>"fish",:user_id=>"37"}})
> Tag.count # => 1
# updating again does not add dups
> post.update_attributes(:tags_attributes=>{"0"=>{:name=>"fish",:user_id=>"37"}})
> Tag.count # => 1
There's a find_or_create_by_ function built right in to Rails
# No 'Summer' tag exists
Tag.find_or_create_by_name("Summer") # equal to Tag.create(:name => "Summer")
# Now the 'Summer' tag does exist
Tag.find_or_create_by_name("Summer") # equal to Tag.find_by_name("Summer")
http://api.rubyonrails.org/classes/ActiveRecord/Base.html (under Dynamic attribute-based finders)
You want to use the magic method find_or_create_by
def check_exists
tag = Tag.find_or_create_by_name_and_user_id(:name => self.name, :user_id => current_user.id)
end
Check out the ActiveRecord::Base docs for more info
The question I originally asked got pretty distorted by the end. So I'm separating it.
People who are trying to do what I originally asked can try this:
before_create :check_tag_exists
private
def check_tag_exists
#tag = Tag.find_by_name_and_user_id(self.name, self.user_id)
if #tag != nil
#
end
end
This will enable you to check if your record has already been created. Any further logic you can drop in that if statment.
I believe the other answers are a bit dated. Here's how you should probably accomplish this for Rails 4
tag = Tag.first_or_initialize(:name => self.name, :user_id => current_user.id)
if !tag.new_record?
tag.id = self.id
tag.save
end
try this
def check_exists
tag = Tag.where(:name => self.name, :user_id => current_user.id).first
tag = Tag.new({:name => self.name, :user_id => current_user.id}) unless tag
end
use Tag.new instead of Tag.create
where returns an empty ActiveRecord on finding no match.

Method ignoring parameter value in Ruby on Rails, using default value instead

I'm having some issues in RoR with some model methods I am setting. I'm trying to build a method on one model, with an argument that gets supplied a default value (nil). The ideal is that if a value is passed to the method, it will do something other than the default behavior. Here is the setup:
I currently have four models: Market, Deal, Merchant, and BusinessType
Associations look like this:
class Deal
belongs_to :market
belongs_to :merchant
end
class Market
has_many :deals
has_many :merchants
end
class Merchant
has_many :deals
belongs_to :market
belongs_to :business_type
end
class BusinessType
has_many :merchants
has_many :deals, :through => :merchants
end
I am trying to pull some data based on Business Type (I have greatly simplified the return, for the sake of brevity):
class BusinessType
def revenue(market=nil)
if market.nil?
return self.deals.sum('price')
else
return self.deals(:conditions => ['market_id = ?',market]).sum('price')
end
end
end
So, if I do something like:
puts BusinessType.first.revenue
I get the expected result, that is the sum of the price of all deals associated with that business type. However, when I do this:
puts BusinessType.first.revenue(1)
It still returns the sum price of all deals, NOT the sum price of all deals from market 1. I've also tried:
puts BusinessType.first.revenue(market=1)
Also with no luck.
What am I missing?
Thanks!
Try this:
class BusinessType
def revenue(market=nil)
if market.nil?
return self.deals.all.sum(&:price)
else
return self.deals.find(:all, :conditions => ['market_id = ?',market]).sum(&:price)
end
end
end
That should work for you, or at least it did for some basic testing I did first.
As I have gathered, this is because the sum method being called is on enumerable, not the sum method from ActiveRecord as you might have expected.
Note:
I just looked a bit further, and noticed you can still use your old code with a smaller tweak than the one I noted:
class BusinessType
def revenue(market=nil)
if market.nil?
return self.deals.sum('price')
else
return self.deals.sum('price', :conditions => ['market_id = ?', market])
end
end
end
Try this!
class BusinessType
def revenue(market=nil)
if market.nil?
return self.deals.sum(:price)
else
return self.deals.sum(:price,:conditions => ['market_id = ?',market])
end
end
end
You can refer this link for other functions. http://en.wikibooks.org/wiki/Ruby_on_Rails/ActiveRecord/Calculations

Resources