tiny ‘n’ smart
database layer

Odkazy: dibi | API reference

Oznámení

Omlouváme se, provoz fóra byl ukončen

LeanMapper – MultiConnection

před 5 lety

VojtaSim
Člen | 55

Zdravím,
nedávno jsem hledal řešení na otázku jak se připojit k 2 databázím v LeanMappru, z níž přihlašovací údaje k 1 získám podle přihlášeného uživatele.
Zde je mé řešení. Funkční, nedopilované (každé připojení má svůj Debug panel), a hlavně je to velký zásah do LeanMapperu.

Začíná to v configu, kde se nastaví dvě připojení:

defaultConnection:
    class: AdminModule\Model\Connections\DefaultConnection( %database.default%, Default )
    setup:
        - registerFilter('sort', [@sorter, 'sort'], 'p')

backendConnection:
    class: AdminModule\Model\Connections\BackendConnection( @defaultConnection::getBackendDatabaseCredentials(@user::getBackendId()), Backend )
    setup:
        - registerFilter('sort', [@sorter, 'sort'], 'p')
        - registerFilter('priority', [@sorter, 'priority'], 'p')
        - registerFilter('translateFromEntity', [@translator, 'translateFromEntity'], 'ep')
        - registerFilter('translate', [@translator, 'translate'])


- AdminModule\Model\Mapper
- LeanMapper\DefaultEntityFactory

// @user::getBackendId() je metoda ve vlastní implementaci třídy User a je to zkratka pro $user->identity->backend_id

Metoda getBackendDatabaseCredentialsdefaultConnection vrací pole s přihlašovácími údaji, které jsou uložené v tabulce, v databázi dostupné před defaultConnection

Třídy DefaultConnection a BackendConenction jsou obyčejné třídy rozšiřující třídu Connection aby byla zachována podpora filtrů.

Takovýchto připojení může být několik, ale musí mít unikátní jméno + 'Connection' a 1 připojení (výchozí) se musí jmenovat podle názvu nastaveného v mappru. Toto připojení dostane repositář vždy, pokud nějaké připojení neexistuje, nebo se pomocí anotace nad repositářem neuvede jinak.

/**
 * @connection backend  -- backend = připojení registrované jako služba `backendConnection`
 * @table user
 * @entity User
 */

Tak samo se musí uvést i u Entity!

Takhle by jsme dostali chtěné připojení do repositáře, ale problém nastává, když se z entity User odkážu třeba na entitu Group, jejíž tabulka leží v jiné databázi. Tohle se řeší pomocí customFlag u definice @property

/**
 * Class User
 * @package Model\Entity
 *
 * @property-read int $id
 * @property-read int $group_id
 *
 * @property Group $group m:hasOne m:connection(default) -- opět se používá jméno pod kterým je připojení registrované v configu
 */

Pokud se odkazuji na stejnou databázi není třeba m:connection uvádět


Technická stránka

Mapper zde zastává roli toho, co přiděluje připojení. Od toho jsem přidal pár nových metod:

/** @var Connection[] */
protected $connections;

/** @var string */
protected $defaultConnectionName = 'default';

public function __construct(Container $container) // Container je potřeba, aby byl přístup ke všem službám a tedy i připojením
{
    $this->container = $container;
}

public function getConnection($name)
{
    if (!isset($this->connections[$name])) {
        try {
            $connectionName = $name . 'Connection';
            return $this->connections[$name] = $this->container->getService($connectionName);

        } catch (MissingServiceException $e) {
            if ($name === $this->defaultConnectionName) {
                throw new InvalidStateException("Default connection with name '$name' does not exist!");
            }

            return $this->getConnection($this->defaultConnectionName);
        }
    }

    return $this->connections[$name];
}

Dále jsem upravil i Repository:

/** @var string */
protected $connectionName;

public function getConnection()
{
    return $this->connection ?: $this->connection = $this->mapper->getConnection($this->getConnectionName());
}

protected function getConnectionName()
{
    if ($this->connectionName === null) {
        $connectionName = AnnotationsParser::parseSimpleAnnotationValue('connection', $this->getDocComment());
        if ($connectionName !== null) {
            return $this->connectionName = $connectionName;
        }
    }
    return $this->connectionName;
}

Místo ->connection se musí používat ->getConnection(), aby se vyhnulo cirkulárních referencím, když by se Connection předávalo __construct(). Implementace přes getter by byla možná, ale:

Funkční, nedopilované…

Hodně změn jsem musel udělat v Entity, jednak pro podporu m:connection a jednak, abych mohl předat název připojení z Entity až do Result, který je v Entity obalený.

V metodách, které získávají hodnoty z různých relací a kontrolují změny…

getHasManyRowDifferences, addToOrRemoveFrom, getBelongsToManyValue, getBelongsToOneValue, getHasManyValue, getHasOneValue, markAsUpdated

…jsem přidal:

$targetConnection = $property->hasCustomFlag('connection') ? $property->getCustomFlagValue('connection') : $this->connectionName;
// pokud není nastaveno m:connection použije se connection podle této entity

Tato hodnota se pak předává před Row do Result do metod:
getReferencedRow, getReferencingRows, addToReferencing, removeFromReferencing, createReferencingDataDifference

a v těchto metodách se už následně pomocí Mappru vybere dané připojení a použije se pro select.


Known bugs

Pokud se použijí vazební tabulky tak tabulka musí být ve stejné databázi jako cílová tabulka, ovšem cesta zpět je horší, protože by vazební tabulka by taky musela ležet v cílené databázi.

Možné řešení: Samostatná entita pro vazební tabulku | jiný způsob než m:connection?


Jeden příklad

Jak se přihlásit do databáze pomoci uživatele, který ještě není přihlášený? K***a těžko…
Někdo tu může uznat jako prasárnu, ale jak jinak?

// backendConnection
class BackendConnection extends Connection
{
    private $connectionName;

    public function __construct($config, $name)
    {
        if (!is_null($config)) {
            parent::__construct($config, $name);
        }

        $this->connectionName = $name;
    }

    public function reconnect($config)
    {
        $this->__construct($config, $this->connectionName);
    }
}

// Authenticator
/** @var \SystemContainer|\Nette\DI\Container */
private $container;


public function __construct(BackendRepository $backendRepository, Nette\DI\Container $container)
{
    $this->backendRepository = $backendRepository;
    $this->container = $container;
}

public function authenticate(array $credentials)
{
    list($email, $password) = $credentials;

    try {
        $loginAccount = $this->backendRepository->getLoginByEmail($email);


        if ($loginAccount->password == $this->backendRepository->hashPassword($password, $password)) {
            $config =  array('driver' => 'mysql', 'profiler' => 'true',
                'host' => $loginAccount->databaseHost,
                'username' => $loginAccount->databaseUser,
                'password' => $loginAccount->databasePassword,
                'database' => $loginAccount->databaseName,
            );

            $connection = $this->container->backendConnection;
            $connection->reconnect($config);

            $accountRepository = $this->container->accounts;
            $account = $accountRepository->get($loginAccount->id);

            return new Nette\Security\Identity($account->id, array(), $account->getRowData());

            }

        throw new Nette\Security\AuthenticationException("Entered email address or password is incorrect!", self::INVALID_CREDENTIAL);

    } catch (Exceptio $e) {
        throw new Nette\Security\AuthenticationException($e->getMessage(), self::IDENTITY_NOT_FOUND);
    }
}