If I have read the rails guides correctly, a before_destroy callback that returns false will stop the object being destroyed by issuing a rollback command.
However, while the object itself is not being removed I'm finding that all objects in a HABTM relationship on that object are. How can I stop this from happening?
Here's the appropriate code block:
class UserGroup < ActiveRecord::Base
# Associations
has_and_belongs_to_many :users, :join_table => "user_group_membership"
attr_protected :is_default
# Callbacks
before_destroy :destroy_associations
def destroy_associations
if self.is_default?
errors.add(:base,"You can't delete the default")
return false
end
self.users.clear
end
end
Now when I call destroy on a "is_default" group, I get the correct error message added to the base, the UserGroup object is NOT destroyed but every association in the :users collection is cleared.
I know that the code "self.users.clear" is never reached - so why am I losing my HABTM collection?
If it helps, I am using JRuby 1.9 with an mssql database.
Related
I have the following factory:
factory :store do
room
factory :store_with_items do
ignore do
items_count 4
end
after(:create) do |store, evaluator|
FactoryGirl.create_list(:equippable_item, evaluator.items_count, store: store)
end
end
end
Next, I create an object:
#store = FactoryGirl.create :store_with_items
My problem is that when I "delete" one of the store's items, the store still shows that it has 4 items.
#store.items[0].store_id = nil
#store.save!
puts #store.items.size
The puts is 4. How do I properly delete an item? Isn't this how you would do it in rails?
I used to prefer this approach, but now I avoid it; its easier and more flexible to let factories be simple and populate has_many associations at runtime.
Try this
Factory for store (same):
factory :store do
room
end
Factory for items:
factory :item do
store # will use the store factory
end
Then in my test I would populate what is appropriate for the case at hand:
#store = FactoryGirl.create :store
#item1 = FactoryGirl.create :item, store: #store
#item2 = FactoryGirl.create :equippable_item_or_whatever_factory_i_use, store: #store
To explain
By passing in the store instance explicitly, the association will be setup for you. This is because when you pass something explicitly in FactoryGirl.create or FactoryGirl.build it overrides whatever is defined in the factory definition. It even works with nil. This way, you'll have real object instances that give you all the real functionality.
To test destroy
I think the code in your example is not good; it breaks the association between store and item, but doesn't actually remove the item record so you're leaving behind an orphan record. I would do this instead:
#store.items[0].destroy
puts #store.items.size
Bonus
You probably also want to setup your child associations to be destroyed when the parent is destroyed if its not already. This would mean when you say #store.destroy all the items belonging to it will also be destroyed (removed from the db.)
class Store < ActiveRecord::Base
has_many :items, dependent: :destroy
.....
end
I have associated models like this:
class Batch
has_many :logs
class Log
belongs_to :batch
I'm using includes to load batches with logs:
b = Batch.includes(:logs)
Which runs 2 selects as expected (batches and logs).
Then I do
b.first.logs.first.batch
and this triggers another select on batches, even when they were actually loaded already.
I figured to "fix" it by doing includes(:logs => :batch) but I'm still thinking that something is wrong here because the same batches are loaded twice. What gives?
You can fix this with the :inverse_of setting, which lets ActiveRecord know that the two associations are the inverse of each other.
class Batch
has_many :logs, :inverse_of => :batch
end
class Log
belongs_to :batch, :inverse_of => :logs
end
I'm trying to figure out a way to partially delete/destroy dependent models in rails.
Code looks something like this:
class User < ActiveRecord::Base
has_many :subscriptions
has_many :photos, :dependent => :destroy
has_many :badges, :dependent => :destroy
before_destroy :partial_destroy
def partial_destroy
self.photos.destroy_all
self.badges.destroy_all
return false if self.subscriptions.any?
end
...
Essentially, I want to destroy the photos and badges, but if the user has any subscriptions, I want to keep those, and also keep the user from being destroyed.
I tried with .each { |obj| obj.destroy } and using delete and delete_all, but it seems to not matter.
It looks like rails is performing some kind of a rollback whenever the before_destroy returns false. Is there a way to destroy part of the dependents but not others?
This is old so I expect you've forgotten it, but I stumbled across it.
I'm not surprised delete and delete_all didn't work, since those bypass callbacks.
You're exactly right that Rails performs a rollback if any before_ callback returns false. Because Rails wraps the entire callback chain in a transaction, you're not going to be able to perform database calls (like destroys) inside the chain. What I would recommend is putting a conditional in the callback:
If the user has subscriptions, kick off a background job which will do this partial delete later (outside the callback transaction), and return false from the callback.
If they don't have subscriptions, you don't start the background job, return true from the callback, and destroy your model as usual.
I ended up doing the following:
override destroy on the User model (see below)
not actually deleting the User, but rather destroying the dependants that are not needed, and blanking any fields on the User model itself, e.g. email.
I created a UserDeleter class that takes the user and performs all clearing operations, just to keep things cleaner / having some kind of single-responsibility
overriding destroy
def destroy
run_callbacks(:destroy) do
UserDeleter.new(self).delete
end
end
deleting dependants and clearing data on User
class UserDeleter
def initialize(user)
#user = user
end
def delete
delete_photos
delete_badges
clear_personal_data
# ...
end
private
def delete_photos
#user.photos.destroy_all
end
def clear_personal_data
#user.update_attributes!(
:email => deleted_email,
:nickname => '<deleted>')
end
def deleted_email
"deleted##{random_string}.com"
end
def random_string(length = 20)
SecureRandom.hex(length)[0..length]
end
#...
end
I'm trying to make a basic checkout page, and here's what I have so far:
The checkout is hosted off of transactions#new, and the form is built off of a new Transaction object. Transaction has a number of nested models underneath it:
class Transaction < ActiveRecord::Base
# ...
accepts_nested_attributes_for :user, :shipping_address, :products
# ...
end
User, Product, and Location (Shipping Address) can be persisted when you arrive at the checkout page, depending on the user flow. Product is always persisted upon arriving at the checkout page.
This setup works for me so far except on the failure cases. I've been trying to re-create the new Transaction record (with the previously entered in user info) to display the appropriate error messages, and I had tried doing this in my controller:
class TransactionsController < ApplicationController
def new
#transaction = Transaction.new
end
def create
#transaction = Transaction.new params[:transaction]
# ...
end
end
But I'm getting this error:
ActiveRecord::RecordNotFound in TransactionsController#create
Couldn't find Product with ID=1 for Transaction with ID=
Request Parameters
{"utf8"=>"✓", "authenticity_token"=>"blahblahblah",
"transaction"=>{"products_attributes"=>{"0"=>{"id"=>"1",
"quantity"=>"1"}}}}
Does anyone know what's up with this? Let me know if you need anymore info about my setup here... tried to pare this issue down to the bare essentials...
class Transaction < ActiveRecord::Base
has_many :product_transactions
has_many :products, :through => :product_transactions
end
and
class Product < ActiveRecord::Base
has_many :product_transactions
has_many :transactions, :through => :product_transactions
end
and
class ProductTransaction < ActiveRecord::Base
belongs_to :transaction
belongs_to :product
end
So, the reason you're getting that error is because you're supplying an id with products_attributes, since you're using accepts_nested_attributes_for the product with that id HAS to already be in the association. This is because the products_attributes= method is expecting to either create or modify the records in the products association.
Since the Product is already persisted and you're just trying to create the ProductTransaction you would need change your accepts_nested_attributes_for to include :product_transactions instead.
This part of your question threw me off
User, Product, and Location (Shipping Address) can be persisted when you arrive at the checkout page, depending on the user flow. Product is always persisted upon arriving at the checkout page.
I don't know if you need to be able to define a product... But if you need to create a Product on the checkout page it would make more sense to define it in the context of a ProductTransaction (ie. ProductTransaction accepts product_attributes or product_id) instead of the context of a Transaction.
Suppose I have the following model relationship:
class Player < ActiveRecord::Base
has_many :cards
end
class Card < ActiveRecord::Base
belongs_to :player
end
I know from this question that Rails will return me a copy of the object representing a database row, meaning that:
p = Player.find(:first)
c = p.cards[0]
c.player.object_id == p.object_id # => false
...and therefore if the Player model modifies self, and the Card model modifies self.player in the same request, then the modifications won't take any notice of each other and the last-saved one will overwrite the others.
I'd like to work around this (presumably with some form of caching), so that all requests for a Player with a given id would return the same object (identical object_ids), thereby allowing both models to edit the same object without having to perform a database save-and-reload. I have three questions:
Is there already a plugin or gem to do this?
Are there good reasons why I shouldn't do this?
Can anyone give me some pointers on how to go about doing this?
This is supported in Rails 3.x. You can use the :inverse_of option for the has_many association for example. Documentation here (search for :inverse_of and Bi-directional associations).