Search code examples
symfonydoctrinesymfony4

Dynamic EntityManager find () method returns "table not found" when a custom repository is in use


First I will explain why and how the solution works and then the problems I have encountered. If you think there is a better way to do what I do, I'd love to hear it. I would also like to know why doctrine behaves in this way.

It turns out that my aplication needs to connect to a different database according to the client. I have a table, in a fixed database, containing the connection information that is used in some request. I have had success with the following code:

class DynamicEntityManager {

protected $em;
private $request;

private $client_id;

public function __construct(RequestStack $request, EntityManagerInterface $em){
    $this->em = $em;
    $this->request = $request;
}

public function getEntityManager(ClientConn $client = null) {
    $request = $this->request->getCurrentRequest();  
    if($client == NULL){
        $domain = $request->attributes->get('domain');
        if($domain == "" || $domain == NULL){
            throw new \Exception("Error de conexion", 1);
        }
        $client = $this->em->getRepository(ClientConn::class)->findOneBy(array(
            "subdomain" => $domain
        ));
        if($client == NULL){
            throw new \Exception("Error de conexion", 1);                
        }
    }
    $connectionDB = $client->getConnection();
    $dbdriver = 'oci8';
    $conexionSplit = explode(':',$connectionDB);
    $dbhost = $conexionSplit[0];
    $dbport = $conexionSplit[1];
    $dbname = $conexionSplit[2];
    $dbuser = $client->getUsuarioBd();
    $dbpass = $client->getClaveBd();
    $service = false;

    $this->client_id = $client->getId();

    if(strpos($dbname,'SN=') !== false){
        $parts = explode('=',$dbname);
        $dbname = $parts[1];
        $service = true;
    }

    $request->attributes->set('client_id',$client->getId());

    $conn = array(
        'driver'    => $dbdriver,
        'host'      => $dbhost,
        'port'      => $dbport,
        'dbname'    => $dbname,
        'user'      => $dbuser,
        'password'  => $dbpass,
        'service'   => $service,
        'charset'   => 'UTF8',
        'schema'    => null
    );
    return EntityManager::create($conn, $this->em->getConfiguration());
    }
}

As you can see I return EntityManager::create($conn, $this->em->getConfiguration ()) with the new connection. The way I use it is the next:

 /**
 * @Route("/api/client/{id}/conf/{confID}", name="conf.show")
 * @Method({"GET"})
 */
public function show(ClientConn $client, Request $request, DynamicEntityManager $dem ,$confId){
    try {

        $em = $dem->getEntityManager($client);
        $entity = $em->getRepository(Configuration::class)->find($confId);
        return new JsonResponse($entity, 200);
    }
    catch(\Exception $ex) {
        return new JsonResponse([
            "excepcion" => $ex->getMessage()
        ], $ex->getCode());
    }
}

It works as expected or so I believed until I saw that when the entity has a custom repository it is unable to use the dynamic connection and therefore the previous route will return a table not found exception.

  • @ORM\Entity() <-- Works like a charm
  • @ORM\Entity(repositoryClass="App\Repository\ConfigurationRepository")<-- Table not found.

It works in the repository if I create the connection again, although I do not like the solution. So, what do I want? I would like to be able to use the basic methods like find (), findBy () and others without having to rewrite them every time I use a custom repository.

class ConfigurationRepository extends ServiceEntityRepository
{
public function __construct(RegistryInterface $registry, DynamicEntityManager $dem)
{
    parent::__construct($registry, Configuration::class);
    $this->dem= $dem;
}

public function uglyFind($client, $confID)
{        
    $query = $this->dem->getEntityManager($client)->createQueryBuilder('conf')
    ->select("conf")
    ->from(ConfPedidosLentes::class,'conf')
    ->where('conf.id = :value')->setParameter('value', $confID)
    ->getQuery();

    return $query->getOneOrNullResult();
}

I will really appreciate any contribution and thought in this matter.


Solution

  • Instead of:

    class ConfigurationRepository extends ServiceEntityRepository
    {
        public function __construct(RegistryInterface $registry, DynamicEntityManager $dem)
        {
            parent::__construct($registry, Configuration::class);
            $this->dem= $dem;
        }
        ...
    

    try extending EntityRepository (without using a constructor) and use find as you did in your controller:

    use Doctrine\ORM\EntityRepository;
    
    class ConfigurationRepository extends EntityRepository
    {
    
    }
    

    ServiceEntityRepository is an optional EntityRepository base class with a simplified constructor for autowiring, that explicitly sets the entity manager to the EntityRepository base class. Since you have not configured your doctrine managers to handle these connections properly (it's not even possible actually with so many connections), ServiceEntityRepository will pass a wrong EntityManager instance to the EntityRepository subclass, that's why you should not extend ServiceEntityRepository but EntityRepository.