Работа в Drupal с несколькими базами данных

Главные вкладки

Аватар пользователя direqtor direqtor 17 сентября 2008 в 14:24

Drupal может работать с несколькими БД
Если вам необходимо на друпал-сайте использовать данные из другой базы (может быть даже с другого хоста), то это достаточно просто сделать. Предоставленные для этого возможности не затрагивают кода ядра и могут быть использованы, как в виде вставок PHP-кода в материалы сайта, так и в собственных модулях и темах.

Находим в папке sites/default файл settings.php.
Редактируем его. Вместо строчки

$db_url = 'mysql://username:password@localhost/databasename';

ставим

$db_url = array(
  'default'=>'mysql://username:pass@localhost/databasename',
  'db1'=>'mysql://username1:pass@host1/databasename1',
  'db2'=>'mysql://username2:pass@host2/databasename2',
/*
**  Сколько угодно параметров для требуемого количества баз данных
**  Индексы кроме 'default' могут быть любыми.  
*/

);

Теперь если в сниппете, модуле, блоке или материале вам понадобятся данные из других баз, используем следующий код:

/*
** Делаем активной базу с параметрами под индексом 'db1'
*/

db_set_active('db1');
$result = db_query("Здесь нужный вам запрос к таблицам в db1")
/*
**  Здесь обрабатываем результат первого запроса
*/

// Делаем активной базу с параметрами под индексом 'db2'
db_set_active('db2');
$result = db_query("Здесь нужный вам запрос к таблицам в db2")
/*
**  Здесь обрабатываем результат второго запроса
*/

/*
**  В конце ОБЯЗАТЕЛЬНО делаем активной «родную» базу,
**  чтобы Drupal мог нормально завершить обработку страницы.
*/

db_set_active('default');

Переключаться между базами можно сколько угодно раз – после первого обращения Drupal кеширует ресурс соединения с БД в массиве $db_conns и повторного соединения не производится.

Если вам недоступно редактирование файла установок, то можно установить требуемое соединение сразу в PHP-коде:

/*
** Получаем глобальные переменные Drupal
*/

global $db_url;
/*
** Добавляем свою строку подключения к БД, а родную оставляем под индексом 'default'
*/

$db_url = array(
  "default"=>$db_url,
  "db1"=>"mysql://username1:pass@host1/databasename1"
);
/*
** Делаем активной базу с параметрами под индексом 'db1'
*/

db_set_active('db1');
$result = db_query("Здесь нужный вам запрос к таблицам в db1")
/*
**  Здесь обрабатываем результат запроса
*/

/*
**  В конце ОБЯЗАТЕЛЬНО делаем активной «родную» базу,
**  чтобы Drupal мог нормально завершить обработку страницы.
*/

db_set_active('default');

Указанные решения работают в D5 и D6.

Единственное ограничение – тип баз данных должен быть одним для всех соединений, т.е. следующий код вызовет ошибку:

$db_url = array(
  'default'=>'mysqli://username:pass@localhost/databasename',
  'db1'=>'pgsql://username1:pass@host1/databasename1',
);

Вы получите: Cannot redeclare db_status_report() (previously declared in /var/www/mysite/includes/database.mysqli.inc:23) in /var/www/mysite.ru/includes/database.pgsql.inc

Устранить данное ограничение можно только хаком ядра Drupal. Хак получается весьма объемистым. С указанной ошибкой все дело не в функции db_status_report(), а со способом которым Drupal подключает интерфейс требуемого типа баз данных. Дело в том, что названия всех функций (за исключением одной – db_check_setup) находящихся в файлах database.pgsql.inc, database.mysql.inc, database.mysqli.inc и database.mysql-common.inc совпадают, что приводит к конфликту в именах 41 функции. Поэтому использовать несколько подключений к базам разных типов можно только полностью переписав слой абстракции баз данных в Drupal.

Чтобы сделать это откроем все упомянутые файлы в редакторе.

Из файлов database.mysql.inc и database.mysqli.inc удаляем строчку

require_once './includes/database.mysql-common.inc';

Содержимое файла database.mysql-common.inc копируем в файлы database.mysql.inc и database.mysqli.inc, а сам файл делаем пустым.

Далее во всех трех файлах database.pgsql.inc, database.mysql.inc, database.mysqli.inc переименовываем все функции добавляя к их именам постфикс типа базы данных: в файле database.pgsql.inc – _pgsql; в файле database.mysql.inc – _mysql; в файле database.mysqli.inc – _mysqli. Переименовывать не надо только функцию db_check_setup – она уникальна для файла database.pgsql.inc.

Открываем файл database.inc и создаем там 41(!!!) функцию со следующими именами.

Все функции выглядят однотипно, различаясь только названием.

function имя_функции () {
  global $db_type;
  $args = func_get_args();
  return call_user_func_array("имя_функции_".$db_type, $args);
}

Несомненно, большой объем правок кода ядра может отпугнуть желающих использовать это. Есть еще один способ, требующий меньшего объема кода. Основная проблема в том, что в PHP обычно нет возможности удаления или переопределения (перегрузки) функций. Правда, одно из расширений PHP – Runkit позволяет сделать это. Если runkit подключен, то достаточно сделать небольшую вставку в код функции db_set_active(). Находим строчки:

    $db_type = substr($connect_url, 0, strpos($connect_url, '://'));
    $handler = "./includes/database.$db_type.inc";
    if (is_file($handler)) {
      include_once $handler;
    }
    else {
      _db_error_page("The database type '". $db_type ."' is unsupported. Please use either
'mysql' or 'mysqli' for MySQL, or 'pgsql' for PostgreSQL databases."
);
    }

    $db_conns[$name] = db_connect($connect_url);
  }

и добавляем свой код:

    $db_type = substr($connect_url, 0, strpos($connect_url, '://'));
    $handler = "./includes/database.$db_type.inc";
    if (is_file($handler)) {
      $fnames = array(
        '_db_create_field_sql', '_db_create_key_sql', '_db_create_keys_sql',
        '_db_process_field', '_db_process_field', '_db_query',
        'db_add_field', 'db_add_index', 'db_add_primary_key',
        'db_add_unique_key', 'db_affected_rows', 'db_change_field',
        'db_column_exists', 'db_connect', 'db_create_table_sql',
        'db_decode_blob', 'db_distinct_field', 'db_drop_field',
        'db_drop_index', 'db_drop_primary_key', 'db_drop_table',
        'db_drop_unique_key', 'db_encode_blob', 'db_error',
        'db_escape_string', 'db_fetch_array', 'db_fetch_object',
        'db_field_set_default', 'db_field_set_no_default', 'db_last_insert_id',
        'db_lock_table', 'db_query', 'db_query_range', 'db_query_temporary',
        'db_rename_table', 'db_result', 'db_status_report',
        'db_table_exists', 'db_type_map', 'db_unlock_tables', 'db_version',
      );
      foreach ($fnames as $fname) {
        @runkit_function_remove($fname);
      }
      include $handler;
    }
    else {
      _db_error_page("The database type '". $db_type ."' is unsupported. Please use either
'mysql' or 'mysqli' for MySQL, or 'pgsql' for PostgreSQL databases."
);
    }

    $db_conns[$name] = db_connect($connect_url);
  }

В этом случае определения всех функций баз данных будут удалены, а при подключении файла другого типа БД определены заново.

Источники:

[ru-api=db_set_active]
[man=runkit_function_remove]
2 соединения с БД
Переключение между базами
Статья в блоге автора

Комментарии

Аватар пользователя VladSavitsky VladSavitsky 17 сентября 2008 в 17:46

Я тоже пока не представляю зачем это нужно.
Есть идеи как это можно использовать?
Разные базы дают возможность получить разные сайты на одном коде.
Напоминает мультисайтовость.

В принципе так можно реализовать мультиязычность без всяких хистростей с отдельным переводом меню, блоков и прочего. Одна база для каждого языка. Это может быть удобно. Статистика будет учитывать язык, блоки можно по разному расставить. Да и база не содержит лишних данных по другим языкам, но я думаю, что выиграш от этого не большой (разве что на сайте где языков десятки).

Странно, что сначала появляется инструмент, а затем придумываем, что им можно делать...
Причина должна идти сначала. А потом следствие.

Аватар пользователя cristobal-junta cristobal-junta 18 сентября 2008 в 0:42

Простейший пример: несколько сайтов с общей таблицей пользователей, юзеры используют один и тот же логин и пароль для доступа к разным сайтам. (Т.е. OpenID-философия в очень-очень грубом приближении.)

Аватар пользователя goodvin2 goodvin2 25 июля 2011 в 15:14

VladSavitsky wrote:
Я тоже пока не представляю зачем это нужно.
Есть идеи как это можно использовать?
Разные базы дают возможность получить разные сайты на одном коде.
Напоминает мультисайтовость.

В принципе так можно реализовать мультиязычность без всяких хистростей с отдельным переводом меню, блоков и прочего. Одна база для каждого языка. Это может быть удобно. Статистика будет учитывать язык, блоки можно по разному расставить. Да и база не содержит лишних данных по другим языкам, но я думаю, что выиграш от этого не большой (разве что на сайте где языков десятки).

Странно, что сначала появляется инструмент, а затем придумываем, что им можно делать...
Причина должна идти сначала. А потом следствие.

Вариант использования данной схемы:
Есть 2-е и более баз данных. Основная master остальные реплицирующие.
1. Для распределения нагрузки всех анонимов (запросы на чтение) направляем на реплицирующие базы, а зарегистрированных на основную с возможностью писать в базу (материалы и комменты).
2. Распределение анонимов по реплицирующим базам, быстрая доставка контента в зависимости от географического положения юзера (соответствующий модуль белым домом разработан и отдан сообществу, нечто на подобии CDN - сеть доставки (и дистрибуции) контента англ. Content Delivery Network или Content Distribution Network, CDN).
Как я понимаю код обращения к базе нужно не в нодах и блоках писать, а использовать свой класс, 7 особенно это хорошо понимает.
Какие будут замечания по схеме?

Аватар пользователя iryston iryston 17 сентября 2008 в 21:45

Красота! Делал такое извращение не на Друпале, было три сайта с разными БД, на одном хосте, новостной блок должен был обновляться одновременно. Помучался с подключениями, а тут ... красота!

Аватар пользователя VladSavitsky VladSavitsky 17 сентября 2008 в 23:36

eagle,
У вас наверное был так называемый "мультисайтинг" - общий код для разных сайтов, а базы разные, но для каждого сайта создается отдельный файл с настройками для доступа к базе данных и может быть свой набор модулей и тем.

А то, что предлагает direqtor, позволяет иметь доступ к нескольким базам из одной, но одинаковой для всех базы кода PHP. Модули и темы будут одинаковые.

Хотя!
Можно сделать общий набор модулей, а в каждой базе будут использоваться только нужные (не все).
Короче, я так понимаю, что этот способ - очень простая и элегантная альтернатива общеизвестному методы построения мультисайтинга.
Ура!
Ещё есть варианты?

Аватар пользователя direqtor direqtor 18 сентября 2008 в 5:10

Ну, не я предложил. Решение описано в англоязычной документации. Кроме того и на d.ru имеется (см. источники). Факт в том, что здесь многие решения появляются как ответ на чей-то вопрос и часто теряются в массе ненужной информации.

Аватар пользователя lemark lemark 18 сентября 2008 в 0:49

Мне это полезно с точки зрения безопасности. В отдельную базу на отдельном сервере я собираюсь вынести данные о пользовательских денежных счетах

Аватар пользователя direqtor direqtor 18 сентября 2008 в 5:12

Ну, а я пожалуй на новом проекте блок курсов валют буду дергать со старого. Там все отлично работает, а в друпале подходящего мне модуля пока нету.

Аватар пользователя VanDerKeen VanDerKeen 18 сентября 2008 в 5:43

А я тоже использую эту возможность. У меня сайт для универа. Там будет использоваться информация (данные о студентах), берущаяся из другой базы, с другого сервака. Только проблема в том, что сайт на mysql, а база универа на mssql. Подскажите, как ядро хакать надо для этого?

Аватар пользователя direqtor direqtor 18 сентября 2008 в 6:30

Может сегодня вечером покодю, пока занят. В любом случае заметку надо дополнить, но вот вроде бы возможности подключения к mssql в Друпал нет.

Аватар пользователя whisk@drupal.org whisk@drupal.org 18 сентября 2008 в 16:00

Очень полезная информация!

Переключения БД при помощи db_set_active() нужны, например, если нам нужна интеграция не-друпаловским сайтом или скриптом.
И насколько я знаю, нельзя задать разные префиксы таблиц для разных соединений с БД.

Добавлю: классический drupal-мультисайтинг c разделением таблиц делается путем настройки settings.php.
Пример1. Допустим, у нас на одном виртуальном хосте два сайта - site1.ru и site2.ru. Эти два независимых сайта должны иметь общую базу пользователей.

Создаем следующую структуру директорий:

/sites/
       site1.ru/
                settings.php
       site2.ru/
                settings.php

Подробное пояснение, как называть директории с файлами настроек, есть в начале default/settings.php.

В site1.ru/settings.php пишем:
<?php
$db_url = 'mysql://root@localhost/db'; # общая БД
$db_prefix = array(
'default' => 'site1_', # разный префикс таблиц
'users' => 'shared_', # префикс общей таблицы (shared_users)
?>

В site2.ru/settings.php пишем:
<?php
$db_url = 'mysql://root@localhost/db'; # общая БД
$db_prefix = array(
'default' => 'site2_', # разный префикс таблиц
'users' => 'shared_', # префикс общей таблицы (shared_users)
?>

В этом примере сайты имеют общий набор пользователей, но каждый сайт может определять свои права и роли пользователей.

Пример 2. Допустим, у нас на одном виртуальном хосте два сайта - site.ru и pda.site.ru. Сайт pda.site.ru должен представлять ту же информацию, что и site.ru, но в виде, удобном для hand-held devices. Помимо предоставления пользователям других тем на выбор, необходимо использовать специальные модули (не нужные на основном сайте) и другую структуру меню.

Создаем следующую структуру директорий:

/sites/
       site.ru/
               settings.php
       pda.site.ru/
                   settings.php

В site.ru/settings.php пишем:
<?php
$db_url = 'mysql://root@localhost/db'; # общая БД
$db_prefix = array(
'default' => 'site_', # общий префикс
?>

В pda.site.ru/settings.php пишем:
<?php
$db_url = 'mysql://root@localhost/db'; # общая БД
$db_prefix = array(
'default' => 'site_', # общий префикс

# отдельные таблицы для независимой настройки модулей
'variable' => 'pda_',
'system' => 'pda_',
'permission' => 'pda_', # нужна отдельная таблица прав доступа пользователей, т.к. включение/отключение модулей приводит к её изменениям
'menu' => 'pda_',

# независимый кэш
'cache' => 'pda_',
'cache_menu' => 'pda_',
?>

Сайты имеют полностью одинаковое содержимое, пользователей, но могут иметь различный набор настроенных модулей, меню, тем.

Ограничение стандартного мультисайтинга: все таблицы должны находится в одной БД.

Аватар пользователя direqtor direqtor 19 сентября 2008 в 3:54

Кажется был вариант для разных бд на одном серваке. Насколько помню выглядел так:

$db_url = 'mysql://root@localhost/db1'; # первая БД
$db_prefix = array(
  'default'       => '', # без префикса в db1
  'users'       => 'db2.', # без префикса в db2
);

Ну, вообще, конечно жаль, что в Drupal нет внутренней поддержки работы на нескольких базах. Правда, тут возникает масса интересных коллизий. К примеру, как выполнять запросы объединяющие несколько таблиц. Если они находятся на одном сервере, еще куда ни шло, DB-layer справится. А если на разных?
Например, выборка всех материалов пользователя станет невозможной, если инфу о юзверях хранить в MySQL, а о нодах – в Postgre.

Аватар пользователя axel axel 19 сентября 2008 в 11:36

direqtor wrote:
Ну, вообще, конечно жаль, что в Drupal нет внутренней поддержки работы на нескольких базах. Правда, тут возникает масса интересных коллизий. К примеру, как выполнять запросы объединяющие несколько таблиц. Если они находятся на одном сервере, еще куда ни шло, DB-layer справится. А если на разных?
Например, выборка всех материалов пользователя станет невозможной, если инфу о юзверях хранить в MySQL, а о нодах – в Postgre.

А никак. Имхо средств такого объединения меду СУБД разных типов нет. В MySQL можно объединять таблицы если они расположены в разных базах на одном сервере.

Аватар пользователя SiR SiR 23 сентября 2008 в 14:27

Можно еще проще, но этот метод не совсем правильный.
Можно использовать только, если разные базы расположены на одном сервере и доступны "пользователю mysql", который "подключается" к самой базе drupal. Я бы рекомендовал использовать его только в целях тестирования или для "одноразовых" манипуляций с базами.

Прямо в модуле пишем

$sql = "SELECT * FROM basa2.table1"; //Не используя фигурные скобки
$result = db_query($sql); //Дальше все как обычно
Аватар пользователя bratello bratello 2 января 2009 в 21:17

Полезная статейка. Возникал такой вопрос, оказывается есть гибкое решение. А мотивация для использования очень простая: масштабируемость. К примеру, сайт потипу одноклассники наверняка имеет огромную базу данных пользователей, таблицу пользователей можно держать на одном сервере, а их аккаунты с постами на нескольких десятках серверов. На протяжении нескольких месяцев можно собрать статистику активности пользователя, пользователь из Владивостока появляется на сайте в ночное (по Москве) время, а житель Рязани в дневное время, далее ДБ балансер (какой нибудь автоматический экшн) может автоматически перекинуть аккаунты на разные сервера, для того чтобы сбалансировать нагрузку на сервера баз данных. Выигрыш получается двойной: оптимальнее используются вычислительные мощности процессоров, и с другой стороны работа с относительно маленькими пейдж файлами базы данных значительно ускоряет работу SQL сервера.

Интересно, в дополнение к ДБ балансеру, есть ли решение для WEB балансера, для полноценного масштабирования Веб Аппликации? По типу того же балансера одноклассников, когда логин делается по одному адресу, после чего балансер редиректом отправляет запрос на менее загруженный Веб Сервер. Это чуть сложнее чем ДБ балансер, больше ньюансов, нужно деликатно разделить таблицы сессий персонально для каждого веб сервера, таблицу пользователей держать общую, а таблицу постов распределить между разными SQL серверами. В принципе можно воспользоваться все той же статистикой, и не делать динамический Веб балансер, а прописать в аккаунте адрес того Веб Сервера, который будет постоянно принимать редирект для построения контента страничек.

Аватар пользователя bratello bratello 21 января 2009 в 16:44

"direqtor" wrote:
Ну, вообще, конечно жаль, что в Drupal нет внутренней поддержки работы на нескольких базах. Правда, тут возникает масса интересных коллизий. К примеру, как выполнять запросы объединяющие несколько таблиц. Если они находятся на одном сервере, еще куда ни шло, DB-layer справится. А если на разных?

Жаль что MySQL не поддерживает linked tables, это решало бы сразу две проблемы - нет необходимости в драйверах для Друпал, и возможность работать как бы с одной базой, без переключения, с поддержкой join и всего остального. Правда возникали бы проблемы из-за разницы в диалектах SQL. Хранимые процедуры тоже не мешало бы иметь, с прекомпиляцией...

одна интересная статейка: http://www.razgonka.ru/info/39

Аватар пользователя faustman faustman 23 июля 2009 в 18:17

Quote:
Содержимое файла database.mysql-common.inc копируем в файлы database.mysql.inc и database.mysqli.inc, а сам файл делаем пустым.

Можно поинтересоваться: - А смысл этого действия?

Файл database.mysql-common.inc подтягивают только database.mysql.inc и database.mysqli.inc.

Аватар пользователя faustman faustman 23 июля 2009 в 18:32

faustman wrote:
Quote:
Содержимое файла database.mysql-common.inc копируем в файлы database.mysql.inc и database.mysqli.inc, а сам файл делаем пустым.

Можно поинтересоваться: - А смысл этого действия?

Файл database.mysql-common.inc подтягивают только database.mysql.inc и database.mysqli.inc.

Ага.. понял. Происходит разьединение mysql и mysqli.

Хм..

Аватар пользователя erikson erikson 12 февраля 2014 в 12:52

Замечательные возможности. Уже некоторое время использую. Только никак не могу найти информацию как создавать таблицы во второй БД при установке модуля.

Использую D7. Имеется 2 БД. В одной ядреные таблицы и таблицы модулей с д.орг. Во второй - таблицы моих модулей. При установке модуля таблицы БД конечно же создаются в дефольтном БД, что меня (читай - заказчика) совершенно не устраивает.

Так же в проекте для контроля таблиц используется модуль schema который ни в какую не видит перенесенные во вторую БД в ручную таблицы моих модулей. Приходится лезть в настройки модуля schema и переключать соединение с БД. Это конечно мелочи, вполне юзабельно, но хотелось бы "все в одном флаконе".

Не подскажите хорошую пилу для этих дров?