Doctrine entity manager using wrong connection - php

According to the Symfony docs, multiple entity managers can be configured by listing them in config/packages/doctrine.yaml then selecting a specific entity manager in a controller like this: $this->getDoctrine()->getManager('customer');.
I set up my config/packages/doctrine.yaml:
doctrine:
dbal:
default_connection: default
connections:
default:
# configure these for your database server
url: '%env(DATABASE_URL)%'
driver: 'pdo_mysql'
server_version: '8.0'
charset: utf8mb4
meter:
# configure these for your database server
url: '%env(DATABASE_METER_URL)%'
driver: 'pdo_mysql'
server_version: '8.0'
charset: utf8mb4
orm:
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
meter:
connection: meter
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
and then tried to make a query with
$meterEntityManager = $this->getDoctrine()->getManager('meter');
$meter = $meterEntityManager->getRepository(Entity\Meter::class)->find($meterId);
However, the result of $meterEntityManager->getRepository(Entity\Meter::class)->find($meterId) is a row from the database connection named default, not the one named meter -- it is as if the code had been
$entityManager = $this->getDoctrine()->getManager('default');
$meter = $entityManager->getRepository(Entity\Meter::class)->find($meterId);
When I check the connection with $meterEntityManager->getConnection()->getParams() it correctly lists the params for the meter connection rather than the default connection, so I can get a row from the meter database connection by executing queries directly on the connection object:
$meterEntityManager = $this->getDoctrine()->getManager('meter');
$stmt = $meterEntityManager->getConnection()->prepare('SELECT * FROM meter WHERE id = :id');
$stmt->execute(['id' => $meterId]);
$meter = $stmt->fetch();
But ultimately I want to fetch rows through the methods offered by getRepository(Entity\Meter::class) so returned data is automatically hydrated into doctrine entities. There seems to be an inconsistency between the connection used by $meterEntityManager->getRepository(Entity\Meter::class) and $meterEntityManager->getConnection(). Why is this? Does it have something to do with the entity managers using different connections but sharing the same mappings to entities?

The basic issue here is that ServiceEntityRepository class iterates through a list of entity managers and uses the first one that supports a given entity. Which is fine except when you happen to have entities supported by multiple entity managers. The solution is to go "old school" and extend from the EntityRepository class instead of what is basically a container aware ServiceEntityRepository.
use Doctrine\ORM\EntityRepository;
class MeterRepository extends EntityRepository
{
// Note: Do not override the constructor here
You also have to add the App\Repository directory to the list of excluded directories in services.yaml.
The reason for the ServiceEntityRepository was to allow the Symfony container to autowire repositories and inject them directly into other services. It also allows the developer to inject additional dependencies into the repository if they so chose.

Related

Read replica database connection never used, goes back to default

I'm following the documentation here https://symfony.com/doc/current/reference/configuration/doctrine.html about setting up alternative database connections for the purpose of read replication. For reference I'm using AWS Aurora PostgreSQL with end points for primary and read only.
I've set the this for my doctrine.yaml
doctrine:
dbal:
default_connection: default
connections:
default:
url: '%env(resolve:DATABASE_URL)%'
read:
url: '%env(resolve:READ_URL)%'
orm:
default_entity_manager: default
entity_managers:
default:
connection: default
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
auto_mapping: true
dql:
datetime_functions:
date_trunc: App\DoctrineExtensions\DateTrunc
mappings:
App:
is_bundle: false
type: annotation
dir: '%kernel.project_dir%/src/Entity'
prefix: 'App\Entity'
alias: App
read:
connection: read
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
auto_mapping: false
dql:
datetime_functions:
date_trunc: App\DoctrineExtensions\DateTrunc
mappings:
App:
is_bundle: false
type: annotation
dir: '%kernel.project_dir%/src/Entity'
prefix: 'App\Entity'
alias: App
auto_generate_proxy_classes: true
And calling the read only orm as:
$user = $this->getDoctrine()->getRepository(User::class, 'read')->findOneBy(['email' => $payload['email']]);
as an example.
If I call this it still goes to the default database. If I misname it Symfony throws an exception as expected so I can confirm the config is being picked up and loading correctly.
How is my config or call incorrect that it's still requesting from the default database connection or is there any better way to define read replicas for the data connection? I haven't found any current documentation for it.
According to this:
One entity can be managed by more than one entity manager. This
however results in unexpected behavior when extending from
ServiceEntityRepository in your custom repository. The
ServiceEntityRepository always uses the configured entity manager for
that entity. In order to fix this situation, extend EntityRepository
instead and no longer rely on autowiring:
So you should make sure your repository extends EntityRepository instead of ServiceEntityRepository.
Then you should now always fetch this repository using ManagerRegistry::getRepository().
So basically this would work since $this->getDoctrine() return an instance of ManagerRegistry.
$user = $this->getDoctrine()->getRepository(User::class, 'read')->findOneBy(['email' => $payload['email']]);
The downside is you will not be able to use autowiring for the repository itself.
So this would not work unless you pass that repository manually.
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}

Best practice for database connection via Doctrine in a Symfony Bundle

In my bundle i create a separate database connection and an EntityManager for it. Everything works fine, except those two things don't show up in the development profiler. There is only the default EntityManager and the default connection.
So basically i created 3 new service definitions for an Doctrine\Common\EventManager, an Doctrine\DBAL\Connection and an Doctrine\ORM\EntityManager. I've already tried to add these new service definition to the ContainerBuilder with the same naming convention which is used by the doctrine bridge, but they still won't show up in the profiler. The connection works fine, but i want debug it with and integrate it in the Symfony lifecycle.
The question is:
What is the best practice to create a separate database connection via Doctrine inside of a Symfony Bundle if the Symfony application is only configured to support one connection?
I believe you should take a look at this doc. They described there how to add another EntityManager, which mean another connection. First step is to create configuration.
Especialy take a look at doctrine.yaml configuration:
# config/packages/doctrine.yaml
doctrine:
doctrine:
dbal:
default_connection: default
connections:
default:
# configure these for your database server
url: '%env(DATABASE_URL)%'
driver: 'pdo_mysql'
server_version: '5.7'
charset: utf8mb4
customer:
# configure these for your database server
url: '%env(DATABASE_CUSTOMER_URL)%'
driver: 'pdo_mysql'
server_version: '5.7'
charset: utf8mb4
orm:
default_entity_manager: default
entity_managers:
default:
connection: default
mappings:
Main:
is_bundle: false
type: annotation
dir: '%kernel.project_dir%/src/Entity/Main'
prefix: 'App\Entity\Main'
alias: Main
customer:
connection: customer
mappings:
Customer:
is_bundle: false
type: annotation
dir: '%kernel.project_dir%/src/Entity/Customer'
prefix: 'App\Entity\Customer'
alias: Customer
Above are two entity managers, 'default' and 'customer'. There are also two cennections, one for each manager.
If configuration is valid you will have access to those managers by passing its names to 'getManager' method.
$entityManager = $this->getDoctrine()->getManager('default');
$customerEntityManager = $this->getDoctrine()->getManager('customer');
If you cant edit configuration:
What what about creating custom class (Manager or something) in which you will manually create connection. Take a look at this, it should help you.
getting-a-connection

Symfony 2.8 : Doctrine getManagerForClass() not returning the right Entity Manager

tl;dr How does the getManagerForClass() method find out which entity manager is the right one for a specific class?
I've made a generic controller that should be able to handle basic actions for different entities.
I also have connections to two different databases, so I'm using two entity managers.
In my controller, I'm trying to use Doctrine's getManagerForClass() method to find which manager to use for each class, as explained on this blog and this SO answer.
But the method does not seem to differentiate my two entity managers and simply returns the first one in the configuration.
My controller action starts like this:
public function indexAction($namespace, $entityName)
{
$classFullName = "AppBundle:$namespace\\$entityName";
$em = $this->getDoctrine()->getManagerForClass($classFullName);
This is my Doctrine configuration:
dbal:
default_connection: postgres
connections:
postgres:
driver: pdo_pgsql
host: "%database_host%"
port: "%database_port%"
dbname: "%database_name%"
user: "%database_user%"
password: "%database_password%"
charset: UTF8
oracle:
driver: oci8
host: "%oracle_host%"
port: "%oracle_port%"
dbname: "%oracle_name%"
user: "%oracle_user%"
password: "%oracle_password%"
charset: UTF8
orm:
auto_generate_proxy_classes: true
entity_managers:
postgres:
connection: postgres
mappings:
AppBundle:
type: annotation
dir: Entity\Postgres
oracle:
connection: oracle
mappings:
AppBundle:
type: annotation
dir: Entity\Oracle
And my folder structure is as follows:
AppBundle
|___Controller
| |___EntityController.php
|
|___Entity
|___Postgres
| |___SomePostgresBasedEntity.php
|
|___Oracle
|___SomeOracleBasedEntity.php
Now I don't know exactly how the method works, and how it it supposed to know about the mapping if not through the configuration.
But if I call it this way, for example:
$em = $this->getDoctrine()->getManagerForClass("AppBundle:Oracle\\SomeOracleBasedEntity");
...I get the entity manager for Postgres.
But if I simply switch the entity manager configuration, putting the one for oracle first, the previous call works, but the following doesn't:
$em = $this->getDoctrine()->getManagerForClass("AppBundle:Postgres\\SomePostgresBasedEntity");
Update 1
getManagerForClass() cycles through every manager and for each one, checks if the class is "non-transient":
foreach ($this->managers as $id) {
$manager = $this->getService($id);
if (!$manager->getMetadataFactory()->isTransient($class)) {
return $manager;
}
}
This goes all the way down to AnnotationDriver->isTransient(). Here the doc says the following:
A class is non-transient if it is annotated with an annotation from the AnnotationDriver::entityAnnotationClasses.
#Entity seems to be one of those annotations that makes a class non-transient.
But then, how could any of my entities be transient at all? How could the driver distinguish an entity that belongs to a specific manager based solely on its annotations?
I must have missed something in the higher level classes.
Update 2
The method works when using yml mappings.
I kind of expected this behaviour. The difference comes from the implementations of the isTransient() method in the different drivers. The FileDriver implementation of isTransient returns true if the metadata file exists in the dir: directory of the mapping configuration.
I would have expected the AnnotationDriver to search for annotations only in the entities contained in the specified dir: directory, but it seems to ignore that parameter.
Or should I use another one?
At long last, I solved it.
The solution was using the prefix parameter.
entity_managers:
postgres:
connection: postgres
mappings:
AppBundle:
type: annotation
dir: Entity\Postgres
prefix: AppBundle\Entity\Postgres
alias: Postgres
oracle:
connection: oracle
mappings:
AppBundle:
type: annotation
dir: Entity\Oracle
prefix: AppBundle\Entity\Oracle
alias: Oracle
Explanation
The prefix parameter gets passed to the corresponding Entity Manager service, and is added to the entityNamespaces property, which otherwise defaults to AppBundle/Entity.
The Annotation Driver will then check for annotations in that specific namespace, whereas the File Driver checks for existing mapping files in the directory specified through the dir parameter.
(The alias parameter is not mandatory.)
At least, that's how I understand it.

Multiple connections and entity managers in a symfony 2 / 3 application

My multi-tenant-app uses a master database, that holds information about tenants (like name, etc.) and a app-specific database per tenant.
I configured a master and some_tenant connection and entity manager in the doctrine section inside config.yml.
This gives me access to the master database from a controller (eg. for validating and getting tenant information for some_tenant based on the subdomain some_tenant.my-app.com). And it lets me use a tenant-specific database and entity manager during the application life-cycle.
The doctrine section in my config looks like this:
doctrine:
dbal:
default_connection: 'master'
connections:
master:
driver: pdo_mysql
host: "%database_host%"
port: "%database_port%"
dbname: "%database_name%"
user: "%database_user%"
password: "%database_password%"
charset: UTF8
some_tenant:
driver: pdo_mysql
host: "%database_host_some_tenant%"
port: "%database_port_some_tenant%"
dbname: "%database_name_some_tenant%"
user: "%database_user_some_tenant%"
password: "%database_password_some_tenant%"
charset: UTF8
orm:
auto_generate_proxy_classes: "%kernel.debug%"
entity_managers:
master:
connection: master
mappings:
BEMultiTenancyBundle: ~
some_tenant:
connection: some_tenant
mappings:
AppBundle: ~
Here comes the part, which I am unhappy with and cannot find a solution:
First of all, tenants will be more than 20. And it starts to get messy, altering the doctrine config this way.
Then there is another config file, called tenants.yml which holds more information like enabled/disabled app-features, tenant-specific themes, etc.
The file is loaded, validated using the Config Component, and a container parameter is set, so that tenants configurations are available app-wide.
I would like to store the database credentials also in that file.
I want to create connections and entity managers based on that config. One per each tenant, which can be used during the app life-cycle.
I need to have them available also at the console command bin/console doctrine:schema:update --em=some_tenant for updating each tenant's database schem and bin/console doctrine:schema:update --em=master for updating the master database scheme.
By now I guess, the only way to achieve this is to add configuration parameters to the doctrine section programmatically after the AppBundle is loaded, and before the doctrine registry is constructed with the given managers and connections.
But I cannot even find a point, where I could achive this.
Is there another way to get to point 1 and 2?
Even though there is a similiar question, which I am not sure if it is exactly about the same problem and is about 3 years old, I wanted to post this question with a bit more explanation.
The solution in comments is a straight way easy solution and it will work. The other solution I've ever meet is to dynamically replace tenant database credentails using some external conditions, i.e request HOST.
You can decorate or extend the connection factory with a service in a way, that having the request stack available (or providing the domain with ENV or console argument for other SAPI's) you can have the same entity manager (even default!) being configured on demand.
on a brief look this would look like
use Doctrine\Bundle\DoctrineBundle\ConnectionFactory;
use Doctrine\Common\EventManager;
use Doctrine\DBAL\Configuration;
use Symfony\Component\HttpFoundation\RequestStack;
class DynamicConnectionFactory extends Factory
{
/** #var RequestStack */
private $requestStack;
public function __construct(array $types, RequestStack $stack)
{
parent::__construct($types);
$this->requestStack = $stack;
}
public function createConnection(array $params, Configuration $config = null, EventManager $eventManager = null, array $mappingTypes = array())
{
$host = $this->requestStack->getMasterRequest()->getHost();
$params = $this->replaceParamsForHost(array $params, $host);
return parent::createConnection($params, $config, $eventManager, $mappingTypes);
}
private function replaceParamsForHost(array $params, $host)
{
//do your magic, i.e parse config or call Memcache service or count the stars
return array_replace($params, ['database' => $host]);
}
}
I think that, To manage multi-tenant with symfony 2/3.
We can config auto_mapping: false for ORM of doctrine.
file: config.yml
doctrine:
dbal:
default_connection: master
connections:
master:
driver: pdo_mysql
host: '%master_database_host%'
port: '%master_database_port%'
dbname: '%master_database_name%'
user: '%master_database_user%'
password: '%master_database_password%'
charset: UTF8
tenant:
driver: pdo_mysql
host: '%tenant_database_host%'
port: '%tenant_database_port%'
dbname: '%tenant_database_name%'
user: '%tenant_database_user%'
password: '%tenant_database_password%'
charset: UTF8
orm:
default_entity_manager: master
auto_generate_proxy_classes: "%kernel.debug%"
entity_managers:
master:
connection: master
auto_mapping: false
mappings:
AppBundle:
type: yml
dir: Resources/master/config/doctrine
tenant:
connection: tenant
auto_mapping: false
mappings:
AppBundle:
type: yml
dir: Resources/tenant/config/doctrine
After that, we cannot handle connection of each tenant by override connection info in request_listener like article: http://mohdhallal.github.io/blog/2014/09/12/handling-multiple-entity-managers-in-doctrine-the-smart-way/
I hope that, this practice can help someone working with multi-tenant
Regards,
Vuong Nguyen

Symfony2 / Doctrine2 - Associating entities to certain connection

I have two MySQL connections set in my Symfony config.yml:
doctrine:
dbal:
default_connection: default
connections:
default: __VARIABLES
dealer: __VARIABLES
I know I can retrieve objects from certain connection by:
$this->get('doctrine')->getEntityManager('dealer');
My question is - is there a way to set connection to certain entity, so the doctrine:schema:update syncing is done only in the scope of the set connection?
Yes - you can specify in entity manager section entity manager's responsibility. You can use "dir" option for this purpose. You have to disable auto_mapping options and tell Doctrine which entities are related to which entity manager. For example:
doctrine:
dbal:
(...)
orm:
auto_generate_proxy_classes: %kernel.debug%
default_entity_manager: default
entity_managers:
default:
connection: default
mappings:
CompanySomeBundle:
dir: Entity/BaseEntity/
secondem:
connection: second_connection
mappings:
CompanySomeBundle:
dir: Entity/AdditionalEntity/
In that every entity in BaseEntity dir belongs to "default" em, and these entities from AdditionalEntity dir belong to "secondem" entity manager.

Categories