[[Djangoを読む]] #contents *はじめに [#m2539ede] ようやくマイグレーションが終わりDjangoのモデルを作成、保存できるようになりました。チュートリアルではマイグレーション後、[[「APIで遊んでみる」>https://docs.djangoproject.com/ja/1.10/intro/tutorial02/#playing-with-the-api]]と題してモデルクラスが提供するAPIがどのようなものであるか紹介しています。 >>> from polls.models import Question, Choice # Create a new Question. >>> from django.utils import timezone >>> q = Question(question_text="What's new?", pub_date=timezone.now()) # Save the object into the database. You have to call save() explicitly. >>> q.save() 少しずつ見ていきましょうということでとりあえずここまで。 念のため、Questionの定義 #code(Python){{ from django.db import models class Question(models.Model): question_text = models.CharField(max_length=200) pub_date = models.DateTimeField('date published') }} *django/db/models/base.py [#z9a85554] というわけでModelクラスを見ていきます。modelsの__init__.pyはインポートしてるだけで実体はbase.pyに書かれています。 **Model.__init__ [#d5de0184] インスタンスを作成しているのでまずはもちろん__init__メソッドを確認します。長いので、今回関係のあるところだけ抜き出し #code(Python){{ def __init__(self, *args, **kwargs): # Set up the storage for instance state self._state = ModelState() if not kwargs: # 省略 else: # Slower, kwargs-ready version. fields_iter = iter(self._meta.fields) # Now we're left with the unprocessed fields that *must* come from # keywords, or default. for field in fields_iter: is_related_object = False # Virtual field if field.attname not in kwargs and field.column is None: continue if kwargs: if isinstance(field.remote_field, ForeignObjectRel): # 省略 else: try: val = kwargs.pop(field.attname) except KeyError: # 省略 else: val = field.get_default() if is_related_object: # 省略 else: if val is not DEFERRED: setattr(self, field.attname, val) super(Model, self).__init__() }} 何をしているかというと、 +メタ情報として持っている各フィールドについて +キーワード引数から値を取得し +インスタンスの属性として設定 を行っています。普通のクラスでやるような初期化をメタ情報から名前を取ってきて設定している、ということになります。 **save [#ed7ba9ac] というわけでモデルのインスタンス化ができたので次は保存を行うsaveメソッドです。いろいろやっていますが同じく今回関係のある部分だけ抜き出し #code(Python){{ def save(self, force_insert=False, force_update=False, using=None, update_fields=None): using = using or router.db_for_write(self.__class__, instance=self) self.save_base(using=using, force_insert=force_insert, force_update=force_update, update_fields=update_fields) }} 普通に使っている分には結局この2行だけになります。usingは、routerは前回も出てきたデータベース振り分けの仕組みですが、振り分けしてなければ"default"のDBを使うということになってsave_baseメソッドに続きます。 save_baseメソッドも例によって関係のあるところだけ抜き出し #code(Python){{ def save_base(self, raw=False, force_insert=False, force_update=False, using=None, update_fields=None): cls = self.__class__ with transaction.atomic(using=using, savepoint=False): updated = self._save_table(raw, cls, force_insert, force_update, using, update_fields) # Store the database on which the object was saved self._state.db = using # Once saved, this is no longer a to-be-added instance. self._state.adding = False }} _save_tableに続きます。tableという名前が出てきたことからそろそろデータベースとやり取りする層に降りてくると思われます。 **_save_table [#p8be7177] 重要そうなのでセクションを変えて、_save_tableメソッドです。いつも通りに今回通らないところは省略 #code(Python){{ def _save_table(self, raw=False, cls=None, force_insert=False, force_update=False, using=None, update_fields=None): """ Does the heavy-lifting involved in saving. Updates or inserts the data for a single table. """ meta = cls._meta pk_val = self._get_pk_val(meta) if pk_val is None: pk_val = meta.pk.get_pk_value_on_save(self) setattr(self, meta.pk.attname, pk_val) pk_set = pk_val is not None updated = False # If possible, try an UPDATE. If that doesn't update anything, do an INSERT. if not updated: fields = meta.local_concrete_fields if not pk_set: fields = [f for f in fields if not isinstance(f, AutoField)] update_pk = bool(meta.has_auto_field and not pk_set) result = self._do_insert(cls._base_manager, using, fields, update_pk, raw) if update_pk: setattr(self, meta.pk.attname, result) return updated }} _get_pk_valメソッドを呼び出してpk、主キーを取得しています。新規レコードなので当然結果はNoneです。次に、フィールドクラスのget_pk_value_on_saveメソッドが呼ばれていますが通常はNoneが返される、というわけで_do_insertが呼ばれてINSERTが行われます(上のコードでは省略していますが、pkの値があるならUPDATEが行われるようになっています) _do_insertメソッドはシンプル、というかmanagerへの移譲が行われているだけです。 #code(Python){{ def _do_insert(self, manager, using, fields, update_pk, raw): """ Do an INSERT. If update_pk is defined then this method should return the new pk for the model. """ return manager._insert([self], fields=fields, return_id=update_pk, using=using, raw=raw) }} **ModelBase._base_manager [#ub941a8b] ところで、manager、より正確には_base_managerとは何者なのでしょうか。メタクラスのModelBaseを見てみましょう。 #code(Python){{ @property def _base_manager(cls): return cls._meta.base_manager }} _meta、optionsモジュールのOptionsクラスに続く。base_managerプロパティはいろいろやっていますが関係ないとこ省くと以下のようになります。 #code(Python){{ @cached_property def base_manager(self): manager = Manager() manager.name = '_base_manager' manager.model = self.model manager.auto_created = True return manager }} というわけで、managerモジュールのManagerクラスのインスタンスが返されます。 ところで、managerという単語は前にも出てきた気がします。具体的には、モデルのインポートを読んだ時のModelBase._prepare #code(Python){{ def _prepare(cls): # 省略 if not opts.managers or cls._requires_legacy_default_manager(): manager = Manager() manager.auto_created = True cls.add_to_class('objects', manager) }} base_managerプロパティを見たとき、「ややこしい処理には行かないはずだからこの最後に書いてあるのだろ」と思ったのですが、「いや待て、Managerインスタンスってもういるはずだよな」とこちらの処理を思い出しadd_to_class以降に進んだのですが、結局、INSERTの処理をする時はこの'objects'は使われないということがわかりました。Managerが2ついるのはどうにも気持ち悪いですが事実そうなっているようです。 *django/db/models/manager.py [#d4017ab9] さて、Managerクラスです。 #code(Python){{ class Manager(BaseManager.from_queryset(QuerySet)): pass }} ・・・。BaseManagerのfrom_querysetメソッド #code(Python){{ @classmethod def from_queryset(cls, queryset_class, class_name=None): if class_name is None: class_name = '%sFrom%s' % (cls.__name__, queryset_class.__name__) class_dict = { '_queryset_class': queryset_class, } class_dict.update(cls._get_queryset_methods(queryset_class)) return type(class_name, (cls,), class_dict) }} _get_queryset_methodsでQuerySetからメソッドを取得し、新しいクラスを作成しています。で、_get_queryset_methods #code(Python){{ @classmethod def _get_queryset_methods(cls, queryset_class): def create_method(name, method): def manager_method(self, *args, **kwargs): return getattr(self.get_queryset(), name)(*args, **kwargs) manager_method.__name__ = method.__name__ manager_method.__doc__ = method.__doc__ return manager_method new_methods = {} # Refs http://bugs.python.org/issue1785. predicate = inspect.isfunction if six.PY3 else inspect.ismethod for name, method in inspect.getmembers(queryset_class, predicate=predicate): # Only copy missing methods. if hasattr(cls, name): continue # Only copy public methods or methods with the attribute `queryset_only=False`. queryset_only = getattr(method, 'queryset_only', None) if queryset_only or (queryset_only is None and name.startswith('_')): continue # Copy the method onto the manager. new_methods[name] = create_method(name, method) return new_methods }} うーん、関数内関数内関数なんて初めて見た(笑) get_querysetメソッド。なんで毎回別のオブジェクト作ってるんだろ、と思いましたが、確かにINSERTはともかく他のだと検索情報を個別に記録しないといけませんね。 #code(Python){{ def get_queryset(self): """ Returns a new QuerySet object. Subclasses can override this method to easily customize the behavior of the Manager. """ return self._queryset_class(model=self.model, using=self._db, hints=self._hints) }} *django/db/models/query.py [#cbf73b18] さて、話を戻して、_insertメソッドです。上で見てきたように各種のメソッドはManagerに直接書かれているわけではなくqueryモジュールのQuerySetクラスに書かれています。 #code(Python){{ def _insert(self, objs, fields, return_id=False, raw=False, using=None): """ Inserts a new record for the given model. This provides an interface to the InsertQuery class and is how Model.save() is implemented. """ self._for_write = True if using is None: using = self.db query = sql.InsertQuery(self.model) query.insert_values(fields, objs, raw=raw) return query.get_compiler(using=using).execute_sql(return_id) _insert.alters_data = True _insert.queryset_only = False }} Pythonってメソッドに属性を設定できるんですね。まあ普通使うことはないと思いますが。 *django/db/models/sql [#lf888e03] sqlモジュールに移りましょう。まずは__init__。 #code(Python){{ from django.db.models.sql.datastructures import EmptyResultSet from django.db.models.sql.query import * # NOQA from django.db.models.sql.subqueries import * # NOQA from django.db.models.sql.where import AND, OR __all__ = ['Query', 'AND', 'OR', 'EmptyResultSet'] }} InsertQueryはqueryモジュールに書かれていそうです。 ・・・と思ったらsubqueriesの方に書いてありました。このsubというのは「SQLの副問い合わせ」という意味じゃなくて「Queryクラスのサブクラス」という意味のようですね。もっともinsert_valuesではSQLの構築は行われずインスタンスへの設定だけが行われるようです。 **django.db.models.sql.query.Query.get_compiler [#se5beaab] というわけでまずget_compilerから見てみましょう。 #code(Python){{ def get_compiler(self, using=None, connection=None): if using: connection = connections[using] return connection.ops.compiler(self.compiler)(self, connection, using) }} 出てきましたconnections。ここで前回も見てきた各DBMSに対応したDatabaseWrapperに処理が移ります。 といっても、ops、sqlite3の方のDatabaseOperationsインスタンスにはcompilerメソッドは定義されておらず、基底クラスのBaseDatabaseOperationsに定義されています。 #code(Python){{ compiler_module = "django.db.models.sql.compiler" def compiler(self, compiler_name): """ Returns the SQLCompiler class corresponding to the given name, in the namespace corresponding to the `compiler_module` attribute on this backend. """ if self._cache is None: self._cache = import_module(self.compiler_module) return getattr(self._cache, compiler_name) }} compiler_name、その元Queryインスタンスのcompiler、はInsertQueryの場合、SQLInsertCompilerになります。つまり、django.db.models.sql.compiler.SQLInsertCompilerが返されることになります。 **django.db.models.sql.compiler.SQLInsertCompiler.execute_sql [#lcf61fb3] 残りはexecute_sqlメソッドです。 #code(Python){{ def execute_sql(self, return_id=False): self.return_id = return_id with self.connection.cursor() as cursor: for sql, params in self.as_sql(): cursor.execute(sql, params) if self.connection.features.can_return_ids_from_bulk_insert and len(self.query.objs) > 1: return self.connection.ops.fetch_returned_insert_ids(cursor) if self.connection.features.can_return_id_from_insert: assert len(self.query.objs) == 1 return self.connection.ops.fetch_returned_insert_id(cursor) return self.connection.ops.last_insert_id( cursor, self.query.get_meta().db_table, self.query.get_meta().pk.column ) }} 先に言っておくと、sqlite3の場合、can_return系のfeaturesはTrueにはならず最後のlast_insert_idの方法でINSERTされたIDが返されるようです。 ***as_sql [#xaf436f3] as_sqlメソッドではDBMSのfeaturesも考慮してINSERT文が構築されます。一部省略して載せると、 #code(Python){{ def as_sql(self): # We don't need quote_name_unless_alias() here, since these are all # going to be column names (so we can avoid the extra overhead). qn = self.connection.ops.quote_name opts = self.query.get_meta() result = ['INSERT INTO %s' % qn(opts.db_table)] has_fields = bool(self.query.fields) fields = self.query.fields if has_fields else [opts.pk] result.append('(%s)' % ', '.join(qn(f.column) for f in fields)) if has_fields: value_rows = [ [self.prepare_value(field, self.pre_save_val(field, obj)) for field in fields] for obj in self.query.objs ] else: # 省略 # Currently the backends just accept values when generating bulk # queries and generate their own placeholders. Doing that isn't # necessary and it should be possible to use placeholders and # expressions in bulk inserts too. can_bulk = (not self.return_id and self.connection.features.has_bulk_insert) placeholder_rows, param_rows = self.assemble_as_sql(fields, value_rows) if can_bulk: # 省略 else: return [ (" ".join(result + ["VALUES (%s)" % ", ".join(p)]), vals) for p, vals in zip(placeholder_rows, param_rows) ] }} pre_save_valメソッドはコメントにあるようにFieldクラスを呼び出して時間を「保存した時」にするなどの処理を行っていますが通常は単にgetattrで値を取得しているだけです。 #code(Python){{ def pre_save_val(self, field, obj): """ Get the given field's value off the given obj. pre_save() is used for things like auto_now on DateTimeField. Skip it if this is a raw query. """ if self.query.raw: return getattr(obj, field.attname) return field.pre_save(obj, add=True) }} prepare_value、assemble_as_sqlについて見ていきます。 ***prepare_value [#k63fa07e] prepare_valueメソッド。resolve_expressionは設定されていないはずなのでFieldクラスのget_db_prep_saveメソッドが実行されます。 #code(Python){{ def prepare_value(self, field, value): """ Prepare a value to be used in a query by resolving it if it is an expression and otherwise calling the field's get_db_prep_save(). """ if hasattr(value, 'resolve_expression'): # 省略 else: value = field.get_db_prep_save(value, connection=self.connection) return value }} Fieldのget_db_prep_saveメソッド。は、get_db_prep_valueメソッドを呼んでるだけです。さらにget_db_prep_valueではget_prep_valueメソッドを呼び出しています。 #code(Python){{ def get_db_prep_save(self, value, connection): """ Returns field's value prepared for saving into a database. """ return self.get_db_prep_value(value, connection=connection, prepared=False) def get_db_prep_value(self, value, connection, prepared=False): """Returns field's value prepared for interacting with the database backend. Used by the default implementations of get_db_prep_save(). """ if not prepared: value = self.get_prep_value(value) return value }} get_prep_valueメソッドはFieldクラスの各サブクラスでオーバーライドされているようです。以下はDateTimeFieldのget_prep_value。to_pythonメソッドは省略しますがvalueをdatetimeオブジェクトにする処理をしています。 #code(Python){{ def get_prep_value(self, value): value = super(DateTimeField, self).get_prep_value(value) value = self.to_python(value) if value is not None and settings.USE_TZ and timezone.is_naive(value): # 省略 return value }} ***assemble_as_sql [#b25aee66] SQLInsertCompilerクラスに戻って、assemble_as_sqlメソッドです。 #code(Python){{ def assemble_as_sql(self, fields, value_rows): """ Take a sequence of N fields and a sequence of M rows of values, generate placeholder SQL and parameters for each field and value, and return a pair containing: * a sequence of M rows of N SQL placeholder strings, and * a sequence of M rows of corresponding parameter values. Each placeholder string may contain any number of '%s' interpolation strings, and each parameter row will contain exactly as many params as the total number of '%s's in the corresponding placeholder row. """ if not value_rows: return [], [] # list of (sql, [params]) tuples for each object to be saved # Shape: [n_objs][n_fields][2] rows_of_fields_as_sql = ( (self.field_as_sql(field, v) for field, v in zip(fields, row)) for row in value_rows ) # tuple like ([sqls], [[params]s]) for each object to be saved # Shape: [n_objs][2][n_fields] sql_and_param_pair_rows = (zip(*row) for row in rows_of_fields_as_sql) # Extract separate lists for placeholders and params. # Each of these has shape [n_objs][n_fields] placeholder_rows, param_rows = zip(*sql_and_param_pair_rows) # Params for each field are still lists, and need to be flattened. param_rows = [[p for ps in row for p in ps] for row in param_rows] return placeholder_rows, param_rows }} 見た感じ、何回も形式変換を行っています。とりあえず、field_as_sqlを見てみましょう。 #code(Python){{ def field_as_sql(self, field, val): """ Take a field and a value intended to be saved on that field, and return placeholder SQL and accompanying params. Checks for raw values, expressions and fields with get_placeholder() defined in that order. When field is None, the value is considered raw and is used as the placeholder, with no corresponding parameters returned. """ if field is None: # 省略 elif hasattr(val, 'as_sql'): # 省略 elif hasattr(field, 'get_placeholder'): # 省略 else: # Return the common case for the placeholder sql, params = '%s', [val] return sql, params }} というわけでまずrows_of_fields_as_sqlは、 ('%s', [挿入する値]) のタプルのリスト(正確にはジェネレータ)になります。さらにそれがオブジェクト分(今回は1個)のジェネレータになっています。 sql_and_param_pair_rowsは各オブジェクト(テーブルの一行)の(フィールド, 値)タプルのzipオブジェクトのジェネレータです。 さらにzipを通すことで、placeholder_rowsとparam_rowsに分離しています。すなわち、それぞれ以下のような形状をしています。 placeholder_rows (('%s', '%s'),) param_rows (([値], [値]),) うーん、だんだんよくわからなくなってきた(上の形状は確認として打ってみた結果です)。最後に、param_rowsで値が1要素のリストになっているのをflattenしています。結果、 param_rows [[値, 値]] というリストに変換されます。このようにして返されたフィールドと値のうち、フィールドはSQLの構築に利用、値は最終的にカーソルオブジェクトに渡されてデータベースに送られることになります。 *おわりに [#zcdd226f] 今回はモデル処理の手始めとしてモデルの保存について見てきました。Managerオブジェクトが登場し、QuerySetと絡んで複雑な処理が行われていました。INSERTの場合は個別のQuerySetは要らないので冗長に感じますが今後なぜこのような構造が必要なのかが出てくると思われます。 また、SQLを組み立てるのにCompilerというオブジェクトが出てきました。コンパイラと言ってもSQLを受け取って構文解析するコンパイラではなく、SQLを組み立てるコンパイラです。これもINSERTだと冗長な感じでしたが複雑な検索処理が設定された場合に力を発揮するのでしょう。 というわけで引き続き、「APIで遊んでみる」を見ていきましょう。