OSSを読んで学ぶ!Django ORMを徹底解剖
この記事の概要
本記事では、Django ORMの仕組みへの理解を実際にOSSの内部実装を見ながら深めていきます。
対象読者
- Djangoを始めたてでORMにまだ慣れていないエンジニア
- フレームワークのOSSを読んでみたい人
Django ORMとは何か
DjangoのORMは、Pythonのクラスとデータベーステーブルを1対1でマッピングする仕組みです。他のORMと同じようにSQLを直接書かずに、データベースのCRUDが記述できます。
例として以下のようなテーブルを考えます。
User
| 項目 | カラム名 | データ型 | 主キー(PK) |
|---|---|---|---|
| ユーザーID | id | UUID | ✅ |
| 名前 | name | VARCHAR(255) | |
| メールアドレス | VARCHAR(255) | ||
| パスワード | password | VARCHAR(255) |
テーブルに対応するクラスは「モデル」と呼ばれ、上記のUserテーブルに対しては以下のように記述できます。
class User(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
name = models.CharField(max_length=255)
email = models.EmailField(unique=True)
password = models.CharField(max_length=255)
def __str__(self):
return self.name
そして、Userを全件取得するクエリは以下のように書くことができます。
# "select * from users;"に対応
User.objects.all()
特定のEメールを持つユーザーのみを取り出したい時は、
User.objects.filter(email='foo@example.com')
# 単一取得の場合は, User.objects.get(email='foo@example.com')
このような表現になります。
同様に、INSERT, DELETEに対応する表現はそれぞれ
User.objects.create(...)
User.objects.filter(email='foo@example.com').update(password='newpass123')
User.objects.filter(email='foo@example.com').delete()
となります。
このような一見シンプルで抽象化された呼び出しの裏では、Manager, QuerySet, Modelといった複数のクラス・概念が密接に連携しています。
ModelとManager:ORMの中核となる2つの概念
では、ここからソースコードを元に解説をしていきます。
対象リポジトリは以下になります。必要に応じてforkして読んでみてください。
Django ORMの仕組みを理解する上で重要になるクラスが2つあります。
1つ目はModelBaseクラス(/django/db/models/base.py)、
2つ目はManagerクラス(/django/db/models/manager.py)です。
ModelBaseクラス:ORMの「自動生成装置」
データベーステーブルに対応するModelクラスは、ModelBaseというmetaclassによって生成されます。
class Model(AltersData, metaclass=ModelBase):
...
ModelBaseクラスの役割は、metaclassとして設定しているクラス(ここではModelクラス)定義時に自動的な前処理を行うこと。その中で特に重要なのが次の部分です。
# /django/db/models/base.pyより抜粋
class ModelBase:
...
def _prepare(cls):
...
if not opts.managers:
if any(f.name == "objects" for f in opts.fields):
raise ValueError(
"Model %s must specify a custom Manager, because it has a "
"field named 'objects'." % cls.__name__
)
# Managerクラスを初期化
manager = Manager()
manager.auto_created = True
# "cls"は"Model"クラスのこと
cls.add_to_class("objects", manager)
ここでDjangoはすべてのModelクラスに対して、objectsという名前のManagerを自動付与します。そうです。User.objectsのobjectsがまさにManagerクラスのインスタンスに相当しているのです。
Managerクラス:ORMの「フロントエンド」
では、Managerとはそもそも何でしょうか?
Djangoのドキュメントを参照すると、
Managerは、Djangoモデルに対してデータベースクエリ操作を提供するインターフェースです。>Djangoアプリケーション内の各モデルには、少なくとも1つのManagerが存在します。
デフォルトでは、Djangoはオブジェクトという名前のManagerをすべてのModelクラスに付与します。
ManagerはPythonのクラスとデータベースを仲介するインターフェイスです。
User.objects.all()やUser.objects.create()といった操作を受け取り、内部的にはQuerySetクラスを呼び出しています。
# /django/db/models/manager.pyより抜粋
class Manager(BaseManager.from_queryset(QuerySet)):
pass
from_querysetメソッドでは、QuerySetのメソッド群をManagerに動的にコピーします。
class BaseManager:
...
@classmethod
def from_queryset(cls, queryset_class, class_name=None):
# type()でクラスを動的に構築して返している.
return type(
class_name or f"{cls.__name__}From{queryset_class.__name__}",
(cls,),
{
"_queryset_class": queryset_class,
# _get_queryset_methodsでQuerySetが持つメソッドをコピー.
**cls._get_queryset_methods(queryset_class),
},
)
これによって、Manager経由でQuerySetのメソッド(filter, Create, update,...)を呼べるようになるのです。
Model.objects.all()の内部処理(SELECT句)
User.objects.all()の呼び出しから、実際にSQLが発行されるまでを見てみましょう。
-
User.objects=> Managerインスタンスが返却される。 -
.all()=> Managerがget_queryset()を呼び、QuerySetインスタンスを生成し、QuerySetインスタンスのallメソッドが実行される(この時点ではまだSQLは発行しない。遅延評価) -
QuerySetの評価 => Python側で値を参照(forループやlist()化)した時点でSQLが実行される。
# /django/db/models/query.pyより抜粋
class QuerySet:
def all(self):
"""
QuerySetインスタンスのコピーを返却.
"""
return self._chain()
QuerySetオブジェクトなどのPythonオブジェクトを、ループやリストにする際に、通常__iter()__メソッドが呼ばれます。
実際にQuerySetクラスの__iter__メソッドをたどっていくと、最終的に_fetch_all()メソッド内でModelIterableクラスのイテレーターが発行されていることがわかります。
同様に、以下に示すModelIterable.__iter__()メソッドが実行されます。
class ModelIterable(BaseIterable):
def __iter__(self):
queryset = self.queryset
db = queryset.db
# 1. "コンパイラ" (= SQL生成器) を取得
compiler = queryset.query.get_compiler(using=db)
fetch_mode = queryset._fetch_mode
# 2. sqlを実行して結果を取得
results = compiler.execute_sql(
chunked_fetch=self.chunked_fetch, chunk_size=self.chunk_size
)
...
# 3. 各行をモデルインスタンスに変換
for row in compiler.results_iter(results):
obj = model_cls.from_db(
db,
init_list,
row[model_fields_start:model_fields_end],
fetch_mode=fetch_mode,
)
if fetch_mode.track_peers:
peers.append(weak_ref(obj))
obj._state.peers = peers
# 関連オブジェクトの処理
for rel_populator in related_populators:
rel_populator.populate(row, obj)
if annotation_col_map:
for attr_name, col_pos in annotation_col_map.items():
setattr(obj, attr_name, row[col_pos])
for field, rel_objs, rel_getter in known_related_objects:
# Avoid overwriting objects loaded by, e.g., select_related().
if field.is_cached(obj):
continue
rel_obj_id = rel_getter(obj)
try:
rel_obj = rel_objs[rel_obj_id]
except KeyError:
pass # May happen in qs1 | qs2 scenarios.
else:
setattr(obj, field.name, rel_obj)
yield obj # モデルインスタンスを返却
最終的にこの部分でSQLが発行されていることがわかりました!
このように、Django ORMはforループやlistなどのイテレーターに変換するときにクエリを発行しているんですね。
Model.objects.create()の内部処理(INSERT句)
今度はINSERTに対応する場合はどうでしょうか?
QuerySetクラスのcreateメソッドを見ると以下のようなフローで処理が進むことがわかります。
# /django/db/models/query.py
Class QuerySet(AltersData):
...
def create(self, **kwargs):
obj = self.model(**kwargs)
self._for_write = True
obj.save(force_insert=True, using=self.db)
return obj
objというのは対象となるModelクラスのことですね。つまり、Modelクラスに定義されているsaveメソッドが実行されることになります。
save()の内部では、save_base() => 'save_table()' => _do_insert()と呼び出していき、最終的にmanagerというオブジェクトの_insert()メソッドが実行されます。ここで、ManagerインスタンスにQuerySetクラスのメソッドをコピーしたことを思い出しましょう。そうです。_insert()メソッドはQuerySetクラスのメソッドです。そして、以下のメソッド内でレコードがDBに挿入されています。
# /django/db/models/query.pyより抜粋
class QuerySet(AltersData):
def _insert(
self,
# <細かい引数>
):
...
# 以下でDBにレコードを挿入している.
query = sql.InsertQuery(
self.model,
on_conflict=on_conflict,
update_fields=update_fields,
unique_fields=unique_fields,
)
query.insert_values(fields, objs, raw=raw)
return query.get_compiler(using=using).execute_sql(returning_fields)
まとめ
今回はDjango ORMにおいてクエリが発行されるまでの仕組みをOSSを読みながら解説しました。
Djangoは単にSQLを隠す仕組みではなく、遅延評価やクエリセットのメソッドチェーン、Fオブジェクトなど、様々な機能が提供されているORMです。
他にもupdateやdelete, where句に相当する処理もあるので、気になった人はぜひ読んでみてはいかがでしょうか?
Discussion