雑食日誌

PythonとJS。ときどきチーム開発

DjangoのORMでちょっとしたSQLをかいた

この記事はDjango Advent Calendar 2018 15日目の記事です。

qiita.com

はじめに

最近色々な勉強会でORMの是非について話を聞きます。 もともと自分はDjangoで単純なSQLしか書いたことがなかったのできちんと議論できませんでした。
そこで、今回はクエリ中心にちょっとしたSQLDjangoの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

コードはリポジトリにて公開しています。

github.com

準備

テーブルはブログを題材に作りました。モデル定義は以下になります。

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みたいな更新系もやってみたいです。