ひょんなことからTiDBを使用する機会に出会った。そしてDjangoからTiDBを利用したかった。DjangoにはデータベースバックエンドとしてMySQLを標準で選択できる。そしてTiDBはNewSQLでMySQLと互換がある。そうであれば躓くことなくDjango+TiDBを構築できるかとも思える。しかし実際にやってみると、そんなに上手くはいかない。現実というものはいつでも厳しいのだ。今回はそんな現実と向き合い、Django+TiDB構成を利用できる ようにするまでに躓き、試行錯誤した内容を書くことにした。
ALTER TABLEと同時にUNIQUE制約を追加するような操作をサポートしていない
現在のTiDBではALTER TABLEと同時にUNIQUE制約を追加するような操作をサポートしていない。それを実現するためのissue及びプルリクエストが提出されている1が、現在目下作業中といったところだ。この操作は多くのO/Rマッパーで使用されているため、サポートすることが待たれているようだ。
簡易的に複数のSQLを実行することで制限を突破する
ALTER TABLEと同時にUNIQUE制約を追加できないのであれば、複数のSQLを実行しカラムを追加した後、UNIQUE制約を追加すれば良い。そのためにはDjangoのマイグレーション時のSQLを生成しているDatabaseSchemaEditorを書き換える必要がある。 django_tidbはtidb用にdjango_tidb.schema.DatabaseSchemaEditorを実装している。このクラスはMysqlDatabaseSchemaEditorを継承している。このクラスを書き換えることで実現する。
カラム追加のためのSQLを発行しているメソッドはadd_field()であるため、これをそのまま親クラスであるMysqlDatabaseSchemaEditorの、更なる親クラスdjango.db.base.schema.BaseDatabaseSchemaEditorからコピーし、次のように変更を追加した。FIXFIXFIXで囲まれている2箇所が修正したところだ。
def add_field(self, model, field):
"""
Create a field on a model. Usually involves adding a column, but may
involve adding a table instead (for M2M fields).
"""
# Special-case implicit M2M tables
if field.many_to_many and field.remote_field.through._meta.auto_created:
return self.create_model(field.remote_field.through)
# Get the column's definition
definition, params = self.column_sql(model, field, include_default=True)
# It might not actually have a column behind it
if definition is None:
return
# Check constraints can go on the column SQL here
db_params = field.db_parameters(connection=self.connection)
if db_params["check"]:
definition += " " + self.sql_check_constraint % db_params
if (
field.remote_field
and self.connection.features.supports_foreign_keys
and field.db_constraint
):
constraint_suffix = "_fk_%(to_table)s_%(to_column)s"
# Add FK constraint inline, if supported.
if self.sql_create_column_inline_fk:
to_table = field.remote_field.model._meta.db_table
to_column = field.remote_field.model._meta.get_field(
field.remote_field.field_name
).column
namespace, _ = split_identifier(model._meta.db_table)
definition += " " + self.sql_create_column_inline_fk % {
"name": self._fk_constraint_name(model, field, constraint_suffix),
"namespace": "%s." % self.quote_name(namespace)
if namespace
else "",
"column": self.quote_name(field.column),
"to_table": self.quote_name(to_table),
"to_column": self.quote_name(to_column),
"deferrable": self.connection.ops.deferrable_sql(),
}
# Otherwise, add FK constraints later.
else:
self.deferred_sql.append(
self._create_fk_sql(model, field, constraint_suffix)
)
# FIXFIXFIXFIXFIXFIXFIXFIXFIXFIXFIXFIXFIXFIXFIXFIXFIXFIXFIXFIX
is_unique_constraint = "UNIQUE" in definition
if is_unique_constraint:
definition = definition.replace("UNIQUE", "")
# FIXFIXFIXFIXFIXFIXFIXFIXFIXFIXFIXFIXFIXFIXFIXFIXFIXFIXFIXFIX
# Build the SQL and run it
sql = self.sql_create_column % {
"table": self.quote_name(model._meta.db_table),
"column": self.quote_name(field.column),
"definition": definition,
}
self.execute(sql, params)
# FIXFIXFIXFIXFIXFIXFIXFIXFIXFIXFIXFIXFIXFIXFIXFIXFIXFIXFIXFIX
if is_unique_constraint:
self.execute(self._create_unique_sql(model, [field.column]), params)
# FIXFIXFIXFIXFIXFIXFIXFIXFIXFIXFIXFIXFIXFIXFIXFIXFIXFIXFIXFIX
# Drop the default if we need to
# (Django usually does not use in-database defaults)
if (
not self.skip_default_on_alter(field)
and self.effective_default(field) is not None
):
changes_sql, params = self._alter_column_default_sql(
model, None, field, drop=True
)
sql = self.sql_alter_column % {
"table": self.quote_name(model._meta.db_table),
"changes": changes_sql,
}
self.execute(sql, params)
# Add an index, if required
self.deferred_sql.extend(self._field_indexes_sql(model, field))
# Reset connection if required
if self.connection.features.connection_persists_old_columns:
self.connection.close()
このスキーマエディタを使用することでエラーは発生しなくなった。
TEXT/BLOB/JSON型のカラムにデフォルトの値を設定できない
空文字であれば出来る。それ以外はできない。仕方がないのでSchemaEditrに次のコードを追加した。
is_contain_default_statement = 'DEFAULT (%s)' in definition
if is_contain_default_statement:
definition = definition.replace('DEFAULT (%s)', '')
if len(params) > 1:
raise ValueError("Cannot detected collect migration")
params = []
以下は検証用のメモ。
set sql_mode='';
CREATE TABLE test_text_default_json(c1 JSON NOT NULL DEFAULT '');
CREATE TABLE test_text_default_json2(c1 JSON NULL DEFAULT '');
ALTER TABLE `test_text_default_json` MODIFY COLUMN `c1` text DEFAULT '';
DROP TABLE test_text_default_json;
DROP TABLE test_text_default_json2;