Doctrine2 ORM无法保存对DateTime字段的更改

26

我有一个用户实体:

use Doctrine\ORM\Mapping as ORM;

/**
 * ExampleBundle\Entity\User
 *
 * @ORM\Entity()
 */
class User
{
    // ...

    /**
     * @ORM\Column(type="service_expires_at", type="date", nullable=true)
     */
    private $service_expires_at;

    public function getServiceExpiresAt()
    {
        return $this->service_expires_at;
    }

    public function setServiceExpiresAt(\DateTime $service_expires_at)
    {
        $this->service_expires_at = $service_expires_at;
    }
}
当我按以下方式更新用户的service_expires_at时,更新后的service_expires_at保存回数据库:
$date = $user->getServiceExpiresAt(); 

var_dump($date->format('Y-m-d')); // 2013-03-08

$date->modify('+10 days');

var_dump($date->format('Y-m-d')); // 2013-03-18

$user->setServiceExpiresAt($date);

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

但是,如果我将一个新的DateTime对象传递给service_expires_at,更新后的值会被正确保存:

$date = $user->getServiceExpiresAt(); 

$date->modify('+10 days');

$user->setServiceExpiresAt(new \DateTime($date->format('Y-m-d'));

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

为什么会发生这种情况?

3个回答

91
ExampleBundle\Entity\User#getServiceExpiresAt() 返回的 DateTime 实例是存储在实体本身中的相同对象,这会破坏封装性
Doctrine ORM 中的工作单元对更改集应用严格比较,这基本上意味着,在包含对象的实体属性的情况下,如果对象实例没有更改,则ORM不会检测到更改。
在严格比较中,以下内容为真:
$dateTime1 = new \DateTime('@0');
$dateTime2 = new \DateTime('@0');
$dateTime3 = $dateTime1;

var_dump($dateTime1 !== $dateTime2); // true
var_dump($dateTime1 === $dateTime3); // true

$dateTime1->modify('+1 day');

var_dump($dateTime1 === $dateTime3); // true

这是OOP编程新手经常犯的一个错误,可以通过修复getter和setter,使原始实例永远不会在对象外共享来快速解决,例如以下示例:

public function getServiceExpiresAt()
{
    return clone $this->service_expires_at;
}

public function setServiceExpiresAt(\DateTime $service_expires_at)
{
    $this->service_expires_at = clone $service_expires_at;
}

这也将解决您在Doctrine ORM方面的问题。
此外,请注意,这可以修复逻辑中可能存在的泄漏问题。例如,以下代码存在缺陷且难以调试(当应用当前已损坏的getter/setter时):
$bankTransaction1 = $someService->getTransaction(1);
$bankTransaction2 = $someService->getTransaction(2);

// leak! Now both objects reference the same DateTime instance!
$bankTransaction2->setDateTime($bankTransaction1->getDateTime());

// bug! now both your objects were modified!
$bankTransaction1->getDateTime()->modify('+1 day');

因此,无论在问题中涉及到ORM的部分与否,请不要破坏封装性。


答案非常详细,指出了确切的问题!感谢你的努力。这让我很烦恼! - Jan
你应该考虑到这并不适用于所有领域。如果你有两个实体之间的关系,克隆对象将会在你触摸代理时触发延迟加载。这可能会破坏你的性能。 - Nicolas Reynis
在这些情况下,性能几乎总是次要的:如果您想从这种操作中获得性能,请直接在数据库级别执行复制操作(通过“INSERT ... SELECT ...”)。 - Ocramius
我曾经遇到一个问题,考虑将持久性刷新包装在事务(beginTransaction/commit)中——它确实起作用了。而且,在本地环境(xampp)中没有问题,只有在生产的Linux环境中才会出现。这种行为是否取决于环境设置? - Taz

1
考虑使用DateTimeImmutable类来处理日期/时间属性。请注意,DateTimeImmutable不是DateTime的实例

0

当我尝试插入一个带有过去日期的实体时(我正在尝试将旧数据库迁移到新模式并保留其数据),我遇到了完全相同的问题。

我尝试在设置器和获取器中克隆对象,但无济于事。Doctrine 2保存当前日期。检查模式,该字段是日期时间而不是时间戳, 默认值为null。

这怎么可能呢?

编辑:

请原谅我的疏忽,我的同事添加了一个prePersist事件:

/**
 * @ORM\PrePersist
 */
function onPrePersist() {
    $this->created_at = new \DateTime('now');
}

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