DjangoのORMでちょっとしたSQLをかいた
この記事はDjango Advent Calendar 2018 15日目の記事です。
はじめに
最近色々な勉強会でORMの是非について話を聞きます。
もともと自分はDjangoで単純なSQLしか書いたことがなかったのできちんと議論できませんでした。
そこで、今回はクエリ中心にちょっとしたSQLをDjangoのORMで実装してみます。
動作環境
Dockerについて
SQLを試してみるにあたって、ローカルのPCのデータベースをセットアップすることは面倒です。
そのため、データベースやPythonのランタイムはDocker Composeを使ってセットアップしています。
実際のDockerfileは以下
FROM python:3.7 ENV PYTHONUNBUFFERED 1 ENV DOCKERIZE_VERSION v0.6.1 RUN wget https://github.com/jwilder/dockerize/releases/download/$DOCKERIZE_VERSION/dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz && \ tar -C /usr/local/bin -xzvf dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz && \ rm dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz WORKDIR /code/ COPY Pipfile* ./ RUN pip install pipenv && \ pipenv install --system --deploy ADD . /code
Docker Compose でマイグレーションと初期データの設定を行なっています。
version: "3" services: db: image: postgres:10 environment: POSTGRES_DB: $POSTGRES_DB POSTGRES_PASSWORD: $POSTGRES_PASSWORD env_file: - .env backend: build: . command: bash -c "dockerize -wait tcp://db:5432 && pipenv install --system --dev && python3 manage.py migrate && python3 manage.py loaddata init.json && python3 manage.py runserver 0.0.0.0:8000" volumes: - .:/code ports: - "8000:8000" working_dir: "/code/orm" env_file: - .env depends_on: - db
コードはリポジトリにて公開しています。
準備
テーブルはブログを題材に作りました。モデル定義は以下になります。
from django.db import models class Post(models.Model): title = models.CharField(max_length=255) text = models.TextField() class Comment(models.Model): post = models.ForeignKey('Post', on_delete=models.CASCADE) text = models.TextField(blank=True, null=True)
ダミーデータはフィクスチャで準備し、各検証はDjango Extensions を使いました。
試すクエリ
以下の5つのクエリを試してみたいと思います。
- 1テーブルから全データを取得
- 外部キー含めたデータを取得
- カラムに条件指定して取得する
- Case式 を使った分岐
- Countによる集計
1. テーブルから全データを取得
Postテーブルから記事データを全て取得してみます。
DjangoのORMでは all
メソッドで全データを取得することができます。
>> posts = Post.objects.all() >> posts.values() <QuerySet [{'id': 1, 'title': 'PyConに行ってきた', 'text': '楽しかった'}, {'id': 2, 'title': 'Djangoチュートリアル', 'text': '投票アプリの構築までできた'}]> >> print(posts.query) SELECT "blog_post"."id", "blog_post"."title", "blog_post"."text" FROM "blog_post"
2. 外部キー含めたデータを取得
Commentと外部キーであるPostを併せて取得してみます。
>> comments = Comment.objects.select_related().all() >> comments.values() <QuerySet [{'id': 1, 'post_id': 1, 'text': '一番よかったセッションは?'}, {'id': 2, 'post_id': 1, 'text': '懇親会はどうだった?'}, {'id': 3, 'post_id': 2, 'text': '自分は難しくてできなかった'}]> >> [comment.post.title for comment in comments] ['PyConに行ってきた', 'PyConに行ってきた', 'Djangoチュートリアル'] >> print(comments.query) SELECT "blog_comment"."id", "blog_comment"."post_id", "blog_comment"."text", "blog_post"."id", "blog_post"."title", "blog_post"."text" FROM "blog_comment" INNER JOIN "blog_post" ON ("blog_comment"."post_id" = "blog_post"."id")
3. カラムに条件指定して取得する
特定の条件下のデータを取得してみます。
>> pycon_comment = Comment.objects.filter(post__id=1) >> pycon_comment <QuerySet [<Comment: Comment object (1)>, <Comment: Comment object (2)>]> >> print(pycon_comment.query) SELECT "blog_comment"."id", "blog_comment"."post_id", "blog_comment"."text" FROM "blog_comment" WHERE "blog_comment"."post_id" = 1
4. CASE式を使った分岐
SQLのCASE-WHENを使った例です。
Postにを新しくジャンルというカラムを作って分類してみます。
>> genre_post = Post.objects.annotate( ...: genre=Case( ...: When(title__contains='Django', then=Value('Django')), ...: default=Value('Python'), ...: output_field=CharField()) ...: ).values_list('title', 'genre') >> genre_post <QuerySet [('PyConに行ってきた', 'Python'), ('Djangoチュートリアル', 'Django')]> >> print(genre_post.query) SELECT "blog_post"."title", CASE WHEN "blog_post"."title"::text LIKE %Django% THEN Django ELSE Python END AS "genre" FROM "blog_post"
5. Countによる集計
記事についているコメント数を集計してみます。
aggregate
メソッドは dict
を返します。
>> Comment.objects.aggregate( ...: pycon=Count('pk', filter=Q(post__id=1)), ...: django=Count('pk', filter =Q(post__id=2)) ...: ) {'pycon': 2, 'django': 1}
まとめ
Case式やCountのオブジェクトが用意されているのは初めて知りました。
Upsertみたいな更新系もやってみたいです。