0

I have a Symfony controller action which adds a specified user to the list of followed users (the list is a ManyToMany). The code is quite simple:

public function followUserAction(Request $request, User $followee, $follow)
{
    /** @var User */
    $user = $this->getUser();

    // same user
    if ($user === $followee) {
        return;
    }

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

    if ($follow && !$user->getFollowedUsers()->contains($followee)) {
        $user->addFollowedUser($followee);
    } elseif (!$follow && $user->getFollowedUsers()->contains($followee)) {
        $user->removeFollowedUser($followee);
    }

    $em->flush();
}

This is the mapping:

/**
 * @var ArrayCollection<int|string, User>
 *
 * @ORM\ManyToMany(targetEntity="User", fetch="EXTRA_LAZY", inversedBy="followingUsers")
 * @ORM\JoinTable(
 *     joinColumns={@ORM\JoinColumn(name="following_user_id", referencedColumnName="id")},
 *     inverseJoinColumns={@ORM\JoinColumn(name="followed_user_id", referencedColumnName="id")}
 * )
 */
private $followedUsers;

/**
 * @var ArrayCollection<int|string, User>
 *
 * @ORM\ManyToMany(targetEntity="User", fetch="EXTRA_LAZY", mappedBy="followedUsers")
 */
private $followingUsers;

The problem is that when a user clicks many times I am receiving a Duplicate entry. I followed the Doctrine's article about concurrency:

https://www.doctrine-project.org/projects/doctrine-orm/en/2.7/reference/transactions-and-concurrency.html

But unfortunately it doesn't cover the following case:

  1. Check if record exists (SELECT)
  2. If it doesn't exist => insert it
  3. If it already exists => do nothing

If two users make the first SELECT at the same, they will both try to insert the new record.

Any way to solve it?

StockBreak
  • 2,857
  • 1
  • 35
  • 61
  • since the docs you reference are refering to mongo db. ... are you using mongo db or a proper dbms? if you're using a proper dbms, maybe the ORM is of better use: https://www.doctrine-project.org/projects/doctrine-orm/en/2.7/reference/transactions-and-concurrency.html#transactions-and-concurrency pay attention to refreshing the entity after the begin of the transaction to have "current" data (in the context of transaction and isolation) – Jakumi Jul 27 '20 at 12:09
  • Sorry, the link is wrong, I am using MySQL but for that specific problem the documentation is the same. I updated the link – StockBreak Jul 27 '20 at 12:43
  • the documentation is not the same. the orm version clearly shows a `beginTransaction` call plus commit/rollback, which doesn't appear in odm. it would actually be the first approach to actually wrap it into a transaction (including a refresh to have the select as part of the transaction). testing this might be hard though. maybe you have to also set the isolation level to be more restrictive: https://www.doctrine-project.org/projects/doctrine-dbal/en/2.10/reference/transactions.html#transactions – Jakumi Jul 27 '20 at 13:04

1 Answers1

-1

You can use UniqueConstraintViolationException as proposed at this answer.

Also I think it's good to block UI or throttle user requests at front.

shvv
  • 369
  • 1
  • 7