本文目录导读:

我来详细讲解一下在Symfony项目中使用乐观锁与表单(Form)的结合方案。
乐观锁基础配置
Doctrine ORM实体配置
// src/Entity/Product.php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Entity]
#[ORM\HasLifecycleCallbacks]
class Product
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private ?int $id = null;
#[ORM\Column(type: 'string', length: 255)]
#[Assert\NotBlank]
private ?string $name = null;
#[ORM\Column(type: 'decimal', precision: 10, scale: 2)]
#[Assert\NotBlank]
#[Assert\Positive]
private ?string $price = null;
#[ORM\Column(type: 'integer', version: true)]
private ?int $version = 1;
// Getters and Setters...
public function getId(): ?int
{
return $this->id;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(string $name): self
{
$this->name = $name;
return $this;
}
public function getPrice(): ?string
{
return $this->price;
}
public function setPrice(string $price): self
{
$this->price = $price;
return $this;
}
public function getVersion(): ?int
{
return $this->version;
}
public function setVersion(int $version): self
{
$this->version = $version;
return $this;
}
}
创建Form类型
// src/Form/ProductType.php
namespace App\Form;
use App\Entity\Product;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\Extension\Core\Type\NumberType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class ProductType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('name', TextType::class, [
'label' => '产品名称',
])
->add('price', NumberType::class, [
'label' => '价格',
'scale' => 2,
])
->add('version', HiddenType::class, [
'mapped' => true,
'required' => false,
]);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Product::class,
'csrf_protection' => true,
'version_field' => 'version',
]);
}
}
控制器处理乐观锁
// src/Controller/ProductController.php
namespace App\Controller;
use App\Entity\Product;
use App\Form\ProductType;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
use Doctrine\ORM\OptimisticLockException;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class ProductController extends AbstractController
{
private EntityManagerInterface $entityManager;
public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
}
#[Route('/product/edit/{id}', name: 'product_edit')]
public function edit(Request $request, Product $product): Response
{
$originalVersion = $product->getVersion();
$form = $this->createForm(ProductType::class, $product);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
try {
// 开始事务
$this->entityManager->beginTransaction();
// 检查版本冲突
if ($this->checkVersionConflict($product, $originalVersion)) {
throw new OptimisticLockException(
'该产品已被其他用户修改,请刷新后重试',
$product
);
}
// 增加版本号
$product->setVersion($product->getVersion() + 1);
$this->entityManager->flush();
$this->entityManager->commit();
$this->addFlash('success', '产品更新成功');
return $this->redirectToRoute('product_list');
} catch (OptimisticLockException $e) {
$this->entityManager->rollback();
$this->addFlash('error', $e->getMessage());
// 重新加载最新数据
return $this->redirectToRoute('product_edit', ['id' => $product->getId()]);
} catch (\Exception $e) {
$this->entityManager->rollback();
$this->addFlash('error', '更新失败:' . $e->getMessage());
}
}
return $this->render('product/edit.html.twig', [
'form' => $form->createView(),
'product' => $product,
]);
}
private function checkVersionConflict(Product $product, int $originalVersion): bool
{
// 重新从数据库获取最新版本
$freshProduct = $this->entityManager
->getRepository(Product::class)
->find($product->getId());
return $freshProduct->getVersion() !== $originalVersion;
}
}
使用自定义乐观锁管理器
// src/OptimisticLock/OptimisticLockManager.php
namespace App\OptimisticLock;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\OptimisticLockException;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
class OptimisticLockManager
{
private EntityManagerInterface $entityManager;
private SessionInterface $session;
public function __construct(
EntityManagerInterface $entityManager,
SessionInterface $session
) {
$this->entityManager = $entityManager;
$this->session = $session;
}
public function lock(string $entityClass, $entityId): void
{
$lockKey = $this->getLockKey($entityClass, $entityId);
$this->session->set($lockKey, time());
}
public function unlock(string $entityClass, $entityId): void
{
$lockKey = $this->getLockKey($entityClass, $entityId);
$this->session->remove($lockKey);
}
public function isLocked(string $entityClass, $entityId): bool
{
$lockKey = $this->getLockKey($entityClass, $entityId);
$lockTime = $this->session->get($lockKey);
if ($lockTime === null) {
return false;
}
// 5分钟超时
if (time() - $lockTime > 300) {
$this->unlock($entityClass, $entityId);
return false;
}
return true;
}
private function getLockKey(string $entityClass, $entityId): string
{
return sprintf('optimistic_lock_%s_%s', str_replace('\\', '_', $entityClass), $entityId);
}
}
前端实现乐观锁显示
{# templates/product/edit.html.twig #}
{% extends 'base.html.twig' %}
{% block body %}
<div class="container">
<h1>编辑产品</h1>
{% if app.session.get('optimistic_lock_' ~ product.id) is defined %}
<div class="alert alert-warning">
<i class="fas fa-lock"></i>
该产品当前被锁定,其他用户正在编辑
</div>
{% endif %}
{{ form_start(form, {'attr': {'class': 'product-form', 'novalidate': 'novalidate'}}) }}
{{ form_row(form.name) }}
{{ form_row(form.price) }}
{# 隐藏的版本字段 #}
<div class="d-none">
{{ form_row(form.version) }}
</div>
<button type="submit" class="btn btn-primary">
<i class="fas fa-save"></i> 保存
</button>
<a href="{{ path('product_list') }}" class="btn btn-secondary">
取消
</a>
{{ form_end(form) }}
</div>
{% endblock %}
{% block javascripts %}
{{ parent() }}
<script>
// 添加版本冲突检测
document.querySelector('.product-form').addEventListener('submit', function(e) {
const versionField = document.querySelector('#product_version');
// 如果有多个用户编辑冲突,前端也可以做检查
if (versionField) {
const currentVersion = '{{ product.version }}';
const formVersion = versionField.value;
if (currentVersion !== formVersion) {
if (!confirm('数据已被修改,是否继续保存?')) {
e.preventDefault();
return false;
}
}
}
});
// 自动更新版本号
setInterval(function() {
const versionField = document.querySelector('#product_version');
if (versionField) {
// 这里可以添加Ajax请求来获取最新版本号
fetch('/product/' + {{ product.id }} + '/version')
.then(response => response.json())
.then(data => {
if (data.version !== versionField.value) {
versionField.value = data.version;
// 显示版本变更提示
}
});
}
}, 30000); // 30秒检查一次
</script>
{% endblock %}
处理乐观锁冲突的控制器扩展
// src/Controller/ProductController.php (添加新方法)
#[Route('/product/{id}/version', name: 'product_version', methods: ['GET'])]
public function getVersion(Product $product): JsonResponse
{
return $this->json([
'version' => $product->getVersion(),
'lastModified' => $product->getUpdatedAt()?->format('Y-m-d H:i:s')
]);
}
// 批量更新处理乐观锁
#[Route('/products/batch-update', name: 'products_batch_update', methods: ['POST'])]
public function batchUpdate(Request $request): Response
{
$data = json_decode($request->getContent(), true);
$results = [];
$this->entityManager->beginTransaction();
try {
foreach ($data['products'] as $productData) {
$product = $this->entityManager
->getRepository(Product::class)
->find($productData['id']);
if (!$product) {
throw new \Exception("Product {$productData['id']} not found");
}
// 验证版本
if ($productData['version'] !== $product->getVersion()) {
$results[] = [
'id' => $productData['id'],
'success' => false,
'error' => '版本冲突'
];
continue;
}
// 更新字段
if (isset($productData['name'])) {
$product->setName($productData['name']);
}
if (isset($productData['price'])) {
$product->setPrice($productData['price']);
}
$product->setVersion($product->getVersion() + 1);
$results[] = [
'id' => $productData['id'],
'success' => true,
'newVersion' => $product->getVersion()
];
}
$this->entityManager->flush();
$this->entityManager->commit();
return $this->json([
'success' => true,
'results' => $results
]);
} catch (\Exception $e) {
$this->entityManager->rollback();
return $this->json([
'success' => false,
'error' => $e->getMessage()
], 500);
}
}
测试乐观锁功能
// tests/Functional/OptimisticLockTest.php
namespace App\Tests\Functional;
use App\Entity\Product;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
class OptimisticLockTest extends WebTestCase
{
private EntityManagerInterface $entityManager;
protected function setUp(): void
{
self::bootKernel();
$this->entityManager = self::$kernel->getContainer()
->get('doctrine')
->getManager();
}
public function testOptimisticLockConflict(): void
{
$product = $this->createTestProduct();
// 模拟两个用户同时编辑
$product1 = $this->entityManager
->getRepository(Product::class)
->find($product->getId());
$product2 = $this->entityManager
->getRepository(Product::class)
->find($product->getId());
// 用户1更新
$product1->setName('Updated by User 1');
$product1->setVersion($product1->getVersion() + 1);
$this->entityManager->flush();
// 用户2尝试更新(应该触发乐观锁异常)
$this->expectException(\Doctrine\ORM\OptimisticLockException::class);
$product2->setName('Updated by User 2');
$this->entityManager->flush();
// 清理
$this->cleanupTestProduct($product);
}
private function createTestProduct(): Product
{
$product = new Product();
$product->setName('Test Product');
$product->setPrice('99.99');
$this->entityManager->persist($product);
$this->entityManager->flush();
return $product;
}
private function cleanupTestProduct(Product $product): void
{
$this->entityManager->remove($product);
$this->entityManager->flush();
}
}
最佳实践建议
配置文件优化
# config/packages/doctrine.yaml
doctrine:
orm:
auto_generate_proxy_classes: true
optimistic_locking: true
filters:
optimistic_lock:
class: App\Doctrine\Filter\OptimisticLockFilter
enabled: true
使用事件监听器
// src/EventListener/OptimisticLockListener.php
namespace App\EventListener;
use Doctrine\ORM\Event\OnFlushEventArgs;
use Doctrine\ORM\Events;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
#[AsEventListener(event: Events::onFlush)]
class OptimisticLockListener
{
public function onFlush(OnFlushEventArgs $args): void
{
$em = $args->getObjectManager();
$uow = $em->getUnitOfWork();
foreach ($uow->getScheduledEntityUpdates() as $entity) {
if (method_exists($entity, 'getVersion')) {
// 自定义版本检查逻辑
if ($entity->getVersion() === null) {
$entity->setVersion(1);
}
}
}
}
}
完整工作流程总结
- 读取数据:用户打开编辑页面时,加载产品数据和当前版本号
- 编辑过程:使用隐藏字段保存版本号,前端定时检查版本变化
- 提交验证:后端验证提交的版本号与数据库版本号一致
- 成功更新:版本号+1,确保其他用户无法覆盖此版本
- 冲突处理:版本冲突时回滚事务,显示错误信息并重定向到最新版本
这样就实现了完整的Symfony表单与乐观锁集成方案!