目的
- 多対多の関係にある動画モデルとタグモデルを用意した
- 動画のタイトルと動画に紐づいているタグを複数指定して、動画を検索する機能を追加する
モデルのイメージ
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を実行するので、処理時間は遅いでしょう。
- 大人しく生クエリ書くか・・・・
もし、もっと良い方法があればどなたか教えていただければと思います。。