Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions docs/en/reference/transactions-and-concurrency.rst
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ a ``LockException`` is thrown, which indicates that the document was already mod
.. note::

Only types implementing the ``\Doctrine\ODM\MongoDB\Types\Versionable`` interface can be used for versioning.
Following ODM types can be used for versioning: ``int``, ``decimal128``, ``date``, and ``date_immutable``.
Following ODM types can be used for versioning: ``int``, ``decimal128``, ``date``, ``date_immutable``, and ``object_id``.

Document Configuration
^^^^^^^^^^^^^^^^^^^^^^
Expand Down Expand Up @@ -190,7 +190,8 @@ Choosing the Field Type
"""""""""""""""""""""""

When using the date-based type in a high-concurrency environment, it is still possible to create multiple documents
with the same version and cause a conflict. This can be avoided by using the ``int`` or ``decimal128`` type.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Honestly think it would be worth explicitly mentioning somewhere the possibility of deriving the timestamp from the object ID. It's quite a good idea and will probably not occur to many users unless it's mentioned somewhere.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIRC, ODM converts ObjectId values to strings, so users won't be able to easily utilize ObjectId::getTimestamp() for that purpose. Perhaps this would warrant a separate tutorial or cookbook entry, as there's likely a bit more code involved to parse the timestamp from the string (could be done with PHP code separate from ext-mongodb).

On a separate note, ObjectIds might also encounter issues in high-concurrency environments (see: SERVER-6054). The 4-byte timestamp and 3-byte counter (see: ObjectId spec) likely provides more uniqueness than a UTCDateTime with millisecond precision, though.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exact, the ObjectIdType convert the ObjectId into a string. You have to convert it back to ObjectId to get the timestamp.

return $value !== null ? (string) $value : null;

ObjectIds might also encounter issues in high-concurrency environments
A max of 16 millions unique ObjectId per second should be more than what a single PHP process can handle.

with the same version and cause a conflict. This can be avoided by using the ``int``, ``decimal128``, or ``object_id`` type.
The ``object_id`` type contains the timestamp of its creation, but also a random value to ensure uniqueness.

Usage
"""""
Expand Down
7 changes: 6 additions & 1 deletion lib/Doctrine/ODM/MongoDB/Types/ObjectIdType.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
/**
* The ObjectId type.
*/
class ObjectIdType extends Type
class ObjectIdType extends Type implements Versionable
{
public function convertToDatabaseValue($value)
{
Expand Down Expand Up @@ -38,4 +38,9 @@ public function closureToPHP(): string
{
return '$return = (string) $value;';
}

public function getNextVersion($current): ObjectId
{
return new ObjectId();
}
}
83 changes: 83 additions & 0 deletions tests/Doctrine/ODM/MongoDB/Tests/Types/VersionableTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<?php

declare(strict_types=1);

namespace Doctrine\ODM\MongoDB\Tests\Types;

use DateTime;
use DateTimeImmutable;
use Doctrine\ODM\MongoDB\Tests\BaseTestCase;
use Doctrine\ODM\MongoDB\Types\Type;
use Doctrine\ODM\MongoDB\Types\Versionable;
use MongoDB\BSON\ObjectId;

class VersionableTest extends BaseTestCase
{
public function testIntNextVersion(): void
{
$type = $this->getType(Type::INT);
self::assertSame(1, $type->getNextVersion(null));
self::assertSame(2, $type->getNextVersion(1));
}

public function testDecimal128NextVersion(): void
{
$type = $this->getType(Type::DECIMAL128);
self::assertSame('1', $type->getNextVersion(null));
self::assertSame('2', $type->getNextVersion('1'));
}

public function testDateTimeNextVersion(): void
{
$type = $this->getType(Type::DATE);
$current = new DateTime();
$next = $type->getNextVersion(null);
self::assertInstanceOf(DateTime::class, $next);
self::assertGreaterThanOrEqual($current, $next);
self::assertLessThanOrEqual(new DateTime(), $next);

$next = $type->getNextVersion(new DateTime('2000-01-01'));
self::assertInstanceOf(DateTime::class, $next);
self::assertGreaterThanOrEqual($current, $next);
self::assertLessThanOrEqual(new DateTime(), $next);
}

public function testDateTimeImmutableNextVersion(): void
{
$type = $this->getType(Type::DATE_IMMUTABLE);
$current = new DateTime();
$next = $type->getNextVersion(null);
self::assertInstanceOf(DateTimeImmutable::class, $next);
self::assertGreaterThanOrEqual($current, $next);
self::assertLessThanOrEqual(new DateTimeImmutable(), $next);

$next = $type->getNextVersion(new DateTimeImmutable('2000-01-01'));
self::assertInstanceOf(DateTimeImmutable::class, $next);
self::assertGreaterThanOrEqual($current, $next);
self::assertLessThanOrEqual(new DateTimeImmutable(), $next);
}

public function testObjectIdNextVersion(): void
{
$type = $this->getType(Type::OBJECTID);
$current = new ObjectId();
$next = $type->getNextVersion(null);
self::assertInstanceOf(ObjectId::class, $next);
self::assertGreaterThan($current, $next);
self::assertLessThan(new ObjectId(), $next);

$next = $type->getNextVersion($current);
self::assertInstanceOf(ObjectId::class, $next);
self::assertGreaterThan($current, $next);
self::assertLessThan(new ObjectId(), $next);
}

private function getType(string $name): Versionable
{
$type = Type::getType($name);

self::assertInstanceOf(Versionable::class, $type);

return $type;
}
}