Динамічні бази даних для activerecord в yii2

Якось давно у мене запитували, як зробити зберігання користувацького контенту в різних базах даних, а недавно це ж питання сплив на форумі знову:

Підкажіть в загальних рисах, як можна реалізувати динамічне перемикання між базами в залежності від підключеного користувача. Тобто якщо залогінився користувач User1, то підключитися до DB1, якщо User2 то до DB2.

Отже, реалізуємо підтримку декількох підключень. При роботі через свій Data Mapper можна передавати ідентифікатор користувача прямо в методи сховища:

і не можна передавати інше підключення $ db в присутні в ActiveRecord методи save. deleteAll і подібні, які аргумент $ db не приймають і повністю покладаються на свій статичний метод:

і працюють усередині тільки з цим єдиним підключенням static :: getDb ().

Розглянемо кілька варіантів перемикання баз даних.

Для демонстрації встановимо yii2-app-basic додаток і запустимо команду:

щоб створити і запустити міграцію:

І додамо для цієї таблиці модель:

щоб збереження вироблялося в потрібну базу даних.

Але в цьому зараз є кілька проблем. Вони посилюється при необхідності робити, наприклад, вкладені операції. При цьому потрібно буде запам'ятати старий $ oldId до операції і повернути його на місце після:

Перша проблема - естетична: потрібно скрізь вписувати купу стороннього коду.

Друга - неатомарность. Замість одного рядка потрібно підтримувати всі три. І при цьому потрібно постійно пам'ятати про правильність їх написання, інакше можна забути або переплутати рядки і змінні.

Третя - порушення інкапсуляції. Тепер метод getActiveId () потрібно робити публічним, щоб всі його могли смикати для запам'ятовування старого стану.

Четверта - наявність глобальних змінних:

У нашого компонента є публічний метод switchId ($ id). дозволяє перемикати внутрішній стан компонента будь-якого зовнішнього коду. Така загальнодоступна «глобальна змінна» може привести до проблеми будь-якої загальнодоступності: в якийсь момент стане не дуже зрозуміло, хто і коли її перемкнув і хто забув її «покласти на місце».

Для уникнення останньої проблеми хороший компонент не повинен всередині себе зберігати перемикається зовні стану, щоб різні модулі сайту, які його використовують, не ламали роботу один одного хаотичними перемиканнями.

Але як бути, якщо все-таки перемикати потрібно? У функціональному підході це реалізується за допомогою незмінних (Immutable) об'єктів. Для цього в методах set () і switchId () замість зміни поля ми просто створюємо новий об'єкт-клон з іншими $ components і $ id:

Поля об'єкта задаються при конструюванні і більше ніколи не змінюються. Замість зміни поля тут через new self () створюється новий об'єкт з новими даними. Тепер якщо різні модулі додатка десь у себе пробують змінювати ідентифікатор, вони отримують свою копію і ніяк не впливають на інші модулі, що використовують свої окремі клони:

Так можна наплодити потрібну кількість незалежних один від одного компонентів. Але навіщо?

У звичайному коді - нема чого. Але якщо спробуєте реалізувати чесну многопоточность в PHP7 з pthreads. де в консольному контролері будете в декількох потоках викликати одночасно switchId () у одного і того ж Yii :: $ app-> locator. то побачите дивовижну плутанину. Замість цього можна або кожному потоку дати свій незалежний клон $ locator. або, що більш коректно, позбутися від зберігання стану c «записуючим» методом switchId (). користуючись тільки «читає» методом get ( 'db', $ userId) для отримання потрібного підключення.

Отже, в функціональному підході це реалізується доступом до всього лише на читання, а з ActiveRecord така багатопоточність не реалізується. Ми не можемо зберігати кілька об'єктів підключення всередині різних витягнутих примірників $ post. щоб два виклики методу save ():

записали об'єкт в різні бази, так як $ post1 і $ post2 при збереженні смикають один і той же статичний static :: getDb ():

а все статичне завжди мається на загальному єдиному екземплярі. Саме при спробах зробити кілька варіацій одного і того ж розумієш, що Сінглтон - зло.

Пожтому для підміни цього глобальної підключення ми і зробили якийсь первісний монструозної варіант:

Другу таку конструкцію, як ми вже говорили, використовувати незручно. Чи можна всі ці дії спростити і виконувати одним викликом? І як уникнути помилок і забудькуватості програміста?

Це все можна спростити обрамленням нашого коду в блок:

Нехай метод doWith () всередині себе перемикає ідентифікатор, виконує нашу функцію і після виконання повертає все назад:

Це зручніше використовувати і менша ймовірність щось переплутати. І при цьому можна видалити метод switchId. а метод getActiveId зробити приватним.

У нас не залишилося глобального змінюваного зовні через switchId ($ id) стану всередині компонента. Ми можемо не переживати, що хтось його випадково змінить значення у вкладеному коді і забуде повернути його назад.

Для многопоточности такий хак не підійде, так як об'єкт всередині все-таки змінюється при виклику doWork () і кожен потік буде заважати сусідам. Але для однопотокового виконання ми здійснили повну емуляцію в рамках Сінглтон статичного методу getDb () в ActiveRecord.

Приступимо тепер до реалізації самого провайдера.

Схожість із стандартним фреймворковскім ServiceLocator дає нам можливість використовувати прямо цей клас як базу для нашого компонента. Отнаследуемся від нього і перевизначити методи get () і clear ():

Усередині методу buildDefinition () ми не можемо звертатися безпосередньо до визначень приватного масиву батьківського класу на кшталт $ this -> _ components [$ id]. тому скористалися викликом методу getComponents (). І тут ми реалізували повний обхід визначень з components через array_walk_recursive. щоб вміти робити заміну навіть у вкладених масивах.

Далі нам потрібно докласти інтерфейс детектора активного користувача:

і його найпростішу реалізацію в рамках Yii2:

Схожі статті