Symfony4 - 在两个实体管理器之间共享缓存池,这是一个坏主意还是我遇到了错误?

Symfony4 - sharing cache pool between two entity managers, is it a bad idea or do I encounter a bug?

它会很长 post,我遇到了奇怪的行为,我在探查器中看到一个实体管理器据说映射了它不映射的实体。它看起来像这样: 这是 doctrine.yaml:

doctrine:
    dbal:
        default_connection: default
        connections:
            default:
                driver:   "pdo_mysql"
                host:     "127.0.0.1"
                port:     "3306"
                dbname:   "example"
                user:     "root"
                password: ""
                charset:  utf8mb4
                server_version: "mariadb-10.4.10"
            logs:
                driver:   "pdo_mysql"
                host:     "127.0.0.1"
                port:     "3306"
                dbname:   "example_logs"
                user:     "root"
                password: ""
                charset:  utf8mb4
                server_version: "mariadb-10.4.10"
    orm:
        auto_generate_proxy_classes: true
        default_entity_manager: default
        entity_managers:
            default:
                query_cache_driver:
                    type: pool
                    pool: apcu.default.cache.pool
                metadata_cache_driver:
                    type: pool
                    pool: apcu.default.cache.pool
                result_cache_driver:
                    type: pool
                    pool: apcu.default.cache.pool
                connection: default
                naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
                mappings:
                    App:
                        is_bundle: false
                        type: annotation
                        dir: '%kernel.project_dir%/src/Entity/Main'
                        prefix: 'App\Entity\Main'
                        alias: App
            logs:
                query_cache_driver:
                    type: pool
                    pool: apcu.default.cache.pool
                metadata_cache_driver:
                    type: pool
                    pool: apcu.default.cache.pool
                result_cache_driver:
                    type: pool
                    pool: apcu.default.cache.pool
                connection: logs
                naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
                mappings:
                    LogBundle:
                        is_bundle: false
                        type: annotation
                        dir: '%kernel.project_dir%/src/Entity/Logs'
                        prefix: 'App\Entity\Logs'
                        alias: App

这里是 framework.yaml 缓存池配置:

framework:
    secret: '%env(APP_SECRET)%'
    session:
        handler_id: null
        cookie_secure: auto
        cookie_samesite: lax

    php_errors:
        log: true

    cache:
        pools:
            apcu.default.cache.pool:
                adapter: cache.adapter.apcu
            apcu.logs.cache.pool:
                adapter: cache.adapter.apcu

如果我从日志 entity_manager 配置中删除 metadata_cache_driver 配置,或将其更改为使用与默认实体管理器不同的缓存池 (apcu.logs.cache.pool),则探查器会报告正确的映射(示例实体默认情况下 em 和日志 em 是空的)。

仅当实体是馈送形式并且 $form->handleRequest() 处理它时才会出现此问题,创建或修改没有表单的实体不会导致此类问题。这是我的控制器:

<?php

namespace App\Controller;

use App\Entity\Main\Example;
use App\Form\Type\ExampleType;
use Doctrine\ORM\EntityManagerInterface;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class ExampleController extends AbstractController {
    /**
     * @Route("/example1")
     * @Template
     */
    public function example1(EntityManagerInterface $em){
        $example = new Example();
        $example->setValue('example value');

        try {
            $em->persist($example);
            $em->flush();
        } catch(\Exception $e){
            return new Response('An error has occurred. '.$e->getMessage());
        }

        return [];
    }

    /**
     * @Route("/example2")
     * @Template
     */
    public function example2(EntityManagerInterface $em){
        $example = $em->getRepository(Example::class)->find(1);
        if(!$example){
            return new Response('No example found.');
        }

        $example->setValue(mt_rand(0, mt_getrandmax()));
        try {
            $em->flush();
        } catch(\Exception $e){
            return new Response('An error has occurred. '.$e->getMessage());
        }

        return [];
    }

    /**
     * @Route("/example3")
     * @Template
     */
    public function example3(Request $request, EntityManagerInterface $em){
        $example = $em->getRepository(Example::class)->find(1);
        if(!$example){
            return new Response('No example found.');
        }

        $form = $this->createForm(ExampleType::class, $example);
        $form->handleRequest($request);

        if($form->isSubmitted() && $form->isValid()){
            $em->flush();
        }

        return ['form' => $form->createView()];
    }


}

example1 和 example2 路由 不会 导致问题,只有 example3 会并且只有在提交表单时才会出现问题,所以只有当我输入 example3 url,然后单击提交只有当为此请求输入分析器时,我才能看到问题。

我的最小复制示例是创建新的 symfony LTS 项目symfony new example-site --version=lts --full

然后这些是我更改过的文件:

数据库由 symfony console doctrine:database:create --connection=defaultsymfony console doctrine:database:create --connection=logs 创建,表由 symfony console doctrine:migrations:diff --em=defaultsymfony console doctrine:migrations:migrate --em=default

创建

这是我尚未包含在 post 中的其他文件的代码:

<?php
//src/Entity/Main/Example.php
namespace App\Entity\Main;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 */
class Example {
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\Column(type="string")
     */
    private $value;

    public function getId(){
        return $this->id;
    }

    public function getValue(){
        return $this->value;
    }

    public function setValue(string $value){
        $this->value = $value;
    }
}
<?php
//src/Form/Type/ExampleType.php
namespace App\Form\Type;

use App\Entity\Main\Example;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class ExampleType extends AbstractType {
    public function buildForm(FormBuilderInterface $builder, array $options){
        $builder->add('value', TextType::class);
        $builder->add('submit', SubmitType::class);
    }

    public function configureOptions(OptionsResolver $resolver){
        $resolver->setDefaults([
            'data_class' => Example::class,
        ]);
    }
}
<!-- template/s/example/example1.html.twig -->
<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>Example</title>
</head>
<body>
    Example1
</body>
</html>
<!-- template/s/example/example2.html.twig -->
<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>Example</title>
</head>
<body>
    Example2
</body>
</html>
<!-- template/s/example/example3.html.twig -->
<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>Example</title>
</head>
<body>
{{ form(form) }}
</body>
</html>

我想补充的最后一件事是,在其他项目中这个问题更为明显,因为当实体引用其他实体时会报告错误(在 non-owning 端 One-to-Many self-referencing协会): 在这种情况下,项目实体是一个饲料槽形式。 对于那些好奇的人,这里是 Item.php: 但我不知道这有什么关系,因为它不是由日志实体管理器管理的,不应该出现在下面。管理该实体的默认实体经理未报告任何问题。

<?php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\Validator\Constraints as Assert;

/**
 * @ORM\Entity(repositoryClass="App\Repository\ItemRepository")
 * @ORM\Table(indexes={
 *          @ORM\Index(name="item_image", columns={"image"})
 *     })
 */
class Item {

    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\Column(type="string", length=32)
     * @Assert\NotBlank()
     * @Assert\Length(min=3, max=32)
     */
    private $name;

    /**
     * @ORM\Column(type="string")
     */
    private $description = '';

    /**
     * @ORM\Column(type="string", length=25, nullable=true)
     */
    private $image;

    /**
     * @ORM\OneToMany(targetEntity="App\Entity\Item", mappedBy="container")
     */
    private $items;

    /**
     * @ORM\ManyToOne(targetEntity="App\Entity\Item", inversedBy="items")
     * @ORM\JoinColumn(name="container", referencedColumnName="id")
     * @var $container Item
     */
    private $container;

    /**
     * @ORM\OneToMany(targetEntity="App\Entity\TagItem", mappedBy="item")
     * @var $tags TagItem[]
     */
    private $tags;

    /**
     * @Assert\Image(mimeTypes="image/jpeg")
     * @var $imageFile null|UploadedFile
     */
    private $imageFile;

    public function __construct() {
        $this->items = new \Doctrine\Common\Collections\ArrayCollection();
        $this->tags = new \Doctrine\Common\Collections\ArrayCollection();
    }

    public function getId(){
        return $this->id;
    }

    public function getName(){
        return $this->name;
    }

    public function setName(string $name){
        $this->name = $name;
    }

    public function getDescription(){
        return $this->description;
    }

    public function setDescription($description){
        $this->description = $description;
    }

    public function hasImage(){
        return isset($this->image);
    }

    public function getImage(){
        return $this->image;
    }

    public function setImage($image){
        $this->image = $image;
    }

    public function hasImageFile(){
        return isset($this->imageFile);
    }

    public function getImageFile(){
        return $this->imageFile;
    }

    public function setImageFile($imageFile){
        $this->imageFile = $imageFile;
    }

    public function getItems(){
        return $this->items;
    }

    public function hasContainer(){
        return isset($this->container);
    }

    public function getContainer(){
        return $this->container;
    }

    public function setContainer(?Item $container){
        return $this->container = $container;
    }

    public function getTags(){
        return $this->tags;
    }

    public function setTags($tags){
        $this->tags = $tags;
    }
}

PHP 版本为 7.3.12 并由 symfony serve

托管

我在 issue threadfancyweb 回来了 github 谁说:

Status: reviewed

The bug is unrelated to the WebProfilerBundle. The reason is that you use the same cache salt for the 2 entity managers. At some places, we blindly call getClassMetadata() on both EM sequentially (ie: DoctrineLoader, DoctrineExtractor). The first call on the rightful EM populates the cache. The second call on the EM that should not know the class hits the cache and thus considers the class as loaded.

Using the same cache is fine, you just need to use different salts.

当被问到配置中是否有选项可以为实体管理器设置盐时,我从 stof:

得到了答案

The clean way to achieve that is to use 2 separate cache pools, as FrameworkBundle takes the pool name into account for the seed to isolate keys from each pool when they share the same storage.