在 Magento 2 开发中,我们经常遇到需要在订单创建时同步自定义数据到 sales_order_grid 表的场景。本文记录了一次从失败方案到成功方案的完整演进过程,分享如何使用 Magento 的 Related Object 机制来解决数据同步时序问题。
问题背景
业务需求
我们的项目需要在订单中存储 Event 相关信息(活动日期、类型、名称、城市、县等),并将 county(县)字段同步到 sales_order_grid 表以便在订单列表中展示。
数据结构
quote_event:购物车阶段的 Event 数据sales_order_event:订单阶段的 Event 数据(包含order_id和county字段)sales_order_grid:订单网格表(需要显示county)
最初方案:使用 events.xml Observer(失败)
实现方式
首先创建 Observer 在订单保存成功后保存 event 数据:
<!-- app/code/Florida/Sales/etc/events.xml -->
<event name="sales_model_service_quote_submit_success">
<observer name="saveOrderEventFromQuote"
instance="Florida\Sales\Observer\SaveOrderEventFromQuote"/>
</event>
<?php
namespace Florida\Sales\Observer;
class SaveOrderEventFromQuote implements ObserverInterface
{
public function execute(\Magento\Framework\Event\Observer $observer)
{
$order = $observer->getEvent()->getOrder();
$quote = $observer->getEvent()->getQuote();
$orderId = $order->getId();
// 从 quote_event 读取数据
$quoteEvent = $this->quoteEventRepository->getByQuoteId($quote->getId());
// 保存到 sales_order_event
$orderEvent = $this->orderEventFactory->create();
$orderEvent->setOrderId((int)$orderId);
$orderEvent->setCounty($quoteEvent->getCounty());
// ... 复制其他字段
$this->orderEventRepository->save($orderEvent);
}
}
同时配置 Grid 的 JOIN 逻辑:
<!-- app/code/Florida/Sales/etc/di.xml -->
<virtualType name="Magento\Sales\Model\ResourceModel\Order\Grid">
<arguments>
<argument name="joins" xsi:type="array">
<item name="sales_order_event" xsi:type="array">
<item name="table" xsi:type="string">sales_order_event</item>
<item name="origin_column" xsi:type="string">entity_id</item>
<item name="target_column" xsi:type="string">order_id</item>
</item>
</argument>
<argument name="columns" xsi:type="array">
<item name="county" xsi:type="string">sales_order_event.county</item>
</argument>
</arguments>
</virtualType>
遇到的问题
订单创建后,发现 sales_order_grid.county 字段始终为空。
通过查看日志和数据库,我们发现:
时间轴:
T+0s: sales_order_grid 更新
SQL: INSERT INTO sales_order_grid ... SELECT ... LEFT JOIN sales_order_event ...
结果:county = NULL(sales_order_event 表中还没有数据!)
T+1s: sales_order_event 写入
结果:数据已保存,但 Grid 已经更新过了
问题根因分析
sales_order_grid 的更新时机
通过分析 Magento 源码发现,Grid 的更新在 sales_order_process_relation 事件中触发:
// vendor/magento/module-sales/etc/events.xml
<event name="sales_order_process_relation">
<observer name="sales_grid_order_sync_insert"
instance="SalesOrderIndexGridSyncInsert"/>
</event>
Grid::refresh() 方法会生成如下 SQL:
INSERT INTO sales_order_grid (..., county, ...)
SELECT ..., sales_order_event.county, ...
FROM sales_order
LEFT JOIN sales_order_event ON sales_order.entity_id = sales_order_event.order_id
WHERE sales_order.entity_id = '497'
关键点:这个 LEFT JOIN 发生在订单保存完成后立即执行。
sales_order_event 的写入时机
我们的 Observer 在 sales_model_service_quote_submit_success 事件中触发:
<event name="sales_model_service_quote_submit_success">
<observer name="saveOrderEventFromQuote" .../>
</event>
这个事件在订单保存之后触发。
时序对比
订单保存流程:
1. OrderRepository::save($order)
↓
2. processRelation() 处理关联对象(items, payment, addresses)
↓
3. 触发 sales_order_process_relation 事件
↓
4. GridSyncInsertObserver::execute()
↓
5. Grid::refresh(order_id)
├─ 生成 LEFT JOIN sales_order_event 的 SQL
└─ 执行 INSERT ... SELECT
❌ 此时 sales_order_event 表中没有数据!county = NULL
↓
6. 触发 sales_model_service_quote_submit_success 事件
↓
7. SaveOrderEventFromQuote::execute()
└─ 保存到 sales_order_event(太晚了!)
结论:由于 sales_order_grid 的更新时机比 sales_order_event 的保存时机早,导致 LEFT JOIN 时获取不到 county 数据。
解决方案研究:借鉴 Magento 核心实现
分析 sales_order_address 的处理方式
既然 sales_order_address 能够正确地将数据同步到 Grid,它是如何实现的呢?
通过分析 Magento 核心代码,我们发现 sales_order_address 使用了 Related Object 机制。
关键代码
- Order Model 的 relatedObjects 属性:
// vendor/magento/module-sales/Model/Order.php
protected $_relatedObjects = [];
public function addRelatedObject(\Magento\Framework\Model\AbstractModel $object)
{
$this->_relatedObjects[] = $object;
return $this;
}
public function getRelatedObjects()
{
return $this->_relatedObjects;
}
- Relation Processor 处理 relatedObjects:
// vendor/magento/module-sales/Model/ResourceModel/Order/Relation.php
public function processRelation(\Magento\Framework\Model\AbstractModel $object)
{
// ... 处理 items, payment, status histories ...
// 处理 related objects
if (null !== $object->getRelatedObjects()) {
foreach ($object->getRelatedObjects() as $relatedObject) {
$relatedObject->setOrder($object);
$relatedObject->save();
}
}
// ... 处理 addresses ...
}
- Address Model 的 setOrder/getOrder 方法:
// vendor/magento/module-sales/Model/Order/Address.php
protected $order = null;
public function setOrder(\Magento\Sales\Model\Order $order)
{
$this->order = $order;
return $this;
}
public function getOrder()
{
if (!$this->order) {
$this->order = $this->orderFactory->create()->load($this->getParentId());
}
return $this->order;
}
- Address ResourceModel 自动设置 parent_id:
// vendor/magento/module-sales/Model/ResourceModel/Order/Address.php
protected function _beforeSave(\Magento\Framework\Model\AbstractModel $object)
{
parent::_beforeSave($object);
// 自动设置 parent_id
if (!$object->getParentId() && $object->getOrder()) {
$object->setParentId($object->getOrder()->getId());
}
return $this;
}
关键发现
通过分析 Address 的实现,我们发现:
- Address 在 Order 保存前就被添加为 relatedObject
- processRelation() 在订单保存时自动保存所有 relatedObjects
- parent_id 由 ResourceModel 自动设置
- 整个过程在 Order 保存的同一个事务中完成
- Grid 更新在 processRelation() 之后触发,此时 Address 已经保存
这正是我们需要的!
最终方案:使用 Related Object 机制
核心思路
将 sales_order_event 像处理 sales_order_address 一样,作为 Order 的 Related Object:
- 在订单保存之前创建 event 对象
- 调用
$order->addRelatedObject($event) - Magento ORM 会自动保存 event
order_id自动设置- Grid 更新时 event 已存在,可以正常 JOIN
实现步骤
步骤 1:为 Event Model 添加关联方法
<?php
namespace Florida\Sales\Model\Order;
use Magento\Sales\Model\Order as SalesOrder;
use Magento\Sales\Model\OrderFactory;
class Event extends AbstractModel implements OrderEventInterface
{
protected OrderFactory $orderFactory;
protected ?SalesOrder $order = null;
public function __construct(
\Magento\Framework\Model\Context $context,
\Magento\Framework\Registry $registry,
OrderFactory $orderFactory,
// ... 其他参数
) {
$this->orderFactory = $orderFactory;
parent::__construct($context, $registry, ...);
}
/**
* Set order object
*/
public function setOrder(SalesOrder $order)
{
$this->order = $order;
return $this;
}
/**
* Get order object (lazy load)
*/
public function getOrder(): ?SalesOrder
{
if (!$this->order) {
$this->order = $this->orderFactory->create()->load($this->getOrderId());
}
return $this->order;
}
}
步骤 2:ResourceModel 自动设置 order_id
<?php
namespace Florida\Sales\Model\ResourceModel\Order;
class Event extends AbstractDb
{
protected function _beforeSave(\Magento\Framework\Model\AbstractModel $object)
{
parent::_beforeSave($object);
// 自动设置 order_id(如果未设置)
if (!$object->getOrderId() && $object->getOrder()) {
$object->setOrderId($object->getOrder()->getId());
}
return $this;
}
}
步骤 3:在订单提交前添加 event 为 relatedObject
<!-- app/code/Florida/Sales/etc/events.xml -->
<event name="sales_model_service_quote_submit_before">
<observer name="addOrderEventAsRelatedObject"
instance="Florida\Sales\Observer\AddOrderEventAsRelatedObject"/>
</event>
<?php
namespace Florida\Sales\Observer;
class AddOrderEventAsRelatedObject implements ObserverInterface
{
public function execute(\Magento\Framework\Event\Observer $observer)
{
$order = $observer->getEvent()->getOrder();
$quote = $observer->getEvent()->getQuote();
// 获取 quote event 数据
$quoteEvent = $this->quoteEventRepository->getByQuoteId($quote->getId());
// 创建 order event 对象
$orderEvent = $this->orderEventFactory->create();
$orderEvent->setDate($quoteEvent->getDate());
$orderEvent->setType($quoteEvent->getType());
$orderEvent->setCity($quoteEvent->getCity());
$orderEvent->setCounty($quoteEvent->getCounty());
// ... 其他字段
// ✨ 关键:设置 order 引用并添加为 related object
$orderEvent->setOrder($order);
$order->addRelatedObject($orderEvent);
}
}
步骤 4:保留兜底机制
虽然主方案已经能解决问题,但我们保留原 Observer 作为兜底,并添加幂等性检查:
<?php
namespace Florida\Sales\Observer;
class SaveOrderEventFromQuote implements ObserverInterface
{
public function execute(\Magento\Framework\Event\Observer $observer)
{
$order = $observer->getEvent()->getOrder();
$quote = $observer->getEvent()->getQuote();
$orderId = $order->getId() ?: $order->getEntityId();
$quoteEvent = $this->quoteEventRepository->getByQuoteId($quote->getId());
if ($quoteEvent) {
// 幂等性检查:如果已存在,说明主机制成功
$orderEvent = $this->orderEventRepository->getByOrderId((int)$orderId);
if ($orderEvent && $orderEvent->getId()) {
// 主机制成功,无需兜底
return $this;
}
// 兜底:如果主机制失败,手动保存
$orderEvent = $this->orderEventFactory->create();
$orderEvent->setOrderId((int)$orderId);
// ... 复制数据
$this->orderEventRepository->save($orderEvent);
}
}
}
方案对比
时序对比
旧方案(失败)
1. OrderRepository::save()
↓
2. processRelation()
↓
3. sales_order_process_relation
↓
4. Grid::refresh()
├─ LEFT JOIN sales_order_event
└─ ❌ county = NULL(event 还没保存)
↓
5. sales_model_service_quote_submit_success
└─ SaveOrderEventFromQuote(太晚了)
新方案(成功)
1. sales_model_service_quote_submit_before
↓
2. AddOrderEventAsRelatedObject::execute()
├─ 创建 event 对象
├─ $event->setOrder($order)
└─ $order->addRelatedObject($event)
↓
3. OrderRepository::save()
↓
4. processRelation()
├─ 遍历 relatedObjects
├─ $event->setOrder($order)
└─ $event->save()
└─ ResourceModel::_beforeSave()
└─ 自动设置 order_id
↓
5. sales_order_process_relation
↓
6. Grid::refresh()
├─ LEFT JOIN sales_order_event
└─ ✅ county 有值(event 已保存)
↓
7. sales_model_service_quote_submit_success
└─ SaveOrderEventFromQuote(幂等检查,跳过)
关键差异
| 对比项 | 旧方案 | 新方案 |
|---|---|---|
| Event 写入时机 | Order 保存后 | Order 保存中 |
| order_id 设置 | 手动设置 | 自动设置 |
| Grid 更新结果 | county = NULL | county 有值 |
| 事务一致性 | 分离 | 同一事务 |
| 与 Magento 一致性 | ❌ 不一致 | ✅ 完全一致 |
技术要点
Related Object 机制
Magento 提供了 addRelatedObject() 方法用于管理子对象:
// 添加 related object
$order->addRelatedObject($event);
// 获取 all related objects
$relatedObjects = $order->getRelatedObjects();
在 Magento\Sales\Model\ResourceModel\Order\Relation::processRelation() 中会自动保存所有 relatedObjects。
懒加载机制
public function getOrder(): ?SalesOrder
{
if (!$this->order) {
// 仅在需要时从数据库加载
$this->order = $this->orderFactory->create()->load($this->getOrderId());
}
return $this->order;
}
自动设置关联 ID
在 ResourceModel 的 _beforeSave() 中自动设置:
if (!$object->getOrderId() && $object->getOrder()) {
$object->setOrderId($object->getOrder()->getId());
}
总结
核心改进
通过从失败方案到成功方案的演进,我们学到了:
- 理解 Magento 的执行流程:分析 Grid 更新和数据保存的时序关系
- 借鉴核心实现:研究
sales_order_address的处理方式 - 使用 Related Object 机制:将自定义对象作为 Order 的子对象
- 自动关联管理:利用 ORM 自动设置关联 ID
- 保留兜底机制:通过幂等性检查确保可靠性
方案优势
- ✅ 解决时序问题:Event 在 Grid 更新前保存
- ✅ 符合 Magento 架构:与核心实现一致
- ✅ 自动管理关联:无需手动设置 ID
- ✅ 事务一致性:在同一事务中完成
- ✅ 性能最优:无额外查询
- ✅ 可维护性强:代码清晰易懂
适用场景
- 需要在订单创建时同步自定义数据到 Grid
- 遇到 Grid 更新时序问题
- 需要确保数据完整性
- 希望代码符合 Magento 最佳实践