目的
- CentOS7
- MariaDB:10.1.6
で動作している Redmine 3.4.8 の データベースを
- PostgreSQL 11.1
に変更し、データを移行する
前提
- Redmineのディレクトリを
%REDMINE_DIR%
と記述します - Redmineの実行ユーザを
%REDMINE_USER%
と記述します - mysqlコマンドが使用可能
- MySQLのタイムゾーンはJST
- psqlコマンドが使用可能
あえてpgLoaderを使わない
のため、現実的には同一ホスト間で動くMySQL→PostgreSQLへの移行となります。
手順
- Redmineを停止する(割愛)
- PostgreSQLをインストールする(割愛)
- PostgreSQLにRedmine用のDB、ユーザを作成する(割愛)
4.embulkを取得し、プラグインをインストールする
https://github.com/embulk/embulk を参考に
curl --create-dirs -o ~/.embulk/bin/embulk -L "https://dl.embulk.org/embulk-latest.jar"
chmod +x ~/.embulk/bin/embulk
~/.embulk/bin/embulk gem install embulk-input-mysql
~/.embulk/bin/embulk gem install embulk-output-postgresql
5.RedmineのDB設定を変更する
変更前
%REDMINE_DIR%/config/database.yml
production:
adapter: mysql2
database: YOUR_DB_MS
host: YOUR_HOST_MS
username: YOUR_NAME_MS
password: YOUR_PASS_MS
encoding: utf8
変更後
%REDMINE_DIR%/config/database.yml
production:
adapter: postgresql
database: YOUR_DB_PG
host: YOUR_HOST_PG
username: YOUR_NAME_PG
password: YOUR_PASS_PG
encoding: utf8
6. postgresqlモジュールのgemを導入する
su - %REDMINE_USER% -s /bin/bash
cd %REDMINE_DIR%
bundle config build.pg --with-pg-config=/usr/pgsql-11/bin/pg_config
bundle install
7. Redmineの初期DB、およびプラグインのDBマイグレーション
RAILS_ENV=production bundle exec rake db:migrate
RAILS_ENV=production bundle exec rake redmine:plugins:migrate
念のため、初期投入のデータを確認。
psql -U YOUR_NAME_PG -h YOUR_HOST_PG YOUR_DB_PG
select relname, n_live_tup from pg_stat_user_tables where n_live_tup > 0;
relname | n_live_tup
-------------------+------------
users | 3
roles | 2
email_addresses | 1
schema_migrations | 281
schema_migrations はこの作業で作成されたレコードのため、移行対象外とします。
8.スクリプトによる移行
留意点
- embulkはBLOBに対応していないため、単純移行ではなく、queryが必要
- MySQLのIDはAUTO INCREMENT列のため、MAX(ID)をPostgreSQLのシリアル列が持つシーケンスに反映する必要がある
- PostgreSQLへの移行のみを目的としているため、truncate_insertとしている
- embulkの実行が失敗した場合にはテンポラリテーブルが残ることがあるため、PostgreSQLのテーブルを全削除して 「7. Redmineの初期DB、およびプラグインのDBマイグレーション」 から再実行
ファイル構成
|-- extra
| |-- import_in_progresses.yml.liquid
| |-- open_id_authentication_associations.yml.liquid
| `-- wiki_content_versions.yml.liquid
|-- mysql2pgsql.sh
`-- mysql2pgsql.yml.liquid
extra:BLOBを含むテーブルの定義ファイルを配置
mysql2pgsql.sh:移行スクリプト
mysql2pgsql.yml.liquid:BLOBを含まないテーブルの定義ファイル
embulk定義ファイル
BLOB列を単に {value_type: string}
とした際には文字化けを起こしたため、convert (XXX using UTF8)
としています。
import_in_progresses.yml.liquid
in:
type: mysql
host: {{ env.MYSQL_HOST }}
user: {{ env.MYSQL_USER }}
password: {{ env.MYSQL_PASS }}
database: {{ env.MYSQL_DB }}
default_timezone: "Asia/Tokyo"
options: {useLegacyDatetimeCode: false, serverTimezone: Asia/Tokyo}
query: |
SELECT id
,user_id
,quote_char
,col_sep
,encoding
,created
,convert(csv_data using utf8) as csv_data
FROM {{ env.TABLE }}
column_options:
csv_data: {value_type: string}
out:
type: postgresql
host: {{ env.PGSQL_HOST }}
user: {{ env.PGSQL_USER }}
password: {{ env.PGSQL_PASS }}
database: {{ env.PGSQL_DB }}
table: {{ env.TABLE }}
default_timezone: "Asia/Tokyo"
mode: truncate_insert
column_options:
csv_data: {value_type: string}
open_id_authentication_associations.yml.liquid
in:
type: mysql
host: {{ env.MYSQL_HOST }}
user: {{ env.MYSQL_USER }}
password: {{ env.MYSQL_PASS }}
database: {{ env.MYSQL_DB }}
default_timezone: "Asia/Tokyo"
options: {useLegacyDatetimeCode: false, serverTimezone: Asia/Tokyo}
query: |
SELECT id
,issued
,lifetime
,handle
,assoc_type
,convert(server_url using utf8) as server_url
,convert(secret using utf8) as secret
FROM {{ env.TABLE }}
column_options:
server_url: {value_type: string}
secret: {value_type: string}
out:
type: postgresql
host: {{ env.PGSQL_HOST }}
user: {{ env.PGSQL_USER }}
password: {{ env.PGSQL_PASS }}
database: {{ env.PGSQL_DB }}
table: {{ env.TABLE }}
default_timezone: "Asia/Tokyo"
mode: truncate_insert
column_options:
server_url: {value_type: string}
secret: {value_type: string}
wiki_content_versions.yml.liquid
in:
type: mysql
host: {{ env.MYSQL_HOST }}
user: {{ env.MYSQL_USER }}
password: {{ env.MYSQL_PASS }}
database: {{ env.MYSQL_DB }}
default_timezone: "Asia/Tokyo"
options: {useLegacyDatetimeCode: false, serverTimezone: Asia/Tokyo}
query: |
SELECT id
,wiki_content_id
,page_id
,author_id
,convert(data using utf8) as data
,compression
,comments
,updated_on
,version
FROM {{ env.TABLE }}
column_options:
data: {value_type: string}
out:
type: postgresql
host: {{ env.PGSQL_HOST }}
user: {{ env.PGSQL_USER }}
password: {{ env.PGSQL_PASS }}
database: {{ env.PGSQL_DB }}
table: {{ env.TABLE }}
default_timezone: "Asia/Tokyo"
mode: truncate_insert
column_options:
data: {value_type: string}
mysql2pgsql.yml.liquid
in:
type: mysql
host: {{ env.MYSQL_HOST }}
user: {{ env.MYSQL_USER }}
password: {{ env.MYSQL_PASS }}
database: {{ env.MYSQL_DB }}
table: {{ env.TABLE }}
default_timezone: "Asia/Tokyo"
options: {useLegacyDatetimeCode: false, serverTimezone: Asia/Tokyo}
out:
type: postgresql
host: {{ env.PGSQL_HOST }}
user: {{ env.PGSQL_USER }}
password: {{ env.PGSQL_PASS }}
database: {{ env.PGSQL_DB }}
table: {{ env.TABLE }}
mode: truncate_insert
default_timezone: "Asia/Tokyo"
シェルスクリプト
基本的にはMySQL/PostgreSQLの接続情報のみ変更してください。
mysql/psqlのディレクトリが違う場合はよしなに。
#!/bin/bash
#######################################################
# 環境依存値
#######################################################
export MYSQL_HOST=YOUR_HOST_MS
export MYSQL_DB=YOUR_DB_MS
export MYSQL_USER=YOUR_NAME_MS
export MYSQL_PASS=YOUR_PASS_MS
MYSQL_BIN=/bin/mysql
export PGSQL_HOST=YOUR_HOST_PG
export PGSQL_DB=YOUR_DB_PG
export PGSQL_USER=YOUR_NAME_PG
export PGSQL_PASS=YOUR_PASS_PG
PGSQL_BIN=/usr/pgsql-11/bin/psql
EMBULK_BIN=~/.embulk/bin/embulk
export PGPASSWORD=${PGSQL_PASS}
export MYSQL_PWD=${MYSQL_PASS}
LOG_FILE=./mysql2pgsql_$(date +'%Y%m%d_%H%M%S').log
#######################################################
# Serial列を持つテーブルの抽出(PostgreSQL)
#######################################################
SERIAL_TABLES=$(export LANG=C ; $PGSQL_BIN ${PGSQL_DB} -h ${PGSQL_HOST} -U ${PGSQL_USER} -c '\d' | grep sequence | awk '{print $3}' | sed -e 's/_id_seq$//g')
#######################################################
# AUTO INCREMENTの値をsequenceに反映
#######################################################
for TABLE in ${SERIAL_TABLES}
do
# MySQLの該当テーブルからMAX(ID)を取得
MAX_ID=$(${MYSQL_BIN} ${MYSQL_DB} -h ${MYSQL_HOST} -u ${MYSQL_USER} -e " SELECT MAX(ID) from ${TABLE}" --skip-column-names -s)
# NULLでなければ(PostgreSQLのシーケンスに値をセット
if [ "${MAX_ID}" != "NULL" ]; then
$PGSQL_BIN ${PGSQL_DB} -h ${PGSQL_HOST} -U ${PGSQL_USER} -c "SELECT setval('${TABLE}_id_seq', ${MAX_ID}, true);" 1>/dev/null
fi
done
#######################################################
# テーブル一覧の抽出(MySQL)
#######################################################
ALL_TABLES=$(${MYSQL_BIN} ${MYSQL_DB} -h ${MYSQL_HOST} -u ${MYSQL_USER} -e "show tables" --skip-column-names -s)
#######################################################
# MySQLからPostgreSQLへテーブルデータを移行
#######################################################
for TABLE in ${ALL_TABLES}
do
# schema_migrationsは移行しない
if [ "$TABLE" != "schema_migrations" ]; then
export TABLE
echo importing ${TABLE} ...
# queryが書かれた定義ファイルがある場合はそちらを実行
if [ -e extra/${TABLE}.yml.liquid ]; then
${EMBULK_BIN} run extra/${TABLE}.yml.liquid 1>>${LOG_FILE}
# そうでなければ共通の定義ファイル
else
${EMBULK_BIN} run mysql2pgsql.yml.liquid 1>>${LOG_FILE}
fi
# 一応、件数だけ突合
MYSQL_CNT=$(${MYSQL_BIN} ${MYSQL_DB} -h ${MYSQL_HOST} -u ${MYSQL_USER} -e "SELECT COUNT(*) FROM ${TABLE}" --skip-column-names -s)
PGSQL_CNT=$($PGSQL_BIN ${PGSQL_DB} -h ${PGSQL_HOST} -U ${PGSQL_USER} -c "SELECT COUNT(*) FROM ${TABLE}" -t | tr -d ' ')
if [ ${MYSQL_CNT} == ${PGSQL_CNT} ]; then
RSLT=match
else
RSLT=not match!
fi
echo " ${RSLT} ... MySQL:${MYSQL_CNT} PostgreSQL:${PGSQL_CNT}"
fi
done
おわりに
- 新規チケット作成
- 新規プロジェクト作成
- Wikiの履歴(BLOB列)
- 既存のファイルダウンロード
などが問題ないことは確認できました。