Symfony 2:多个和动态数据库连接

53

我很新于SF2,我想知道如何将多个数据库的连接管理到一个bundle中。 目前我有这个解决方案 - 它运行良好 - 但我不知道它是否是正确的做法...

在myBundle\Ressource\config\config.yml中:

doctrine:
dbal:
    default_connection:       default
    connections:
        default:
            dbname:           SERVER
            user:             root
            password:         null
            host:             localhost
        client:
            dbname:           CLIENT_134
            user:             root
            password:         null
            host:             localhost
orm:
    default_entity_manager:   default
    entity_managers:
        default:
            connection:       default
            mappings:
                MyBundle: ~
        client:
            connection:       client
            mappings:
                MyBundle: ~

然后,为了切换到其中一个 BD 或另一个 BD,我执行:

$O_ressource=  $this->get('doctrine')->getEntityManager('client');
$O_ressource=  $this->get('doctrine')->getEntityManager('default');

你们觉得这是管理的好方法吗?

我的第二个问题是:

如何设置动态数据库连接? 我的系统中有100个数据库,我不能在config.yml文件中设置它们全部。 所以我希望能够随时更改数据库。

感谢帮助!


“动态数据库连接”是指从您的控制器创建一个DBAL连接吗? - Problematic
3
是的,没错!要能够从一个数据库切换到另一个数据库,而这些数据库可能没有在config.yml文件中声明。 - Fish
getEntityManager 在 Symfony2 的最新版本中已废弃。 - guy_fawkes
3个回答

53
如果您使用ConnectionFactory,那么连接附加的事件订阅者将停止工作,例如stofDoctrineExtensions。
以下是我的方法。我像ConnectionFactory一样拥有空连接和实体管理器。在工作时,我只需通过反射替换连接配置即可。适用于SF 2.0.10 ;)
class YourService extends ContainerAware
{ 

  public function switchDatabase($dbName, $dbUser, $dbPass) 
  {
    $connection = $this->container->get(sprintf('doctrine.dbal.%s_connection', 'dynamic_conn'));
    $connection->close();

    $refConn = new \ReflectionObject($connection);
    $refParams = $refConn->getProperty('_params');
    $refParams->setAccessible('public'); //we have to change it for a moment

    $params = $refParams->getValue($connection);
    $params['dbname'] = $dbName;
    $params['user'] = $dbUser;
    $params['password'] = $dbPass;

    $refParams->setAccessible('private');
    $refParams->setValue($connection, $params);
    $this->container->get('doctrine')->resetEntityManager('dynamic_manager'); // for sure (unless you like broken transactions)
  }
}

更新:

针对Doctrine 2.2 / SF 2.3的更加优雅的解决方案(不需要反射),适用于PHP5.4(我喜欢新的数组初始化器:D)我们可以使用Doctrine中的连接包装器功能,详情请参见http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/portability.html

此示例使用会话服务暂时存储连接详细信息。

首先,我们必须创建一个特殊的连接包装器:

namespace w3des\DoctrineBundle\Connection;

use Doctrine\DBAL\Connection;
use Symfony\Component\HttpFoundation\Session\Session;
use Doctrine\Common\EventManager;
use Doctrine\DBAL\Events;
use Doctrine\DBAL\Event\ConnectionEventArgs;

/*
 * @author Dawid zulus Pakula [zulus@w3des.net]
 */
class ConnectionWrapper extends Connection
{

const SESSION_ACTIVE_DYNAMIC_CONN = 'active_dynamic_conn';

/**
 * @var Session
 */
private $session;

/**
 * @var bool
 */
private $_isConnected = false;

/**
 * @param Session $sess
 */
public function setSession(Session $sess)
{
    $this->session = $sess;
}

public function forceSwitch($dbName, $dbUser, $dbPassword)
{
    if ($this->session->has(self::SESSION_ACTIVE_DYNAMIC_CONN)) {
        $current = $this->session->get(self::SESSION_ACTIVE_DYNAMIC_CONN);
        if ($current[0] === $dbName) {
            return;
        }
    }

    $this->session->set(self::SESSION_ACTIVE_DYNAMIC_CONN, [
        $dbName,
        $dbUser,
        $dbPass
    ]);

    if ($this->isConnected()) {
        $this->close();
    }
}

/**
 * {@inheritDoc}
 */
public function connect()
{
    if (! $this->session->has(self::SESSION_ACTIVE_DYNAMIC_CONN)) {
        throw new \InvalidArgumentException('You have to inject into valid context first');
    }
    if ($this->isConnected()) {
        return true;
    }

    $driverOptions = isset($params['driverOptions']) ? $params['driverOptions'] : array();

    $params = $this->getParams();
    $realParams = $this->session->get(self::SESSION_ACTIVE_DYNAMIC_CONN);
    $params['dbname'] = $realParams[0];
    $params['user'] = $realParams[1];
    $params['password'] = $realParams[2];

    $this->_conn = $this->_driver->connect($params, $params['user'], $params['password'], $driverOptions);

    if ($this->_eventManager->hasListeners(Events::postConnect)) {
        $eventArgs = new ConnectionEventArgs($this);
        $this->_eventManager->dispatchEvent(Events::postConnect, $eventArgs);
    }

    $this->_isConnected = true;

    return true;
}

/**
 * {@inheritDoc}
 */
public function isConnected()
{
    return $this->_isConnected;
}

/**
 * {@inheritDoc}
 */
public function close()
{
    if ($this->isConnected()) {
        parent::close();
        $this->_isConnected = false;
    }
}
}

接下来在你的Doctrine配置中注册它:



connections:
  dynamic:
    driver:   %database_driver%
    host:     %database_host%
    port:     %database_port%
    dbname:   'empty_database'
    charset:  UTF8
    wrapper_class: 'w3des\DoctrineBundle\Connection\ConnectionWrapper'

我们的ConnectionWrapper已经成功注册。现在进行session注入。

首先创建一个特殊的CompilerPass类:

namespace w3des\DoctrineBundle\DependencyInjection\CompilerPass;

use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;

class ConnectionCompilerPass implements CompilerPassInterface
{

/**
 * {@inheritDoc}
 */
public function process(ContainerBuilder $container)
{
    $connection = $container
    ->getDefinition('doctrine.dbal.dynamic_connection')
    ->addMethodCall('setSession', [
        new Reference('session')
    ]);
}
}

我们将新的编译器类记录在*Bundle类中:

public function build(ContainerBuilder $container)
{
    parent::build($container);
    $container->addCompilerPass(new ConnectionCompilerPass());
}

就是这样了!

连接将根据会话属性在需要时创建。

要切换数据库,只需使用:

$this->get('doctrine.dbal.dynamic_connection')->forceSwitch($dbname, $dbuser, $dbpass);

优点

  1. 不再需要反射
  2. 按需创建
  3. 优雅而强大

缺点

  1. 您必须手动清理实体管理器,或创建特殊的Doctrine事件来处理此问题
  2. 需要编写更多的代码

1
dynamic_conn或dynamic_manager的目的是什么?你能详细说明一下它们的目的吗?谢谢。 - Reza S
我有一个应用程序,每个客户端都有自己的数据库,其模式与其他客户端相同。这种方法允许我通过会话权限切换到真实数据库。 - zulus
感谢您的回复。在http://knpuniversity.com/screencast/question-answer-day/symfony2-dynamic-subdomains的帮助下,我能够根据给定的子域选择数据库。 - Rick Slinkman
1
你必须通过CompilerPass来完成这个任务。可以直接参考(http://richardmiller.co.uk/2012/02/15/symfony2-service-container-compiler-passes/),或者将整个容器(就像我对session服务所做的那样)注入到你的包装器中。ID:service_container。 - zulus
看起来你应该交换两行代码——在第一行中params始终未定义$driverOptions = isset($params['driverOptions']) ?$params['driverOptions'] : array(); $params = $this->getParams(); - Andrew Zhilin
显示剩余3条评论

23

您可以查看Symfony\Bundle\DoctrineBundle\ConnectionFactory,使用容器服务doctrine.dbal.connection_factory

$connectionFactory = $this->container->get('doctrine.dbal.connection_factory');
$connection = $connectionFactory->createConnection(array(
    'driver' => 'pdo_mysql',
    'user' => 'root',
    'password' => '',
    'host' => 'localhost',
    'dbname' => 'foo_database',
));

这只是一个快速示例,但它应该让你开始。


谢谢,这很有帮助!我想知道是否有一种方法可以更改在我的配置文件中定义的参数的值?例如:

app/config/config.yml

parameters: my_mailer.class: Acme\HelloBundle\Mailer my_mailer.transport: sendmailservices: my_mailer: class: %my_mailer.class% arguments: [%my_mailer.transport%]我能否更改:%my_mailer.transport% 的值?
- Fish
2
我成功地通过定义一个与虚拟dbal.connection项目相关联的实体来“即时”创建连接 - 在我的config.yml文件中 - 然后我使用doctrine.dbal.connection_factory创建正确的连接,最后将这个新连接设置为$this->container$this->container->set('doctrine.dbal.custom_client_connection', $connection); $myObject = $this->get('doctrine') ->getEntityManager('custom_client') ->getRepository('FooBarBundle:MyObject) ->find($id);我不知道这是否是正确/最佳的方法,但它可以工作。 - Fish
在Symfony 2.1中似乎无法直接访问容器属性,您需要使用辅助方法$this->getContainer()->get('...') - David Barreto
非常棒的短例子,展示了如何创建一个多数据库工厂。向你致敬! - ehime

0

我遇到了同样的问题,需要为每个客户端拥有相同模式的不同数据库。自从Symfony 2.3之后,resetEntityManager方法被弃用后,我注意到代码在不关闭连接和不重置(旧实体)管理器的情况下运行良好。

这是我的当前工作代码:

public function switchDatabase($dbName, $dbUser, $dbPass) {
    $connection = $this->container->get(sprintf('doctrine.dbal.%s_connection', 'dynamic_conn'));

    $refConn = new \ReflectionObject($connection);
    $refParams = $refConn->getProperty('_params');
    $refParams->setAccessible('public'); //we have to change it for a moment

    $params = $refParams->getValue($connection);
    $params['dbname'] = $dbName;
    $params['user'] = $dbUser;
    $params['password'] = $dbPass;

    $refParams->setAccessible('private');
    $refParams->setValue($connection, $params);
}

我正在构建一个仪表板系统,将不同的数据库集成到一起,从而提供一个界面来使用Symfony3.0管理这些数据库。问题在于Doctrine使用parameters.yml文件来声明连接,我没有看到任何人谈论如何在这个角度上实现与该线程中所包含的相同的事情。文档也没有很清楚地说明,因为我已经尝试过它们,但我仍然会遇到错误。有人能提供一个Symfony3.0的解决方案吗?谢谢。 - scruffycoder86

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接