Iteration D2: Connecting Products to Carts

We’re looking at sessions because we need somewhere to keep our shopping cart. We’ll cover sessions in more depth in Rails Sessions, but for now let’s move on to implement the cart.

Let’s keep things simple. A cart contains a set of products. Based on the Initial guess at application data diagram, combined with a brief chat with our customer, we can now generate the Rails models and populate the migrations to create the corresponding tables:

 depot>​​ ​​bin/rails​​ ​​generate​​ ​​scaffold​​ ​​LineItem​​ ​​product:references​​ ​​cart:belongs_to
  ...
 depot>​​ ​​bin/rails​​ ​​db:migrate
 == CreateLineItems: migrating ==============================================
 -- create_table(:line_items)
  ->​​ ​​0.0013s
 == CreateLineItems: migrated (0.0014s) =====================================

The database now has a place to store the references among line items, carts, and products. If you look at the generated definition of the LineItem class, you can see the definitions of these relationships:

 class​ LineItem < ApplicationRecord
  belongs_to ​:product
  belongs_to ​:cart
 end

The belongs_to method defines an accessor method—in this case, carts and products—but more importantly it tells Rails that rows in line_items are the children of rows in carts and products. No line item can exist unless the corresponding cart and product rows exist. A great rule of thumb for where to put belongs_to declarations is this: if a table has any columns whose values consist of ID values for another table (this concept is known by database designers as foreign keys), the corresponding model should have a belongs_to for each.

What do these various declarations do? Basically, they add navigation capabilities to the model objects. Because Rails added the belongs_to declaration to LineItem, we can now retrieve its Product and display the book’s title:

 li = LineItem.​find​(...)
 puts ​"This line item is for ​​#{​li.​product​.​title​​}​​"

To be able to traverse these relationships in both directions, we need to add some declarations to our model files that specify their inverse relations.

Open the cart.rb file in app/models, and add a call to has_many:

 class​ Cart < ApplicationRecord
» has_many ​:line_items​, ​dependent: :destroy
 end

That has_many :line_items part of the directive is fairly self-explanatory: a cart (potentially) has many associated line items. These are linked to the cart because each line item contains a reference to its cart’s ID. The dependent: :destroy part indicates that the existence of line items is dependent on the existence of the cart. If we destroy a cart, deleting it from the database, we want Rails also to destroy any line items that are associated with that cart.

Now that the Cart is declared to have many line items, we can reference them (as a collection) from a cart object:

 cart = Cart.​find​(...)
 puts ​"This cart has ​​#{​cart.​line_items​.​count​​}​​ line items"

Now, for completeness, we should add a has_many directive to our Product model. After all, if we have lots of carts, each product might have many line items referencing it. This time, we make use of validation code to prevent the removal of products that are referenced by line items:

 class​ Product < ApplicationRecord
» has_many ​:line_items
 
» before_destroy ​:ensure_not_referenced_by_any_line_item
 
 #...
 
 
»private
 
»# ensure that there are no line items referencing this product
»def​ ​ensure_not_referenced_by_any_line_item
»unless​ line_items.​empty?
» errors.​add​(​:base​, ​'Line Items present'​)
»throw​ ​:abort
»end
»end
 end

Here we declare that a product has many line items and define a hook method named ensure_not_referenced_by_any_line_item. A hook method is a method that Rails calls automatically at a given point in an object’s life. In this case, the method will be called before Rails attempts to destroy a row in the database. If the hook method throws :abort, the row isn’t destroyed.

Note that we have direct access to the errors object. This is the same place that the validates method stores error messages. Errors can be associated with individual attributes, but in this case we associate the error with the base object.

Before moving on, add a test to ensure that a product in a cart can’t be deleted:

»test ​"can't delete product in cart"​ ​do
» assert_difference(​'Product.count'​, 0) ​do
» delete product_url(products(​:two​))
»end
»
» assert_redirected_to products_url
»end
 
 test ​"should destroy product"​ ​do
  assert_difference(​'Product.count'​, -1) ​do
  delete product_url(@product)
 end
 
  assert_redirected_to products_url
 end

And change the fixture to make sure that product two is in both carts:

 one:
» product: ​two
  cart: ​one
 
 two:
  product: ​two
  cart: ​two

We’ll have more to say about intermodel relationships starting in Specifying Relationships in Models.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.21.104.109