目的
- 多対多の関係にある動画モデルとタグモデルを用意した
- 動画のタイトルと動画に紐づいているタグを複数指定して、動画を検索する機能を追加する
モデルのイメージ
from django.db import models
class Movie(models.Model):
"""動画"""
title = models.CharField()
tag = models.ManyToManyField(Tags)
class Tag(models.Model):
"""タグ"""
name = models.CharField()
- 動画テーブル(movie)
ID | タイトル |
---|---|
1 | A動画 |
2 | B動画 |
- 中間テーブル(movie_tag)
ID | 動画ID | タグID |
---|---|---|
1 | 1 | 1 |
2 | 1 | 2 |
3 | 1 | 3 |
4 | 2 | 2 |
5 | 2 | 4 |
- タグテーブル(tags)
ID | タグ名 |
---|---|
1 | ホラー |
2 | 恋愛 |
3 | SF |
4 | 歴史 |
失敗したコード
- ネットで調査しつつ以下の実装を行った
失敗コード
class MovieSearch(generics.ListAPIView):
def get(self, request):
# キーワード抽出
params = request.GET.get('keyword')
# 全角スペースを半角スペースに変換
# 半角スペースで分割
keywords = keyword_serializer.validated_data.get('keyword').replace(' ', ' ').split(' ')
# Qオブジェクトを生成
query = Q()
# タイトルとタグをキーワードで絞り込む
for keyword in keywords:
query &= (Q(title__icontains=keyword) | Q(tag__name__icontains=keyword))
# 対象の動画一覧取得
queryset = Movie.object.filter(query)
- キーワードに
ホラー
と恋愛
を指定して、動画A
が取得できることを期待した - しかし、結果は0件で何も取得できなかった
原因調査
- filterの後ろに
query
とつけることで、生クエリを確認することができる -
print(Movie.object.filter(query).query)
をコードに定義し、生クエリを確認した
SELECT /*長いので省略*/ FROM movie LEFT OUTER JOIN movie_tag ON (movie.uuid = movie_tag.movie_id) LEFT OUTER JOIN tags ON (movie_tag.tags_id = tags.id) WHERE (movie.title LIKE %ホラー% OR tags.name LIKE %ホラー%) AND (movie.title LIKE %恋愛% OR tags.name LIKE %恋愛%))
-
中間テーブルとタグテーブルを
LEFT OUTER JOIN
しており、結合されたテーブルを(タイトル | タグ)
のAND条件で検索していた -
結合テーブルのイメージ
動画ID | タイトル | 中間テーブルID | タグID | タグ名 |
---|---|---|---|---|
1 | A動画 | 1 | 1 | ホラー |
1 | A動画 | 2 | 2 | 恋愛 |
1 | A動画 | 3 | 3 | SF |
2 | B動画 | 4 | 2 | 恋愛 |
2 | B動画 | 5 | 4 | 歴史 |
-
ホラー
と恋愛
をもつ1レコードなど存在しないため、結果が0件になっていた
修正したコード
- 自力で色々試して、とりあえずやりたいことは以下のコードで実現できた
成功コード
class MovieSearch(generics.ListAPIView):
def get(self, request):
# キーワード抽出
params = request.GET.get('keyword')
# 全角スペースを半角スペースに変換
# 半角スペースで分割
keywords = keyword_serializer.validated_data.get('keyword').replace(' ', ' ').split(' ')
# 対象の動画一覧取得
queryset = Movie.object.all()
# タイトルとタグをキーワードで絞り込む
for keyword in keywords:
queryset = queryset.filter(Q(title__icontains=keyword) | Q(tag__name__icontains=keyword))
# 重複レコードを削除
queryset = queryset.distinct()
-
ホラー
と恋愛
で検索すると、1回目は結合テーブルをホラー
で、2回目は1回目の検索結果を元に結合テーブルを作成しホラー
と恋愛
で検索するため、重複レコードが発生する - そのため、
distinct()
で重複レコードを作成する処理を追加した
懸念
- 毎回filterを実行するので、処理時間は遅いでしょう。
- 大人しく生クエリ書くか・・・・
もし、もっと良い方法があればどなたか教えていただければと思います。。