Closed bigs21 closed 3 years ago
If this is not working as described, the issue should be reported to the Doctrine issue tracker where the implementing code is located. Though actually I do not see why that should not be working as described.
Well, after all, it was, as after a few more tests I found out that I had an issue with where the Entities were placed: instead of having them in 'src/Entity/Main' and 'src/Entity/Customer', I had them in 'src/Entity' and 'src/EntityCustomer', with them being configured in that order.
With that, Doctrine was mapping the customer entities to the first (and default) entity manager.
Anyway, from what I've found out that the second parameter to getRepository
is currently ignored, as there's no such parameter (is this from a previous version of symfony?).
As so, we can say the following:
// Retrieves a repository managed by the "customer" em, the Customer's entity em
$customers = $this->getDoctrine()
->getRepository(Customer::class)
->findAll()
;
// Retrieves a repository managed by the "customer" em
$customers = $this->getDoctrine()
->getRepository(Customer::class, 'customer')
->findAll()
;
So, the entity manager used in the repository is always the mapped one of that Entity, and by mapping the entities to a given em, we can't change a different connection to access some Entity, as it will only use the connection for it's em. That comes from a use case where we were trying to use different connections (different databases) to access the same Entity (the same table structure), but in the same request always use the same connection. For example, depending on some parameter the request would use connection_A or connection_B.
I propose to add a note in the page specifying that it's not possible to use different entity managers to access the same Entity.
@xabbuh what do you propose to do here? If it's true that getRepository()
ignores the second argument ... shouldn't we just remove the second example? Thanks!
Yes, I think we shouldn't have it.
Hello,
A brief introduction to our system: The mission: we want to import data from a MSSQL database into our new application.
We have a vagrantbox, in there running the new application. And a docker for the MSSQL database. Now we have an Import Symfony Command Script on hostsystem that connects to the MSSQL database (Docker) and to the Maria database (Vagrantbox) via SSH 3333:172.0.0.1:3306.
Our Doctrine Config
doctrine:
dbal:
default_connection: default
types:
uuid_binary_ordered_time: Ramsey\Uuid\Doctrine\UuidBinaryOrderedTimeType
connections:
default:
# configure these for your database server
driver: 'pdo_mysql'
[...]
url: '%env(resolve:DATABASE_URL)%' # Port 3306
[...]
command:
driver: 'pdo_mysql'
[...]
url: '%env(resolve:COMMAND_DATABASE_URL)%' # Port 3333
[...]
orm:
auto_generate_proxy_classes: '%kernel.debug%'
[...]
default_entity_manager: default
entity_managers:
default:
connection: default
naming_strategy: doctrine.orm.naming_strategy.underscore
mappings:
App:
is_bundle: false
type: annotation
dir: '%kernel.project_dir%/src/Entity'
prefix: 'App\Entity'
alias: App
command:
connection: command
naming_strategy: doctrine.orm.naming_strategy.underscore
mappings:
App:
is_bundle: false
type: annotation
dir: '%kernel.project_dir%/src/Entity'
prefix: 'App\Entity'
alias: AppCommand
In our Command Script:
$doctrine = $this->container->get('doctrine');
$doctrine->getRepository(Salutation::class, 'command')->findAll()
// But Doctrine use the default EntityManager
We found in this File: vendor/doctrine/persistence/lib/Doctrine/Common/Persistence/AbstractManagerRegistry.php Doctrine\Common\Persistence\AbstractManagerRegistry
The method "getManagerForClass" ignores the defindet connection
getManagerForClass() (Line: 156)
[...]
foreach ($this->managers as $id) {
$manager = $this->getService($id);
if (! $manager->getMetadataFactory()->isTransient($class)) { // <-- first goal
return $manager;
}
}
```php
[...]
// Debug output
$this = {Doctrine\Bundle\DoctrineBundle\Registry} [7]
...
*Doctrine\Common\Persistence\AbstractManagerRegistry*managers = {array} [2]
default = "doctrine.orm.default_entity_manager" // <-- first goal
command = "doctrine.orm.command_entity_manager"
...
The problem is just by Entitys with defined CustomRepositorys
```php
/**
* @ORM\Table(name="salutations")
* @ORM\Entity(repositoryClass="App\Repository\Person\SalutationRepository") // <-- Without CustomRepository it works
*/
class Salutation {}
Without CustomRepository it works because the Method "getRepository" in Class "ContainerRepositoryFactory" found no a $customRepositoryName so the call
return $this->getOrCreateRepository($entityManager, $metadata); (Line 77) // this works finde
but have the method found a CustomRepository the call
$repository = $this->container->get($customRepositoryName);
is there any news regarding the resolution of this issue?
关于解决这个问题的消息有什么消息吗?
Just don't use a custom repository. This will work. But IMHO that's no solution. Its a bug, which should be fixed.
I'm in the same situation. I want to use another entity manager to connect to a read-only replica of the original database using the same entities.
the only way that i found to use both custom repositories and more than one database is to use symfony version 3.4
So far I have found this temporary solution for Symfony4
private $registry;
public function __construct(RegistryInterface $registry)
{
$this->registry = $registry;
//parent::__construct($registry, MyEntity::class);
}
public function findAllByFieldName($customFieldName, $connection): array
{
$em = $this->registry->getEntityManager($connection);
$customFields = $em->getRepository(MyEntity::class)->findOneBy(['Field_name'=>$customFieldName]);
/* ...... */
}
Ugly but works..
this issue just cost me about a day's worth of debugging (I'm just starting out with symfony/doctrine so it took a while). together with a colleague we came to this workaround (we have two types of users, each with its own repositry and entitymanager)
namespace App\Repository;
use App\Entity\CustomUser;
use Symfony\Bridge\Doctrine\RegistryInterface;
use Doctrine\ORM\EntityRepository;
class CustomUserRepository extends AbstractUserRepository
{
public function __construct(RegistryInterface $registry)
{
$manager = $registry->getEntityManager('namedentitymanager');
parent::__construct($registry, CustomUser::class);
EntityRepository::__construct($manager, $manager->getClassMetadata(CustomUser::class));
}
}
the thought being that the parent::construct() puts in the default entity manager (the wrong one). the call to EntityRepository::construct() fixes this.
Today I've run into similar problem. Long story short - I want to copy data from database no2 into database no1. Both are using same code base.
@andrmoel is right - only solution for now is to not use custom repositories.
The fastest way will be to use QueryBuilder: https://www.doctrine-project.org/projects/doctrine-orm/en/2.6/reference/query-builder.html#constructing-a-new-querybuilder-object
In another words - don't user getRepository
method.
Example:
// get EntityManagers you want to use
$emTo = $registry->getManager(); // manager connected to "main" database
$emFrom = $registry->getManager('migrate'); // manager connected to other database
// create QueryBuilder for Entity from 'migrate' database
$emFrom->createQueryBuilder()
->select('a')
->from('App\Entity\Article', 'a')
;
// add some regular methods and conditions next
It looks easy (because it is) and obvious, but it wasn't easy to figure it out. Thank you guys for help!
Hi! any news regarding this issue? I tried with Symfony 4.3.0, same issue... Thanks for your help!
Can someone create a small example application that allows to debug? I would like to find out whether that's an issue in the documentation, in Symfony or in Doctrine, but I don't have such a use case in any of my applications.
Hi! I created a new installation here: https://github.com/r2my/symfomem There are 3 EM: main, customer01 and customer02. In ProductController, you'll see that 'customer01' and 'customer02' in getRepository() method are ignored.
config/packages/doctrine.yaml
doctrine:
dbal:
default_connection: default
connections:
default:
# configure these for your database server
url: '%env(DATABASE_DB1_URL)%'
driver: 'pdo_mysql'
server_version: '5.7'
charset: utf8mb4
offerbdd:
# configure these for your database server
url: '%env(DATABASE_DB2_URL)%'
driver: 'pdo_mysql'
server_version: '5.7'
charset: utf8mb4
orm:
default_entity_manager: default
entity_managers:
default:
connection: default
mappings:
App:
is_bundle: false
type: annotation
dir: '%kernel.project_dir%/src/Entity'
prefix: 'App\Entity\'
alias: App
offerbdd:
connection: offerbdd
mappings:
Offer:
is_bundle: false
type: annotation
dir: '%kernel.project_dir%/src/Entity'
prefix: 'App\Entity\'
alias: Offer
My Repository
namespace App\Repository;
use App\Entity\Offer;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Symfony\Bridge\Doctrine\RegistryInterface;
/**
* @method Offer|null find($id, $lockMode = null, $lockVersion = null)
* @method Offer|null findOneBy(array $criteria, array $orderBy = null)
* @method Offer[] findAll()
* @method Offer[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class OfferRepository extends ServiceEntityRepository
{
private $registry;
public function __construct(RegistryInterface $registry)
{
$this->registry = $registry;
}
/**
* @return mixed
*/
public function findListedAll($connection)
{
$sql = "SELECT o FROM App\Entity\Offer o WHERE o.unlisted = :val OR o.unlisted IS NULL "
. "ORDER BY o.id DESC";
return $this->registry->getEntityManager($connection)
->createQuery($sql)
->setParameter('val', false)
->getResult();
}
}
My Controller:
public function index(Request $request)
{
...
$offerEntityManager = $this->getDoctrine()->getManager('offerbdd');
$offers = $offerEntityManager->getRepository(Offer::class)->findListedAll('offerbdd');
....
}
It's work! Thanks to you tech-no-logical and to others for their contribution. It helped me a lot.
Hey! Same here. Ended up in removing repositoryClass
from mapping, but it feels a very strange!
Edit: another possible workaround
ArticleRepository extends ServiceEntityRepository
{
...
public function setEntityManager(EntityManagerInterface $entityManager): self
{
$this->_em = $entityManager;
return $this;
}
....
}
e.g.
$activeEntityManager = ...;
$articles = $activeEntityManager
->getRepository('App:Article')
->setEntityManager($activeEntityManager)
->findAll()
;
@xabbuh for me it seems to be doctrine issue. Some debugging brought me to the same point as @delorie already showed in his post.
@xabbuh did you already find some time to try the reproducer mentioned in https://github.com/symfony/symfony-docs/issues/9878#issuecomment-498207781 ?
The example application didn't make use of the getRepository()
method of the ManagerRegistry
, but called the getRepository()
method of a concrete entity manager instead. This cannot work as that method doesn't have the manager name argument.
However, if we change the code to actually use the manager registry, we will see no difference in the behaviour. This comes from the fact that ContainerRepositoryFactory
class that is used behind the scenes will only find one service for the repository class. If we further changed the code to let the repository not extend the ServiceEntityRepository
, we will finally see a change in the behaviour.
I'm replying here on the subject of having multiple entity managers managing the same entities and the bug related to it.
However, if we change the code to actually use the manager registry, we will see no difference in the behaviour. This comes from the fact that
ContainerRepositoryFactory
class that is used behind the scenes will only find one service for the repository class. If we further changed the code to let the repository not extend theServiceEntityRepository
, we will finally see a change in the behaviour.
Agreed with the description @xabbuh but as @andrmoel said, we can consider there is still a bug here, because there is a different behavior based on whether the entity is defined with a custom repository and how this one is defined or whether no custom repository is used.
It's subtle because if the entity uses a custom repository extending the EntityRepository
class, the behavior is different than if the repo extends the newer ServiceEntityRepository
class.
Here is a repro project: https://github.com/ipernet/symfony-doctrine-multi-em-test
And the test command:
<?php
namespace App\Command;
use App\Entity\Cat;
use App\Entity\Dog;
use App\Entity\Duck;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Doctrine\Common\Persistence\ManagerRegistry;
class SameEntityDualManagerCommand extends Command
{
// the name of the command (the part after "bin/console")
protected static $defaultName = 'app:test';
/**
* @var ManagerRegistry
*/
private $registry;
public function __construct(ManagerRegistry $registry)
{
$this->registry = $registry;
$this->defautEm = $this->registry->getManager('default');
$this->otherEm = $this->registry->getManager('em2');
parent::__construct();
}
protected function configure()
{
// ...
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$this->output = $output;
$this->testEntityWithNoCustomRepository();
$this->testEntityWithNoCustomRepositoryUsingRegistryRepository();
$this->testEntityWithCustomRepositoryExtendingServiceEntityRepository();
$this->testEntityWithCustomRepositoryExtendingServiceEntityRepositoryUsingRegistryRepository();
$this->testEntityWithCustomRepositoryExtendingEntityRepository();
$this->testEntityWithCustomRepositoryExtendingEntityRepositoryUsingRegistryRepository();
}
/**
*
* @return void
*/
private function testEntityWithNoCustomRepository()
{
$entityDefaultEm = $this->defautEm->getRepository(Dog::class)->findOneBy(['id' => 1]);
$entityEm2 = $this->otherEm->getRepository(Dog::class)->findOneBy(['id' => 1]);
$this->testResult($entityDefaultEm, $entityEm2);
$this->output->writeln(' - '.__FUNCTION__);
}
/**
*
* @return void
*/
public function testEntityWithNoCustomRepositoryUsingRegistryRepository()
{
$entityDefaultEm = $this->registry->getRepository(Dog::class, 'default')->findOneBy(['id' => 1]);
$entityEm2 = $this->registry->getRepository(Dog::class, 'em2')->findOneBy(['id' => 1]);
$this->testResult($entityDefaultEm, $entityEm2);
$this->output->writeln(' - '.__FUNCTION__);
}
/**
* @return void
*/
private function testEntityWithCustomRepositoryExtendingServiceEntityRepository()
{
$entityDefaultEm = $this->defautEm->getRepository(Cat::class)->findOneBy(['id' => 1]);
$entityEm2 = $this->otherEm->getRepository(Cat::class)->findOneBy(['id' => 1]);
$this->testResult($entityDefaultEm, $entityEm2);
$this->output->writeln(' - '.__FUNCTION__);
}
/**
*
* @return void
*/
public function testEntityWithCustomRepositoryExtendingServiceEntityRepositoryUsingRegistryRepository()
{
$entityDefaultEm = $this->registry->getRepository(Cat::class, 'default')->findOneBy(['id' => 1]);
$entityEm2 = $this->registry->getRepository(Cat::class, 'em2')->findOneBy(['id' => 1]);
$this->testResult($entityDefaultEm, $entityEm2);
$this->output->writeln(' - '.__FUNCTION__);
}
/**
*
* @return void
*/
public function testEntityWithCustomRepositoryExtendingEntityRepository()
{
$entityDefaultEm = $this->defautEm->getRepository(Duck::class)->findOneBy(['id' => 1]);
$entityEm2 = $this->otherEm->getRepository(Duck::class)->findOneBy(['id' => 1]);
$this->testResult($entityDefaultEm, $entityEm2);
$this->output->writeln(' - '.__FUNCTION__);
}
/**
*
* @return void
*/
public function testEntityWithCustomRepositoryExtendingEntityRepositoryUsingRegistryRepository()
{
$entityDefaultEm = $this->registry->getRepository(Duck::class, 'default')->findOneBy(['id' => 1]);
$entityEm2 = $this->registry->getRepository(Duck::class, 'em2')->findOneBy(['id' => 1]);
$this->testResult($entityDefaultEm, $entityEm2);
$this->output->writeln(' - '.__FUNCTION__);
}
private function testResult($entityDefaultEm, $entityEm2)
{
if (spl_object_hash($entityDefaultEm) === spl_object_hash($entityEm2)) {
$this->output->write('<error>NOT OK: The two entities should not be the same object</error>');
} else {
$this->output->write('<info>OK: The two entities are different objects</info>');
}
}
}
Sorry for bothering you Andreas (@alcaeus ), but can you please have a look at this issue and the reproducers and may support us here? Thanks 🙏 🍺
No problem. Without reading through the entire issue, there used to be an issue in the manager registry where calling getRepository
did not call getManagerForClass
: https://github.com/doctrine/persistence/issues/45. This was fixed in https://github.com/doctrine/persistence/pull/48 which was released in 1.1.1 back in April. With this fixed, calling $this->getDoctrine()->getRepository(SomeEntity::class)
should always return a repository for the correct manager.
To cut a lot of discussion short: this auto-detection only works when an entity is only managed by a single entity manager. If the same entity is managed by multiple entity managers, getManagerForClass
(which is used internally when calling getRepository
on the manager registry) will return the first entity manager it discovers that manages the class. In such cases, getRepository
on the registry should always be called with a specific entity manager name.
In the future, we may consider throwing an exception in getManagerForClass
if we can't find a single entity manager that manages a class. Since this is a BC break, I don't know when we'll be able to add this, but I've created https://github.com/doctrine/persistence/issues/68 to track this.
One more issue that was highlighted here was that of service repositories. Keep in mind that the entire setup of service repositories has the "90%-use-case" in mind, which in this case would be a single entity manager, or at least having the entity only managed by a single entity manager. Once an entity is managed by multiple entity managers, you'd need to have multiple services for the repository, which the system currently doesn't handle. This is not to say that you can't use service repositories at all in such a scenario, you just need to create more glue code to work around these issues. Throwing an exception if an entity is managed by multiple entity managers would also fix this, as the repository service will no longer be instantiable, forcing the user to realise that they've taken a wrong turn somewhere.
So, to summarise:
getRepository
on the registryHi @alcaeus
Thanks for your time and the very detailed information given here. I understand the limits of the "service repositories" you describe and so can only agree with the conclusion you draw.
II'll try to contribute to the improvement of the How to Work with multiple Entity Managers and Connections documentation to include the juicy bits of information we have here.
Thanks!
Awesome, thanks @ipernet!
So, to summarise:
* If using multiple entity managers, try not to have an entity managed by multiple managers * If you do so, always specify the entity manager in question when calling `getRepository` on the registry * Also, don't use service repositories when managing the same entity with multiple managers. Instead, stick to "classic" repositories or write the glue code yourself.
Hi @alcaeus, as a possible answer to these limitations I have put an idea here: https://github.com/doctrine/DoctrineBundle/issues/1044
If using multiple entity managers, try not to have an entity managed by multiple managers
The project I have is to build an API to manage 3 db on 3 different sites. So I have to use 1 entity on 3 Managers. I'll wait for the doc Thanks :)
After a year still documentation is not enough to cover this issue and still not working in a straight-forward way ... https://github.com/r2my/symfomem this repo is still not working.
Being told : If you do so, always specify the entity manager in question when calling getRepository on the registry is also not working as of 4.3.6
this is not an ignorable problem imho , at least a proper workaround should be added to docs to be prepared for future updates.
Woh, I'm having the same problem with Symfony 4.4. I managed entities that can be present on multiple connections. Doing that does not work at all. I get the error that my table is undefined
$data_niveau = $this->doctrine->getRepository(DataNiveau::class, $connection)->findAll();
Works perfectly fine on SF 3.4. It completely break my application, cannot upgrade to SF 4.4 for now.
This bug is reproduced over and over. Symfony 5.04 still has the same issue.
Same problem on a Symfony 3.4 app. The default connection is always used :(
Hi everyone,
since Symfony 5 the provided workaround from @tech-no-logical (https://github.com/symfony/symfony-docs/issues/9878#issuecomment-472448029) does not longer work.
Has anybody a new solution? I'm struggling at this point.
Thank you very much!
Hi everyone,
since Symfony 5 the provided workaround from @tech-no-logical (#9878 (comment)) does not longer work.
Has anybody a new solution? I'm struggling at this point.
Thank you very much!
@Ph0xEn I managed to use a 2nd EM with @soeren-helbig's solution https://github.com/symfony/symfony-docs/issues/9878#issuecomment-523812504. At least it works :).
Hi @Glioburd,
thank you for the link. It works like a charm :)
Very confused. These issues are a result of using the ServiceEntityRepository in order to support autowire. Extend your repository from EntityRepository, exclude your repositories from autowire and (if needed) go back to manually creating and injecting repository services.
Is the workaround of @soeren-helbig in his comment still working? Could someone give me a more explicit code-example? I can't get his code to run. Or is there already an official solution to use two entity managers for one entity?
The DoctrineBundle no official solution to manage one entity type with multiple managers. It has been that way ever since the ServiceEntityRepository class was introduced and seems unlikely to change. Best approach is to simply not do that.
You can however:
At this point you should be good to go as long as you are fine with always using $em->getRepository(Entity::class) to get your repositories. If you have the urge to inject the repositories as services then you need to manually define them using a factory service definition. It is what we used to do back in the pre-autowire days.
It is also instructive to compare the DefaultRepositoryFactory with the ContainerRepositoryFactory to see why it's difficult to support multiple entity managers with autowire.
One final note: I have sometime seen stuff about how resetting an entity manager does not work properly with the DoctrineBundle code. I don't do that sort of thing and I don't recall exactly what the problem was. Just be careful if you do happen to use reset.
I use this workaround : https://github.com/symfony/symfony-docs/issues/9878#issuecomment-523812504. I find this to be the simplest solution, And you can still easily use the default repository.
@pouetman87 Pretty sure that work around is not doing what you think it is. As long as the ContainerRepositoryFactory is being used then you will never get different repository instances. And swapping around which manager a repository belongs to seems like asking for trouble.
Is there a way to add an entity manager in the doctrine config so that it is specifically NEVER used when injecting a repository that extends ServiceEntityRepository
? In my case I have an entity manager that should always be used when injecting repositories, but for a special action I want to use a different entity manager that manages the same entities on another database.
The only difference between the two entity managers is the database url ... maybe there is an easier way to accomplish this?
What's about using the EntityRepository
and load the parameters with:
my.entity.metadata:
class: App\Entity\Metadata
arguments:
$entityName: App\Entity\MyEntity
App\Repository\MyEntityRepository:
arguments:
[$class: my.entity.metadata]
This should solve the problem? A EntityRepository autowireable
In the manual page How to Work with multiple Entity Managers and Connections for Symfony 4, we can see in the end the following code snippet:
Using the ORM configuration code indicated in the beginning of that document, where the Customer entity is mapped to the 'customer' entity manager, the
'customer'
parameter for thegetRepository()
method is ignored. As so, this last code about repository calls should be updated.Also, I suggest to add the code on how to use multiple entity managers for a single Entity, or to add a note that it's not possible if that's the case.