本文目录导读:

我来详细讲解一下在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表单与乐观锁集成方案!
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播