如何在 Rails 中为订单设置 shipping 和 billing address
结论
这里不需要使用 STI。ShippingAddress 和 BillingAddress 并不是两种地址类型,而是同一个 Address 在订单中承担的两种角色。
可以按下面的方式处理:
- 在
orders表中保存billing_address_id和shipping_address_id。 - 在
Order中定义两个指向Address的belongs_to关联。 - 使用
accepts_nested_attributes_for接收嵌套参数。 - 在表单中用
fields_for分别渲染账单地址和收货地址。
生成的字段名如下:
order[billing_address_attributes][zipcode]
order[shipping_address_attributes][zipcode]
Rails 会把这两组参数写入各自关联的地址对象。
模型定义
class Order < ApplicationRecord
belongs_to :billing_address,
class_name: "Address"
belongs_to :shipping_address,
class_name: "Address"
accepts_nested_attributes_for :billing_address
accepts_nested_attributes_for :shipping_address
end
class_name: "Address" 表示这两个关联使用的都是 Address 模型。Rails 会根据关联名称查找对应的外键:
billing_address_id
shipping_address_id
Address 不必拆成两个子类:
class Address < ApplicationRecord
validates :zipcode, presence: true
end
原示例中的关联选项应该使用 class_name,不能写成 class:
belongs_to :billing_address, class_name: "Address"
数据库结构
orders 表需要两个指向 addresses 表的外键:
class AddAddressesToOrders < ActiveRecord::Migration[7.1]
def change
add_reference :orders,
:billing_address,
foreign_key: { to_table: :addresses }
add_reference :orders,
:shipping_address,
foreign_key: { to_table: :addresses }
end
end
迁移版本号要与项目使用的 Rails 版本一致。
核心结构如下:
orders.billing_address_id -> addresses.id
orders.shipping_address_id -> addresses.id
这两个字段可以保存同一个地址 ID,也就是说,账单地址和收货地址可以指向同一条记录。
初始化关联对象
渲染新建订单表单前,需要先构建两个地址对象,否则 fields_for 可能找不到可渲染的对象:
class OrdersController < ApplicationController
def new
@order = Order.new
@order.build_billing_address
@order.build_shipping_address
end
def create
@order = Order.new(order_params)
if @order.save
redirect_to @order
else
render :new, status: :unprocessable_entity
end
end
private
def order_params
params.require(:order).permit(
:email,
billing_address_attributes: address_attributes,
shipping_address_attributes: address_attributes
)
end
def address_attributes
%i[
id
recipient_name
street
city
province
zipcode
country
]
end
end
如果需要通过嵌套表单更新已有地址,通常要允许提交 id。不允许提交时,Rails 可能会把编辑操作当成创建新地址。
使用 form_for 构建表单
仍在使用 form_for 的 Rails 项目可以这样写:
<%= form_for @order do |form| %>
<%= form.email_field :email %>
<fieldset>
<legend>账单地址</legend>
<%= form.fields_for :billing_address do |address_form| %>
<%= address_form.label :recipient_name %>
<%= address_form.text_field :recipient_name %>
<%= address_form.label :street %>
<%= address_form.text_field :street %>
<%= address_form.label :city %>
<%= address_form.text_field :city %>
<%= address_form.label :zipcode %>
<%= address_form.text_field :zipcode %>
<% end %>
</fieldset>
<fieldset>
<legend>收货地址</legend>
<%= form.fields_for :shipping_address do |address_form| %>
<%= address_form.label :recipient_name %>
<%= address_form.text_field :recipient_name %>
<%= address_form.label :street %>
<%= address_form.text_field :street %>
<%= address_form.label :city %>
<%= address_form.text_field :city %>
<%= address_form.label :zipcode %>
<%= address_form.text_field :zipcode %>
<% end %>
</fieldset>
<%= form.submit %>
<% end %>
fields_for 必须通过订单的表单构建器调用:
<%= form.fields_for :billing_address do |address_form| %>
不要单独对地址调用 form_for。单独构建地址表单时,Rails 只知道当前对象是 Address,所以会生成 address[zipcode] 这样的字段名。
使用 form_with
较新的 Rails 项目通常使用 form_with,关联部分的写法没有变化:
<%= form_with model: @order do |form| %>
<h2>账单地址</h2>
<%= form.fields_for :billing_address do |address_form| %>
<%= address_form.text_field :recipient_name %>
<%= address_form.text_field :street %>
<%= address_form.text_field :city %>
<%= address_form.text_field :zipcode %>
<% end %>
<h2>收货地址</h2>
<%= form.fields_for :shipping_address do |address_form| %>
<%= address_form.text_field :recipient_name %>
<%= address_form.text_field :street %>
<%= address_form.text_field :city %>
<%= address_form.text_field :zipcode %>
<% end %>
<%= form.submit %>
<% end %>
处理“收货地址与账单地址相同”
如果用户勾选“与账单地址相同”,不要直接让前端提交两套完全相同的嵌套地址数据。这样做通常会创建两条内容相同、ID 不同的 addresses 记录。
可以在控制器或专门的表单对象中复制属性。例如,先接收一个虚拟字段:
class Order < ApplicationRecord
attr_accessor :shipping_same_as_billing
belongs_to :billing_address, class_name: "Address"
belongs_to :shipping_address, class_name: "Address"
accepts_nested_attributes_for :billing_address
accepts_nested_attributes_for :shipping_address
end
保存前再按照具体的业务规则处理。首先要确定系统需要哪种语义:
- 两个关联指向同一条地址记录;
- 创建两条内容相同、彼此独立的地址记录;
- 把地址内容直接作为快照保存在订单中。
订单系统通常更适合保存地址快照。用户以后修改通讯录中的地址时,历史订单的配送地址不应跟着变化。因此,即使账单地址和收货地址目前相同,也可以为订单保存两份独立快照。
注意事项
STI 用于表示真正存在继承关系,而且行为有所不同的模型类型。账单地址和收货地址通常只是用途不同,字段和行为基本一致,没有必要为此建立 STI 层次。
嵌套表单生成的参数也不是简单的:
billing_address[zipcode]
正确格式是:
order[billing_address_attributes][zipcode]
这是 accepts_nested_attributes_for 要求的参数结构。模型关联、fields_for 和 Strong Parameters 保持一致后,Rails 就能同时创建或更新两组地址。
备注:内容仅供参考。
