[[Djangoを読む]]
 
 #contents
 
 *はじめに [#r08c1023]
 
 makemigrationsでマイグレーションが作られる様子を見たのでいよいよマイグレーションが適用される様子を見ていきます。まずは復習、migrateコマンドの流れです。
 
 +アプリのmigrationsディレクトリにあるファイルを読み込む(django.db.migrations.loader.MigrationLoader)
 +実行するマイグレーションの決定(django.db.migrations.executor.MigrationExecutor)
 +各マイグレーションの実行(django.db.migrations.executor.MigrationExecutor)
 
 で、前に見たように「各マイグレーションの実行」で肝となるのはMigrationExecutorクラスのapply_migrationメソッドの以下の部分
 
 #code(Python){{
 with self.connection.schema_editor(atomic=migration.atomic) as schema_editor:
     state = migration.apply(state, schema_editor)
 }}
 
 connectionはデータベースとの接続、より具体的に言うと、django.db.backends.sqlite3.baseのDatabaseWrapperオブジェクトです。(もちろん、使うDBが別の場合はsqlite3の部分が変わります)
 
 *django/db/backends/base/base.py [#o549a7fd]
 
 schema_editorメソッドは基底クラスのBaseDatabaseWrapperの方に定義されています。
 
 #code(Python){{
     def schema_editor(self, *args, **kwargs):
         """
         Returns a new instance of this backend's SchemaEditor.
         """
         if self.SchemaEditorClass is None:
             raise NotImplementedError(
                 'The SchemaEditorClass attribute of this database wrapper is still None')
         return self.SchemaEditorClass(self, *args, **kwargs)
 }}
 
 ちゃんと動くので、もちろんSchemaEditorClassは適切に設定されています。こんどはsqlite3以下の方(の抜粋)
 
 #code(Python){{
 from .schema import DatabaseSchemaEditor                    # isort:skip
 
 class DatabaseWrapper(BaseDatabaseWrapper):
     SchemaEditorClass = DatabaseSchemaEditor
 }}
 
 *django/db/backends/base/schema.py [#q0aa141c]
 
 SchemaEditorの方も同じようにbaseと個別のDBMS用の実装の構成になっています。
 
 基底クラスのBaseDatabaseSchemaEditorに以下の記述があります。
 
 #code(Python){{
     def __enter__(self):
         self.deferred_sql = []
         if self.atomic_migration:
             self.atomic = atomic(self.connection.alias)
             self.atomic.__enter__()
         return self
 
     def __exit__(self, exc_type, exc_value, traceback):
         if exc_type is None:
             for sql in self.deferred_sql:
                 self.execute(sql)
         if self.atomic_migration:
             self.atomic.__exit__(exc_type, exc_value, traceback)
 }}
 
 with文に入るときに__enter__が実行され、出るときに__exit__が実行されるようです。
 atomicは、実際なところatomicに実行されるようなのですがめんどくさいので無視。それ以外で注目するところとしてはdeferred_sqlです。名前から判断するとマイグレーションで指示されているoperationに対応するSQLをためておいて最後に実行している雰囲気です。
 
 サブクラスのsqlite3.schema.DatabaseSchemaEditorでは__enter__時にデータベースとやり取りしているようですが、個別の話なのでまだそこには踏み込まないことにして、先にmigrationの方を確認しましょう。
 
 *django/db/migrations/migration.py [#ef1bccbf]
 
 Migrationクラスのapplyメソッド
 
 #code(Python){{
     def apply(self, project_state, schema_editor, collect_sql=False):
         """
         Takes a project_state representing all migrations prior to this one
         and a schema_editor for a live database and applies the migration
         in a forwards order.
 
         Returns the resulting project state for efficient re-use by following
         Migrations.
         """
         for operation in self.operations:
             # If this operation cannot be represented as SQL, place a comment
             # there instead
             if collect_sql:
                 # 省略
             # Save the state before the operation has run
             old_state = project_state.clone()
             operation.state_forwards(self.app_label, project_state)
             # Run the operation
             atomic_operation = operation.atomic or (self.atomic and operation.atomic is not False)
             if not schema_editor.atomic_migration and atomic_operation:
                 # Force a transaction on a non-transactional-DDL backend or an
                 # atomic operation inside a non-atomic migration.
                 with atomic(schema_editor.connection.alias):
                     operation.database_forwards(self.app_label, schema_editor, old_state, project_state)
             else:
                 # Normal behaviour
                 operation.database_forwards(self.app_label, schema_editor, old_state, project_state)
         return project_state
 }}
 
 atomicかどうかはともかく、各operationのdatabase_forwardsに続きます。
 
 **django/db/migrations/operations [#obee2162]
 
 基底クラスOperationのdatabase_forwardsは例外を投げるだけなので個々のサブクラス、models.CreateModelクラスのdatabase_forwardsを見てみましょう。
 
 #code(Python){{
     def database_forwards(self, app_label, schema_editor, from_state, to_state):
         model = to_state.apps.get_model(app_label, self.name)
         if self.allow_migrate_model(schema_editor.connection.alias, model):
             schema_editor.create_model(model)
 }}
 
 allow_migrateって、許可してもらわないと困るのですが(笑)、この先はOperation(基底クラス)→router(django.db.utilsのConnectionRouterオブジェクト)と進み処理が行われています。DATABASE_ROUTERSはモデルにより保存するデータベースを振り分ける仕組みのようですが、普通に使っている分には空配列なので単純にTrueが返されることになります。というわけでschema_editorのcreate_modelメソッドが呼び出されます。
 
 *BaseDatabaseSchemaEditor.create_model [#dad76d68]
 
 さて、SchemaEditorに戻ってcreate_modelメソッドです。まず前半
 
 #code(Python){{
     def create_model(self, model):
         """
         Takes a model and creates a table for it in the database.
         Will also create any accompanying indexes or unique constraints.
         """
         # Create column SQL, add FK deferreds if needed
         column_sqls = []
         params = []
         for field in model._meta.local_fields:
             # SQL
             definition, extra_params = self.column_sql(model, field)
             if definition is None:
                 continue
             # Check constraints can go on the column SQL here
             db_params = field.db_parameters(connection=self.connection)
             if db_params['check']:
                 definition += " CHECK (%s)" % db_params['check']
             # Autoincrement SQL (for backends with inline variant)
             col_type_suffix = field.db_type_suffix(connection=self.connection)
             if col_type_suffix:
                 definition += " %s" % col_type_suffix
             params.extend(extra_params)
             # FK
             if field.remote_field and field.db_constraint:
                 to_table = field.remote_field.model._meta.db_table
                 to_column = field.remote_field.model._meta.get_field(field.remote_field.field_name).column
                 if self.connection.features.supports_foreign_keys:
                     self.deferred_sql.append(self._create_fk_sql(model, field, "_fk_%(to_table)s_%(to_column)s"))
                 elif self.sql_create_inline_fk:
                     definition += " " + self.sql_create_inline_fk % {
                         "to_table": self.quote_name(to_table),
                         "to_column": self.quote_name(to_column),
                     }
             # Add the SQL to our big list
             column_sqls.append("%s %s" % (
                 self.quote_name(field.column),
                 definition,
             ))
             # Autoincrement SQL (for backends with post table definition variant)
             if field.get_internal_type() in ("AutoField", "BigAutoField"):
                 autoinc_sql = self.connection.ops.autoinc_sql(model._meta.db_table, field.column)
                 if autoinc_sql:
                     self.deferred_sql.extend(autoinc_sql)
 }}
 
 モデルのフィールドごとにSQLの列を作っている雰囲気です。こういう場合は入力と出力を見てどのようなことが行われているか見るのが一番、ということで、入力、
 
 #code(Python){{
         migrations.CreateModel(
             name='Choice',
             fields=[
                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                 ('choice_text', models.CharField(max_length=200)),
                 ('votes', models.IntegerField(default=0)),
             ],
         ),
 }}
 
 対応する出力(SQLiteの場合。「python manage.py sqlmigrate polls 0001」で確認できます)
 
  CREATE TABLE "polls_choice" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
  "choice_text" varchar(200) NOT NULL, "votes" integer NOT NULL);
 
 では追いかけていきましょう。
 
 **column_sql [#p5bf0d36]
 
 coolumn_sqlメソッド。include_defaultがFalseで呼ばれているからデフォルト値の処理がされてないと思うけどそれでいいのだろうか。
 
 #code(Python){{
     def column_sql(self, model, field, include_default=False):
         """
         Takes a field and returns its column definition.
         The field must already have had set_attributes_from_name called.
         """
         # Get the column's type and use that as the basis of the SQL
         db_params = field.db_parameters(connection=self.connection)
         sql = db_params['type']
         params = []
         # Check for fields that aren't actually columns (e.g. M2M)
         if sql is None:
             return None, None
         # Work out nullability
         null = field.null
         # If we were told to include a default value, do so
         include_default = include_default and not self.skip_default(field)
         if include_default:
             # 省略
         # Oracle treats the empty string ('') as null, so coerce the null
         # option whenever '' is a possible value.
         if (field.empty_strings_allowed and not field.primary_key and
                 self.connection.features.interprets_empty_strings_as_nulls):
             null = True
         if null and not self.connection.features.implied_column_null:
             sql += " NULL"
         elif not null:
             sql += " NOT NULL"
         # Primary key/unique outputs
         if field.primary_key:
             sql += " PRIMARY KEY"
         elif field.unique:
             sql += " UNIQUE"
         # Optionally add the tablespace if it's an implicitly indexed column
         tablespace = field.db_tablespace or model._meta.db_tablespace
         if tablespace and self.connection.features.supports_tablespaces and field.unique:
             sql += " %s" % self.connection.ops.tablespace_sql(tablespace, inline=True)
         # Return the sql
         return sql, params
 }}
 
 fields、すなわち、django.db.models.fields.Fieldのdb_parametersと関連メソッド。サブクラスでオーバーライドされていることもありますが基本は同じです。
 
 #code(Python){{
     def db_parameters(self, connection):
         """
         Extension of db_type(), providing a range of different return
         values (type, checks).
         This will look at db_type(), allowing custom model fields to override it.
         """
         type_string = self.db_type(connection)
         check_string = self.db_check(connection)
         return {
             "type": type_string,
             "check": check_string,
         }
 
     def db_type(self, connection):
         """
         Return the database column data type for this field, for the provided
         connection.
         """
         data = DictWrapper(self.__dict__, connection.ops.quote_name, "qn_")
         try:
             return connection.data_types[self.get_internal_type()] % data
         except KeyError:
             return None
 
     def get_internal_type(self):
         return self.__class__.__name__
 }}
 
 connection、つまり、DatabaseWrapperクラスを確認するとフィールドに対応するデータベースでの型が記述されています。
 
 #code(Python){{
     data_types = {
         'AutoField': 'integer',
         'CharField': 'varchar(%(max_length)s)',
         'IntegerField': 'integer',
         他のマッピング...
     }
 }}
 
 CharFieldの場合、selfの__dict__(をラップしたもの)を渡すことで、「%(max_length)s」の部分が置換され、「varchar(200)」のようになります。ともかく、column_sqlメソッドが実行することで「integer NOT NULL PRIMARY KEY」のような型と制約を表すSQL文字列が取得できました。
 
 **オートインクリメントの処理(テーブル定義内) [#t70f2bda]
 
 制御がcreate_modelに戻って今度はFieldクラスのdb_type_suffixメソッドを呼んでいます。
 
 #code(Python){{
             # Autoincrement SQL (for backends with inline variant)
             col_type_suffix = field.db_type_suffix(connection=self.connection)
             if col_type_suffix:
                 definition += " %s" % col_type_suffix
 }}
 
 #code(Python){{
     def db_type_suffix(self, connection):
         return connection.data_types_suffix.get(self.get_internal_type())
 }}
 
 connection(DatabaseWrapper)のdata_types定義(SQLiteのやつ)
 
 #code(Python){{
     data_types_suffix = {
         'AutoField': 'AUTOINCREMENT',
         'BigAutoField': 'AUTOINCREMENT',
     }
 }}
 
 というわけでAUTOINCREMENTが付加されます。
 余談ですが、ループの最後にあるオートインクリメントSQLが実際追加されるのはOracleだけなようです。
 
 **外部キーの処理 [#g21ba9b5]
 
 先ほど挙げた中には外部キーの記述はなかったのですが、というかDjangoでは外部キーは別途AddFieldで追加される、かつ、SQLiteの場合はフィールド一つ追加するのに新しいテーブル作ってコピってという変なことをしているので(ALTER TABLEの制限のためらしいです)、そういう頑張っている部分を頑張って見るのはやめますが、ともかく外部キーがどう処理されているか見ておきましょう。
 
 sqlmigrateで出力すると外部キーの部分は次のようになります。
 
  "question_id" integer NOT NULL REFERENCES "polls_question" ("id")
 
 REFERENCES以下を作っているのはここ
 
 #code(Python){{
                 if self.connection.features.supports_foreign_keys:
                     self.deferred_sql.append(self._create_fk_sql(model, field, "_fk_%(to_table)s_%(to_column)s"))
                 elif self.sql_create_inline_fk:
                     definition += " " + self.sql_create_inline_fk % {
                         "to_table": self.quote_name(to_table),
                         "to_column": self.quote_name(to_column),
                     }
 }}
 
 featuresはfeaturesモジュールのDatabaseFeaturesクラスです。で、SQLiteの場合はsupports_foreign_keysはFalseになっています(先ほど書いたALTER TABLEの制限のためのようです)。その代わり、sql_create_inline_fkに値が設定されており(こちらはSchemaEditorです)REFERENCES以下が作られるようになっています。
 
 *BaseDatabaseSchemaEditor.create_model続き [#j32f3a46]
 
 こんな感じに各フィールドのSQL表現ができたらいよいよテーブルの作成です。
 
 #code(Python){{
         # Add any unique_togethers (always deferred, as some fields might be
         # created afterwards, like geometry fields with some backends)
         for fields in model._meta.unique_together:
             columns = [model._meta.get_field(field).column for field in fields]
             self.deferred_sql.append(self._create_unique_sql(model, columns))
         # Make the table
         sql = self.sql_create_table % {
             "table": self.quote_name(model._meta.db_table),
             "definition": ", ".join(column_sqls)
         }
         if model._meta.db_tablespace:
             tablespace_sql = self.connection.ops.tablespace_sql(model._meta.db_tablespace)
             if tablespace_sql:
                 sql += ' ' + tablespace_sql
         # Prevent using [] as params, in the case a literal '%' is used in the definition
         self.execute(sql, params or None)
 
         # Add any field index and index_together's (deferred as SQLite3 _remake_table needs it)
         self.deferred_sql.extend(self._model_indexes_sql(model))
 
         # Make M2M tables
         for field in model._meta.local_many_to_many:
             if field.remote_field.through._meta.auto_created:
                 self.create_model(field.remote_field.through)
 }}
 
 いろいろやっていますが、executeだけ見ておけばいいでしょう。
 
 #code(Python){{
     def execute(self, sql, params=[]):
         """
         Executes the given SQL statement, with optional parameters.
         """
         # Log the command we're running, then run it
         logger.debug("%s; (params %r)", sql, params, extra={'params': params, 'sql': sql})
         if self.collect_sql:
             # 省略
         else:
             with self.connection.cursor() as cursor:
                 cursor.execute(sql, params)
 }}
 
 connectionからcursorを取得し、そちらに処理を委譲しています。
 
 *BaseDatabaseWrapper.cursor [#ne476698]
 
 話がDatabaseWrapperにやってきました。cursorメソッドは基底クラスのBaseDatabaseWrapperに定義されています。
 
 #code(Python){{
     def cursor(self):
         """
         Creates a cursor, opening a connection if necessary.
         """
         self.validate_thread_sharing()
         if self.queries_logged:
             cursor = self.make_debug_cursor(self._cursor())
         else:
             cursor = self.make_cursor(self._cursor())
         return cursor
 }}
 
 場合分けされていますが、_cursorとmake_cursorを見ておけばいいでしょう。まず_cursor。
 
 #code(Python){{
     def _cursor(self):
         self.ensure_connection()
         with self.wrap_database_errors:
             return self.create_cursor()
 }}
 
 **connect [#q400f5c6]
 
 ensure_connectionに進む。
 
 #code(Python){{
     def ensure_connection(self):
         """
         Guarantees that a connection to the database is established.
         """
         if self.connection is None:
             with self.wrap_database_errors:
                 self.connect()
 }}
 
 しつこい(笑)。connectメソッドです。
 
 #code(Python){{
     def connect(self):
         """Connects to the database. Assumes that the connection is closed."""
         # Check for invalid configurations.
         self.check_settings()
         # In case the previous connection was closed while in an atomic block
         self.in_atomic_block = False
         self.savepoint_ids = []
         self.needs_rollback = False
         # Reset parameters defining when to close the connection
         max_age = self.settings_dict['CONN_MAX_AGE']
         self.close_at = None if max_age is None else time.time() + max_age
         self.closed_in_transaction = False
         self.errors_occurred = False
         # Establish the connection
         conn_params = self.get_connection_params()
         self.connection = self.get_new_connection(conn_params)
         self.set_autocommit(self.settings_dict['AUTOCOMMIT'])
         self.init_connection_state()
         connection_created.send(sender=self.__class__, connection=self)
 
         self.run_on_commit = []
 }}
 
 接続を行っています。connectで呼ばれているメソッドのうち、get_connection_params、get_new_connection、init_connection_stateはBaseDatabaseWrapperではNotImplementedErrorを投げるだけで個々のサブクラスで実装、実際のデータベース接続を行うようになっています。
 
 **create_cursor [#sa29827d]
 
 create_cursorメソッドもサブクラスで実装すべきメソッドです。というわけで、sqlite3のDatabaseWrapperのcreate_cursor
 
 #code(Python){{
     def create_cursor(self):
         return self.connection.cursor(factory=SQLiteCursorWrapper)
 }}
 
 ややこしいですが、このconnectionというのは先ほどのconnectメソッド中、get_new_connectionメソッドを呼び出して返されたオブジェクトです。今の場合、sqlite3.dbapi2(Databaseという名前でインポートされています)のconnect関数の戻り値のConnectionオブジェクトです。
 
 そのsqliteのConnectionオブジェクトのcursorメソッドを呼び出してカーソルを返しています。もちろんこのカーソルはsqlite3で定義されているカーソルです。
 
 **django.db.backends.utils.CursorWrapper [#zac8ccf1]
 
 さて、create_cursorメソッドで個々のDBMSのカーソルが返されました。次にそれをmake_cursorメソッドに渡しています。
 
 #code(Python){{
     def make_cursor(self, cursor):
         """
         Creates a cursor without debug logging.
         """
         return utils.CursorWrapper(cursor, self)
 }}
 
 Wrapperもう飽きたよ(笑)ってところですが、ラップされ返されています。
 
 で、そんなこんなで返されたDjangoレベルでのカーソルオブジェクトのexecuteメソッドが呼び出されます。
 
 #code(Python){{
     def execute(self, sql, params=None):
         self.db.validate_no_broken_transaction()
         with self.db.wrap_database_errors:
             if params is None:
                 return self.cursor.execute(sql)
             else:
                 return self.cursor.execute(sql, params)
 }}
 
 実際にはさらにここでSQLiteCursorWrapperのexecuteが呼ばれ、sqlite3のCursorオブジェクトのexecuteが呼ばれる、ということになりますがもういいでしょう。ともかくこのようにして作成されたSQLが実行されます。
 
 *おわりに [#aea207a0]
 
 今回はデータベースマイグレーションの様子、モデルに対するテーブル作成のSQLがどう実行されるのかを見てきました。やっていることとしては大枠・共通の処理を記述した基底クラスと、個々のDBMSに対応したサブクラスという教科書的な実装になっていました。基底クラスの処理を眺めるときもサブクラス(実際)がどうなっているのかをチェックしないといけないのが少し面倒ですが難しい処理はそんなにありません。
 

トップ   編集 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS