我在下面创建了一个场景,在这个场景中,用户可以构建一个比萨饼并选择他们的配料,然后订购他们的比萨:
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 and related toppings
cheesy_pizza = Pizza.create(name: "cheeeeeeeese")
cheap_cheesy_group = cheesy_pizza.toppings_groups.create!(name: 'basic cheeses')
mozza = cheap_cheesy_group.toppings.create!(name: 'mozzarella')
fancy_cheesy_group = cheesy_pizza.toppings_groups.create(name: 'fancy cheeses')
goat = fancy_cheesy_group.toppings.create!(name: 'goat')
cheddar = fancy_cheesy_group.toppings.create!(name: 'cheddar')
# imagine there is a fancy user interface where the user can choose
# toppings from topping groups, but certain groups
# have min/max limits on the toppings you can choose, etc etc
# for now here's a method that
# just takes in chosen toppings, and ties it to the ordered_pizza:
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 # imagine this line is lots of complicated validations
order
end
end
# user orders a pizza
jims_order = create_order(pizza: cheesy_pizza, chosen_toppings: [mozza, goat])
jims_order.reload
jims_order.ordered_pizzas
# for business reasons, some toppings are no longer available, and are removed.
mozza.destroy
goat.destroy
jims_order.reload
jims_order.ordered_pizzas.first.toppings
历史的完整性现在被破坏了!吉姆订好的比萨饼和所有其他点菜的比萨饼上都缺少这些配料。
几个问题:
发布于 2016-12-03 15:28:01
以这种方式保存定制的比萨,看起来还可以,但也取决于应用程序的其余部分是如何构造的。我认为你在这里最关心的应该是你在第二个问题中提到的历史数据的完整性。
当涉及到数据时,它们所处的环境帮助您了解应该如何对待它们。例如,在经营一家在线订购比萨饼店的企业时,一旦顾客可以买到的配料就不能被“删除”。在这些术语中,它们可能变得不可用、中断或其他什么,但不能删除。如果顾客要求你在他们的比萨饼中添加意大利辣香肠,你就不能回复:“哦,我们删除了辣香肠!”
一旦数据命中生产,它们将始终是您应用程序的一部分,尽管它们的状态如何变化,所以您必须把它们当作总是存在的。
这在某种程度上是一个常见的问题,有几种解决办法。我可以提到我过去用过的两种。
第一种方法是对您的数据使用某种软删除策略。你可以在互联网上找到很多关于这一技术的帖子,你也可以找到很多宝石 for ActiveRecord
,这些都可以直接用在盒子里。实际上,您使用软删除实现的是,当您删除数据时,并不是真正地删除它们,而是将它们标记为已删除,以便可以将它们排除在查询之外。
最常见的方法是向表中添加一个新列,该列表示记录已被删除(例如,deleted_at:datetime
、deleted:boolean
),或者添加一个单独的镜像表,其中将“删除”数据(例如deleted_toppings
)。这两种选择各有优缺点,因此这取决于应用程序是如何设计的。
记住,软删除可能会增加应用程序的复杂性。例如,当您要求Topping#name
成为unique
并且尝试添加一个新的名称相同的名字,并且已经属于一个软删除的顶部时,会发生什么呢?
的单独模型
第二种方法是以完全不同的方式对待OrderedPizzaTopping
s。这里的想法是将OrderedPizzaTopping
与PizzaTopping
分离开来。
看起来可能是这样的:
class OrderedPizza < ActiveRecord::Base
belongs_to :order
has_many :ordered_pizza_toppings
end
class OrderedPizzaTopping < ActiveRecord::Base
belongs_to :ordered_pizza
end
每次创建一个订购的披萨时,您都会将每个Topping
的S数据转储到一个OrderedPizzaTopping
实例中,然后保存它。
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
如果您注意到OrderedPizzaTopping
所做的唯一事情就是存储PizzaTopping
的散列版本。只是一个简单的数据结构,包含名称、描述、价格、图像url等。现在,即使PizzaTopping
在某个时候被硬删除,您也将始终拥有原始数据,这将允许您以任何想要的方式表示Order
。可以选择地存储对原始PizzaTopping
的引用,以防它存在于数据库中。
历史数据完整性不仅仅是关于已删除的数据。如果有人改变了已经存在一年的Topping
的价格,会发生什么呢?所有的订单,包括比萨饼包含这一顶部将受到影响。我认为这里的关键字是审计。审核一个模型,意味着记录整个时间内的数据更改。
发布于 2016-12-03 00:29:29
我要说的是,最好的表达方式是向toppings
中添加一个布尔标志列,该列指示当前是否可以订购顶部。
从SQL数据库中删除一行的“语义”含义是它包含的数据不再存在,并且/或再也不会被应用程序访问。
向面向客户端隐藏某些数据,同时在面向业务端保存这些数据的语义方法是创建一个可以对查询进行作用域的标志列。
请注意,您可以使用域/查找表/enum/多个布尔值来表达更复杂的想法。
可用=> 凤尾鱼,意大利香肠
季节性=> 龙虾
停产=> Soylent酒吧
被禁的=> 鹅肝、大麻油
https://codereview.stackexchange.com/questions/148664
复制相似问题