0
0

More than 1 year has passed since last update.

【Django】ManyToManyFieldをQオブジェクトで検索する時の注意点

Posted at

目的

  • 多対多の関係にある動画モデルとタグモデルを用意した
  • 動画のタイトルと動画に紐づいているタグを複数指定して、動画を検索する機能を追加する

モデルのイメージ

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を実行するので、処理時間は遅いでしょう。
  • 大人しく生クエリ書くか・・・・

もし、もっと良い方法があればどなたか教えていただければと思います。。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0