Bonjour,

Voila je rencontre un petit problème avec mon code.

Ce que je fais

Ma situation :
J'ai une classe Conversation censée représenter une conversation entre plusieurs utilisateurs de mon projet :

class Conversation
{
   /**
   * @var int
   * @ORM\Column(name="id", type="integer")
   * @ORM\Id
    * @ORM\GeneratedValue(strategy="AUTO")
    */
    private $id;

    /**
     * @ORM\ManyToMany(targetEntity="THR\UserBundle\Entity\User")
     */
    private $members;

    /**
     * @ORM\OneToMany(targetEntity="THR\ChatBundle\Entity\Message", mappedBy="conversation", cascade={"persist"})
     */
    private $messages;
    }

Ce que je veux

Ce que je cherche à faire :
Récupérer une conversation en base de données en fonction de ses membres , pouvoir dire par exemple : je veux réucperer la conversation concernant les membres d'id 1, 5 et 12. (tout en sachant qu'il peut y avoir une autre conversation concernant les membres 1, 5, 12, et 24 par exemple).

Ce que j'obtiens

public function findConversation($userids)
{
   $qb = $this->createQueryBuilder('c')->innerJoin('c.members', 'u');
   $qb->where($qb->expr()->in('u.id', $userids));
   return $qb->getQuery()->getResult();
}

Le code ci dessus ne correspond malheuresement pas à ce que je cherchecar il me retourne toutes les conversations de tous les membres dans $userids et non la conversation concernant tous les membres de $userids (et seulement ceux là).

J'espère avoir donné assez de détails car je sèche réellement sur ce problème !
Merci d'avance à tout ceux qui essaieront de m'aider !
Cordialement,
Mika.

17 réponses


skp
Réponse acceptée

Pas sûr de comment va l'interpréter doctrine, mais en SQL ça fonctionne SQL Fiddle :


    public function findConversation($userids)
    {
        /* %SUBQUERY% = 
        SELECT c2_.IDConversation FROM conversation c2_ 
            INNER JOIN conversation_user cu2_ ON c2_.IDConversation = cu2_.IDConversation 
            INNER JOIN user u2_ ON cu2_.IDUser = u2_.IDUser 
            GROUP BY c2_.IDConversation 
            HAVING 2 = COUNT(u2_.IDUser);
        */
        $qb = $this->createQueryBuilder('c2')
            ->select('c2.id')
            ->innerJoin('c2.members', 'u2')
            ->groupBy('c2.id')
            ->having(':count = COUNT(u2.id)');

        /*
        SELECT c1_.IDConversation FROM conversation c1_ 
            INNER JOIN conversation_user cu1_ ON c1_.IDConversation = cu1_.IDConversation 
            INNER JOIN user u1_ ON cu1_.IDUser = u1_.IDUser 
            WHERE c1_.IDConversation IN (%SUBQUERY%) AND u1_.IDUser IN (1, 2)
            GROUP BY c1_.IDConversation 
            HAVING 2 = COUNT(c1_.IDConversation);
        */
        return $this->createQueryBuilder('c')
            ->select('c', 'u')
            ->innerJoin('c.members', 'u')
            ->where('c.id IN (' . $qb->getDql() . ') AND u.id IN (:userids)')
            ->groupBy('c.id')
            ->having(':count = COUNT(c.id)')
            ->setParameter('userids', $userids)
            ->setParameter('count', count($userids));
    }

Bonjour,

En prenant comme exemple les ids [1, 5, 12]. Tu dois dire à ta requête que tu veux récuperer les conversations qui ont 3 participants et que ces 3 ont les ids [1, 5, 12].

Je n'ai pas de BDD pour tester ton cas, mais cette solution devrait fonctionner:

//$userids doit être un Array
public function findConversation($userids)
{
  return $this->createQueryBuilder('c')
    ->innerJoin('c.members', 'u')
    ->groupBy('c.id')
    ->having(':count = COUNT(u.id) AND u.id IN (:userids)')
    ->setParameter('count', count($userids))
    ->setParameter('userids', $userids)
    ->getQuery()
    ->getResult();
}

Si tu ne comprends pas le fonctionnement, je peux développer.

Ikemad
Auteur

Bonjour, merci de ta réponse !
j'ai saisi le raisonnement cependant cette requète génère une erreur au niveau du COUNT(u.di).

An exception occurred while executing 'SELECT c0_.id AS id0 FROM conversation c0 INNER JOIN conversationuser c2 ON c0.id = c2.conversationid INNER JOIN user u1 ON u1.id = c2.userid GROUP BY c0.id HAVING ? = COUNT(u1.id) AND u1.id IN (?, ?)' with params [2, 1, 2]:

SQLSTATE[42S22]: Column not found: 1054 Unknown column 'u1_.id' in 'having clause'

InvalidFieldNameException: An exception occurred while executing 'SELECT c0_.id AS id_0 FROM conversation c0_ INNER JOIN conversation_user c2_ ON c0_.id = c2_.conversation_id INNER JOIN user u1_ ON u1_.id = c2_.user_id GROUP BY c0_.id HAVING ? = COUNT(u1_.id) AND u1_.id IN (?, ?)' with params [2, 1, 2]

Ce qui est logique car doctrine passe par une table intermèdiaire pour modéliser la relation conversation -> User.
J'ai pour l'instant pu contourner le problème comme ceci :

public function findConversation($userids)
    {
        $qb = $this->createQueryBuilder('c')
            ->innerJoin('c.messages', 'm')
            ->addSelect('m')
            ->innerJoin('c.members', 'u')
        ;

        foreach($userids as $id)
        {
            $qb->orWhere($qb->expr()->eq('u.id', $id));
        }

        $qb->distinct();
        $results = $qb->getQuery()->getResult();

        foreach($results as $conversation)
        {
            $ids = $conversation->getMembersIds();

            sort($ids);
            sort($userids);

            if($ids === $userids)
            {
                return $conversation;
            }
        }
        return null;
    }

Mais je ne sais pas quel seras l'impact de cette méthode niveau performance dans le cas ou il y a un nombre important de conversation .
En tous cas merci de ton aide !

Il manque le select je pense:

//$userids doit être un Array
public function findConversation($userids)
{
  return $this->createQueryBuilder('c')
    ->select('c', 'u') //avec cette ligne ça devrait fonctionner
    ->innerJoin('c.members', 'u')
    ->groupBy('c.id')
    ->having(':count = COUNT(u.id) AND u.id IN (:userids)')
    ->setParameter('count', count($userids))
    ->setParameter('userids', $userids)
    ->getQuery()
    ->getResult();
}
Ikemad
Auteur

Le problème de cette méthode (qui ne génère pas d'erreur cette fois ci) et qu'elle ne me retourne pas le résultat attendu , je m'explique :

pour l'exemple ou $usersids vaut [1, 2] : cette requête me retourne les conversation ayant 2 membres (le count fonctionne) et possédant les membres d'id SOIT 1 soit 2. Cette requête me retournera donc par exemple les conversation ayant pour membres : [1, 3] , [2, 4] , [1,2] , [2, 6] , etc...
Et cela se comprends aisément en regardant la requête sql qui est générée par doctrine :

SELECT c0_.id AS id_0, u1_.username AS username_1, (...)  FROM conversation c0_ INNER JOIN conversation_user c2_ ON c0_.id = c2_.conversation_id INNER JOIN user u1_ ON u1_.id = c2_.user_id GROUP BY c0_.id HAVING 2 = COUNT(u1_.id) AND u1_.id IN (1, 2);

Encore une fois merci de ton aide, je commence à desespérer qu'il y ai une solution simple à ce problème !

J'ai une requête en tête, mais faut que je prenne du temps pour te l'écrire. Comme je suis au boulot, je te l'écris après 17h si tu n'as pas trouvé de solution.

Ikemad
Auteur

Super, merci bien, je travaille sur une autre partie du projet en attendant !

Ikemad
Auteur

Au top, Merci beaucoup !!!
Je viens de faire différent tests ça fontionne impec dans toutes les situations !
Merci beaucoup skp , tu me retires une belle épines du pied !

Tu peux t'en sortir avec GROUP_CONCAT

SELECT id_conv, GROUP_CONCAT(id_user ORDER BY id_user ASC SEPARATOR ',') as participants FROM `conversation_user` 
GROUP BY id_conv
HAVING participants='1,5,12'

par contre aucune idée pour faire ça avec doctrine

@Ikemad mais de rien.

La requête de @Huggy fonctionne très bien aussi. Mais sauf erreur, c'est disponible uniquement sous MySQL et c'est surement aussi pour ça que doctrine ne le prend pas en charge de base. En tout cas bonne alternative.

Je ne connais pas doctrine mais en général les ORM proposent toujours une alternative "raw query"

Ikemad
Auteur

Bonsoir,
Merci Huggy de ta réponse, j'ai tenté ta solution en faisant une requête native (dnc en sql) :

$rsm = new ResultSetMapping();
$em = $this->getEntityManager();
$query = $em->createNativeQuery("SELECT conversation_id, GROUP_CONCAT(user_id ORDER BY user_id ASC SEPARATOR ',') as participants FROM `conversation_user` GROUP BY conversation_id HAVING participants='1,2' ", $rsm);

Cependant cette requête ne me retourne aucun résultat et je ne sais pas trop comment la manipuler, ne connaissans pas GROUP_CONCAT .

La requête de skp fonctionne très bien ceci dis, à l'exception d'une seul chose (et après j'arrête de vous embeter) : Lorsque j'ai tenté d'ajouter la selection des messages(correspondant à la conversation) à la requête celle ci soit ne fonctionne plus soit me retourne une conversation différente de celle demandée ! J'avoue être réellement perplexe à ce sujet et ne pas comprendre du tout ce qu'il se passe !
Pour exemple lorque j'ai tenté de rajouter les messages dans la requête avec comme paramètre les membres d'id 1 et 2 la requête m'a retourné la conversation consernant les user 2 et 3 avec seulement les messages de l'utilisateur d'id 2.
A savoir que sans le rajout des messages dans la requête, symfony declanche lui meme une requete pour les récupérer quand je les appelle dans ma vue.

Pour récuperer les résultats sous forme d'objet tu as : 16. Native SQL

Sinon tu peux récuperer depuis l'EntityManager l'object Connection

@Ikemad tu peux essayer :

$sql = "SELECT conversation_id, GROUP_CONCAT(user_id ORDER BY user_id ASC SEPARATOR ',') as participants FROM conversation_user GROUP BY conversation_id HAVING participants= :participants";
$stmt = $this->getEntityManager()->getConnection()->prepare($sql);
$stmt->execute(["participants" => impload(",", $userids)]);
var_dump($stmt->fetchAll());
Ikemad
Auteur

Nickel, je commence à avoir un truc qui me plait vraiment ! est ce que quelque chose comme ça est envisageable :

public function findOneConv($userids)
    {
        sort($userids);
        $sql = "SELECT conversation_id, GROUP_CONCAT(user_id ORDER BY user_id ASC SEPARATOR ',') as participants 
                    FROM conversation_user 
                    GROUP BY conversation_id 
                    HAVING participants= :participants";

        $stmt = $this->getEntityManager()->getConnection()->prepare($sql);
        $stmt->execute(["participants" => implode(",", $userids)]);

        return $this->createQueryBuilder('c')
            ->where('c.id = :id')
            ->innerJoin('c.messages', 'm')
            ->addSelect('m')
            ->innerJoin('c.members', 'u')
            ->addSelect('u')
            ->innerJoin('u.avatar', 'a')
            ->addSelect('a')
            ->setParameter('id', $stmt->fetchColumn())
            ->getQuery()->getOneOrNullResult();
 }

J'ai bien évidement tester ce code avant, il fonctionne parfaitement et ne génère aucune requête supplémentaire lors de l'appelle dans la vue mais j'aimerais savoir si cela vous parait optimisé (car il y a au final deux requête) ?!
Après ça je vous fais une médaille !

Tes conversations sont liées à tes utilisateurs, mais aussi avec des messages c'est juste ? Est-ce que tes messages sont liés à tes utilisateurs ? Parce que si tu me dis oui, je te dirais que ce n'est pas très sain ^^.

Ikemad
Auteur

Héhé oui et non...
Mes messages sont lié à UN utilisateur : son auteur. Pour récapituler (3 classes : User, Conversations et Messages) :

Classe Conversation :
-> Relation Many To Many unidirectionnelle avec User : $members
-> Relation OneToMany avec Message : $messages (donc bidirectionnelle -> mappedBy="conversation")

Classe Message :
-> Relation ManyToOne avec Conversation : $conversation (inversedBy="messages")
-> Relation ManyToOne avec User : $author

Classe User :
-> Rien du tout, il en sait rien qu'il est lié à tout ça !

En effet dans ma conception de la chose il était logique qu'un message n'est pas d'attribut destinaire (n'ayant qu'un auteur mais un nombre indéfini de destinataire) mais fasse partie d'une conversation rassemblant ceux-ci ! Je me suis lancé dans cette conception qui fonctione bien et est pratique par plein d'aspects sans réaliser qu'il serait difficile de récupérer une conversation en fonction de ses membres.
Si cette façon de faire te choques et que tu as des suggestions je suis tout a fait ouvert à les entendres !!!

Après réflexion, oui tu as raison. Je n'avais pas pensé à tout les aspects. Par exemple, une conversation peut avoir 3 participants, mais contenir les messages d'un ancien participant. Alors que dans mon cas, pour exlure un participant il faut supprimer ses messages. :)