HiveBrain v1.2.0
Get Started
← Back to all entries
patternrubyrailsMinor

Build a pizza and choose your toppings

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
yourpizzachoosetoppingsandbuild

Problem

I've created a scenario below, in which a user can build a pizza and choose their toppings, then order their pizza:

```
require 'active_record'
require 'logger'

ActiveRecord::Base.establish_connection adapter: 'sqlite3', database: ':memory:'
ActiveRecord::Base.logger = Logger.new $stdout
ActiveSupport::LogSubscriber.colorize_logging = false

ActiveRecord::Schema.define do
self.verbose = false

create_table :pizzas do |t|
t.string :name
end

create_table :pizzas_toppings_groups do |t|
t.integer :pizza_id
t.integer :toppings_group_id
end

create_table :toppings_groups do |t|
t.string :name
t.integer :user_id
end

create_table :toppings do |t|
t.integer :toppings_group_id
t.string :name
end

create_table :orders do |t|
t.integer :user_id
end

create_table :ordered_pizzas do |t|
t.integer :order_id
t.string :name
end

create_table :ordered_pizza_toppings do |t|
t.integer :ordered_pizza_id
t.integer :topping_id
end
end

class Pizza < ActiveRecord::Base
has_many :pizzas_toppings_groups
has_many :toppings_groups, through: :pizzas_toppings_groups
has_many :toppings, through: :toppings_groups
end

class PizzasToppingsGroup < ActiveRecord::Base
belongs_to :pizza
belongs_to :toppings_group
end

class ToppingsGroup < ActiveRecord::Base
belongs_to :user
has_many :pizzas_toppings_groups
has_many :pizzas, through: :pizzas_toppings_groups
has_many :toppings

validates :name, presence: true
end

class Topping < ActiveRecord::Base
belongs_to :toppings_group

validates :name, presence: true
end

class Order < ActiveRecord::Base
belongs_to :user
has_many :ordered_pizzas
end

class OrderedPizza < ActiveRecord::Base
belongs_to :order

has_many :ordered_pizza_toppings
has_many :toppings, through: :ordered_pizza_toppings
end

class OrderedPizzaTopping < ActiveRecord::Base
belongs_to :ordered_pizza
belongs_to :topping
end

# admin defines pizzas and available toppings groups

Solution

Saving customized pizzas this way, looks OK but it also depends on how the rest of application is structured. I think your biggest concern here should be the historical data integrity you mentioned in your second question.

When it comes to data, the context they live in, helps you figure out how you should treat them. For example, when running a business like a pizza store with online ordering, toppings once available to customers can't just be "deleted". They can become unavailable, discontinued or something in these terms, but not deleted. You can't respond to customers that ask you to add pepperoni to their pizzas: "Oh, we deleted pepperoni!".

Once data hit production they will always be part of your application, despite how their status changes, so you have to treat them as if they will always be around.

This is somewhat a common issue and there are a few ways to solve it. I can mentioned two of them which I've used in the past.

1st Approach: Soft Data Deletion

The first approach, would be to go with some sort of soft delete policy for your data. You can find many posts across the internet about this technique and you can find many gems for ActiveRecord which could be used right out of the box. Practically, what you achieve with soft deletes, is that, when you're deleting data, you're not really deleting them, but you mark them as deleted so you can exclude them from your queries.

The most common ways to do this is either by adding a new column to your table that indicates a record has been deleted (e.g. deleted_at:datetime, deleted:boolean), or by adding a separate, mirror table in which "deleted" data will be moved (e.g. deleted_toppings). Both options have their pros and cons so it really depends on how the application is designed.

Keep in mind that soft delete may add complexity to your app. For example what will happen when you require Topping#name to be unique and you try to add a new topping with an identical name that already belongs to a soft-deleted topping?

2nd Approach: Separate model for customized ordered pizzas

The second approach would be to treat OrderedPizzaToppings in a completely different way. The idea here is to decouple OrderedPizzaTopping from PizzaTopping.

It could look like this:

class OrderedPizza < ActiveRecord::Base
  belongs_to :order

  has_many :ordered_pizza_toppings
end

class OrderedPizzaTopping < ActiveRecord::Base
  belongs_to :ordered_pizza
end


Every time an ordered pizza is created you will dump each Topping's data to a OrderedPizzaTopping instance and you'll save it. For example you could have something like this:

class OrderedPizzaTopping < ActiveRecord::Base
  belongs_to :ordered_pizza

  def self.from(topping = Topping.new)
    create!(data: topping.to_h)
  end
end

# ....

def create_order(pizza:, chosen_toppings:)
  Order.transaction do
    order = Order.create
    ordered_pizza = order.ordered_pizzas.create(name: pizza.name)
    ordered_pizza.toppings << chosen_toppings.map { |t| OrderPizzaTopping.from(t) }
    order
  end
end


If you noticed the only thing that OrderedPizzaTopping does is to store a hash version of PizzaTopping. Just a simple data structure containing name, description, price, image url, etc. Now, even if the PizzaTopping gets hard-deleted at some point, you'll always have the raw data which will allow you to represent the Order in any way you want. Optionally, you could store a reference to the original PizzaTopping in case it exists in your database.

Historical data integrity is not about deleted data only. What will happen if someone changes the price for a Topping that has been around for a year? All orders that include pizzas containing this topping will be affected. I think the keyword here is auditing. Auditing a model, means to log data changes throughout time.

Code Snippets

class OrderedPizza < ActiveRecord::Base
  belongs_to :order

  has_many :ordered_pizza_toppings
end

class OrderedPizzaTopping < ActiveRecord::Base
  belongs_to :ordered_pizza
end
class OrderedPizzaTopping < ActiveRecord::Base
  belongs_to :ordered_pizza

  def self.from(topping = Topping.new)
    create!(data: topping.to_h)
  end
end

# ....

def create_order(pizza:, chosen_toppings:)
  Order.transaction do
    order = Order.create
    ordered_pizza = order.ordered_pizzas.create(name: pizza.name)
    ordered_pizza.toppings << chosen_toppings.map { |t| OrderPizzaTopping.from(t) }
    order
  end
end

Context

StackExchange Code Review Q#148664, answer score: 2

Revisions (0)

No revisions yet.