Symfony Serializer与Doctrine ORM的简单REST,不支持反规范化?

3
我有一个问题/请求,关于在标准Symfony 4.2环境中反序列化Json的想法。
考虑以下两个实体“Post”和“Author”。每个“Post”都有一个“Author”(单向ManyToOne Association)。
“Post Entity”
/**
 * @ORM\Entity()
 */
class Post
{

    /**
     * @var int
     *
     * @ORM\Id
     * @ORM\Column(type="integer", options={"unsigned": true})
     * @ORM\GeneratedValue
     */
    private $id;

    /**
     * @var string
     *
     * @ORM\Column(type="text", length=50)
     */
    private $text;

    /**
     * @var Author
     *
     * @ORM\ManyToOne(targetEntity="Author", fetch="EAGER")
     */
    private $author;

    /**
     * @return int
     */
    public function getId(): int
    {
        return $this->id;
    }

    /**
     * @return string
     */
    public function getText(): string
    {
        return $this->text;
    }

    /**
     * @param string $text
     */
    public function setText(string $text): void
    {
        $this->text = $text;
    }

    /**
     * @return Author
     */
    public function getAuthor(): Author
    {
        return $this->author;
    }

    /**
     * @param Author $author
     */
    public function setAuthor(Author $author): void
    {
        $this->author = $author;
    }

}

作者实体

/**
 * @ORM\Entity()
 */
class Author
{

    /**
     * @var int
     *
     * @ORM\Id
     * @ORM\Column(type="integer", options={"unsigned": true})
     * @ORM\GeneratedValue
     */
    private $id;

    /**
     * @var string
     *
     * @ORM\Column(type="text", length=50)
     */
    private $name;

    /**
     * @return int
     */
    public function getId(): int
    {
        return $this->id;
    }

    /**
     * @return string
     */
    public function getName(): string
    {
        return $this->name;
    }

    /**
     * @param string $name
     */
    public function setName(string $name): void
    {
        $this->name = $name;
    }

}

现在让我们创建一个简单的控制器,它允许获取发布帖子:
class SimpleController extends AbstractController
{

    /**
     * @Route("/fapi/post/{id}", methods={"GET", "HEAD"})
     * @param int                 $id
     *
     * @return Response
     */
    public function getEntityAction($id)
    {

        $em = $this->getDoctrine()->getManager();

        $post = $em->getRepository(Post::class)->find($id);

        $encoders = array(new JsonEncoder());
        $normalizers = array(new ObjectNormalizer());
        $serializer = new Serializer($normalizers, $encoders);

        $serializedPost = $serializer->serialize($post, 'json');

        return new Response($serializedPost);
    }

    /**
     * @Route("/fapi/post", methods={"POST"})
     *
     * @param Request $request
     *
     * @return Response
     */
    public function postEntityAction(Request $request)
    {

        $data = $request->getContent();

        $encoders = array(new JsonEncoder());
        $normalizers = array(new ObjectNormalizer());
        $serializer = new Serializer($normalizers, $encoders);

        $post = $serializer->deserialize($data, Post::class, 'json');

        $em = $this->getDoctrine()->getManager();

        $em->persist($post);
        $em->flush();

        return new Response(''); // return newly created Post

    }

}

假设数据库中已经存在一篇帖子(id:1)。我们使用 GET /fapi/post/1 命令来获取它。这是结果:
{"id":1,"text":"Hello World!","author":{"id":1,"name":"Huber"}}

这很好,Symfony的ObjectNormalizer负责规范化Post对象,并将Author对象包含在结果中。
现在让我们尝试使用POST /fapi/post创建一个已存在作者的新文章:
{"id":null,"text":"Hello Universe!","author":{"id":1,"name":"Huber"}}

现在我们收到了Symfony的错误信息:

NotNormalizableValueException:类“App\Entity\Post”的“id”属性的类型必须是“int”(给出了“NULL”)

我很感激这个错误并理解它发生的原因,但Symfony序列化有没有内置的方法可以确保在这种“标准情况”下不考虑自动生成的值(由ORM或构造函数生成的UUID等)。
我知道的解决方案有:
  1. 根本不发送请求中的id(但根据我对REST的理解,应该交换对象的完整表示)
  2. 使用GetSetMethodNormalizer代替ObjectNormalizer(因为没有适用于id的Setter,但有其他几个缺陷)。
  3. 从反序列化上下文中排除id(例如通过“ignored_attributes”)
  4. 创建自定义规范化器以处理此情况
现在让我们尝试从ObjectNormalizer更改为GetSetMethodNormalizer以克服我们的错误,并再次尝试进行POST。
现在我们又得到了另一个错误。
致命的可抛出错误:传递给App\Entity\Post::setAuthor()的第1个参数必须是App\Entity\Author的实例,但传递了一个数组。
根据GetSetMethodNormalizer的文档,在反序列化中设置值时不会进行进一步的规范化。
所以我很感激这个错误并理解发生了什么。
API Platform通过自定义的反规范化系统来处理这些问题。
我理解和赞赏Symfony和Doctrine是两个不同的包,但是它们经常结合在一起(事实上通常非常好地协作):
在上述“标准情况”下,是否有任何内置或已知的方式可以让我实现此链路运行而没有错误,并且不需要自定义实现规范化器或其他内容?
数据库 => Doctrine ORM => Symfony GET对象 => json => POST相同的json => Symfony => Doctrine ORM => 数据库
附加小例子:
GET /fapi/post/1只提供
{"id":1,"text":"Hello World!","author":{"id":1,"name":"Huber"}}

由于ManyToOne关联被急加载,如果设置了默认的LAZY,则变成:
{"id":1,"text":"Hello World!","author":{"id":1,"name":"Huber","__initializer__":null,"__cloner__":null,"__isInitialized__":true}}

因为ObjectNormalizer序列化Doctrine代理对象。
另外,我知道JMSSerializerBundle,也经常使用它,但我完全支持Symfony拥有自己的序列化器组件。

我不明白为什么你发送 "id":null,因为实体不能具有值为 null 的 id(但对象可能会)。不过你的代理反序列化是另一回事。 - Jakumi
谢谢@Jakumi,我明白你的意思。但是我在更广泛地讨论Symfony Serializer支持Doctrine实体方面。 - LBA
我建议在现有对象上使用反序列化,例如https://symfony.com/doc/current/components/serializer.html#deserializing-in-an-existing-object,并为创建和更新实体保留不同的路由(顺便说一句,这就是应该的)。然而,根本问题在于你必须想办法处理子实体。我不明白为什么这应该成为独立的Symfony序列化器的一部分,因为它相当特定于Doctrine。我希望你能理解我的意思,(反)序列化器不会神奇地猜测正确的对象,这不是它的责任。 - Jakumi
如果为某些对象添加一些回调函数以实际创建引用(Doctrine可能具有此相关功能),当仅提供ID时创建实体,而不是创建实体,则会更加复杂。但是,如果混合使用,则会更加复杂。我建议不要同时更新不同种类的多个实体。 - Jakumi
@Jakumi 我明白你的意思,正如你所看到的,我正在谈论实体(不感兴趣例如创建一个带有帖子的新作者等)- 是的,它不应该是标准行为,但是同样的方式Serializer使用元数据来规范化,为什么DoctrineNormalizer处理Doctrine对象和关联也不能甚至不应该呢?你明白我想说什么吗? - LBA
1
是的,我完全明白你的意思。相关且可能有用的链接:https://dev59.com/w1oU5IYBdhLWcg3wK0tX(其中较晚的答案提供了一个自定义规范化器...) - Jakumi
1个回答

1

在我的情况下,添加 nullable=true 解决了这个错误。添加后,没有检测到对数据库映射的任何更改。因此,我认为它是完全安全的。

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接