0

I've been creating a cart feature in rails and I have the following models:

Cart:

class Cart < ActiveRecord::Base
  has_many :items
end

Item:

class Item < ActiveRecord::Base
  belongs_to :cart
  belongs_to :product
end

An item also has a quantity attribute.

Now, I have an instance method on cart that given an item will either a) save the item to the database and associate it with the cart or b) if the item with the product_id already exists, simply update the quantity.

The code for this is below:

def add_item(item)
  if(item.valid?)
    current_item = self.items.find_by(product_id: item.product.id)
    if(current_item)
      current_item.update(quantity: current_item.quantity += item.quantity.to_i)
    else
      self.items << item
    end
    self.save
  end
end

And this works fine.

However, I wanted to test this in the console so i opened up the console in sandbox mode and ran the following commands:

cart = Cart.new #create a cart
cart.add_item(Item.new(product: Product.find(1), quantity: 5)) #Add 5 x Product 1
cart.items #lists items, I can see 5 x Product 1 at this point.

cart.add_item(Item.new(product: Product.find(1), quantity: 3)) #Add 3 x Product 1
cart.items #lists items, still shows 5 x Product 1, not 8x

cart.items.reload #reload collectionproxy
cart.items #lists items, now with 8x Product 1

Here i create a cart, add a purchase of 5 x Product 1 and i can see this in the cart.items. If then add another purchase of 3 x Product 1, the cart.items still lists the purchase as 5 x Product 1 until i manually reload the collection proxy.

I can add more products and these will show up, it is just when updating an existing one, it does not update the collection.

I have tests around this method too which pass.

before(:each) do
  @cart = create(:cart)
  @product = create(:product)
  @item = build(:item, product: @product, quantity: 2)
end

context "when the product already exists in the cart" do
  before(:each) {@cart.add_item(@item)}
  it "does not add another item to the database" do
    expect{@cart.add_item(@item)}.not_to change{Item.count}
  end
  it "does not add another item to the cart" do
    expect{@cart.add_item(@item)}.not_to change{@cart.items.count}
  end
  it "updates the quantity of the existing item" do
    @cart.add_item(@item)
    expect(@cart.items.first.quantity).to eq 4
  end
end

context "when a valid item is given" do
  it "adds the item to the database" do
    expect{@cart.add_item(@item)}.to change{CartItem.count}.by(1)
  end
  it "adds the item to the cart" do
    expect{@cart.add_item(@item)}.to change{@cart.items.size}.by(1)
  end
end

What i want to know is, why do i have to reload the CollectionProxy when I use this method in the console?

cast01
  • 663
  • 8
  • 23

1 Answers1

2

Association caches the results of the query to achieve better performance. When you call @cart.item for the first time it will call the db to get all the items associated with given cart and it will remember its output (in internal variable called 'target'), hence every time you call it after this initial call it will give you same results without calling db at all. The only way to force it to go again to db is to clear that target variable - this can be done with reload method or passing true to association call @car.items(true).

The reason why you don't need to reload association in you rspec tests is because you are not calling items on any object twice. However if you write test like:

it 'adds an item if it is not in the cart' do
  before_count = @cart.items.size     # size forces the association db call 
  @cart.add build(:item) 
  after_count = @cart.items.size          # items has been fetched from db, so it will return same set of results
  after_count.should_not eq before_count
end

That test will fail, since you are calling items twice on the same object - and hence you will get same results. Note, that using count instead of size will make this test to pass, because count is altering the SQL query itself(which results are not being cached) while size is being delegated to the association target object.

BroiSatse
  • 44,031
  • 8
  • 61
  • 86
  • @BroiStatse thanks for that explanation! I'm still a little confused as to why my tests pass, I thought rspec 'change{}' essentially does what you wrote above? I copied your test over like for like and it passes. The before_count is 0 and after adding an item it is 1, however, if I add 2 (different) items in the middle it is 2, and if i add 2 of the same items in, it is 1, these are the results i would expect. – cast01 Mar 23 '14 at 16:19
  • Carrying on from above.. adding a new item seems fine, and i do not have to reload the collection proxy in the console either, this is happening when I update an existing item. – cast01 Mar 23 '14 at 16:43