View on GitHub

評価値の登録と取得

Home

評価値の登録と取得

モデルの定義

src/backend/api/online/models.py

......
# ↓追加
class UserMovieRating(models.Model):
    """
    ユーザ-映画中間モデル(評価値)

    Attributes
    ----------
    id : CharField
        ID
    user : ForeignKey[User]
        ユーザ
    movie : ForeignKey[Movie]
        映画
    rating : FloatField
        評価値
    rated_at : DateTimeField
        評価日時(自動更新)
    """
    id = models.CharField(primary_key=True, max_length=43)
    user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='user_movies')
    movie = models.ForeignKey(Movie, on_delete=models.CASCADE, related_name='movie_users')
    rating = models.FloatField(blank=False, null=False)
    rated_at = models.DateTimeField(blank=False, null=False, auto_now=True)

    class Meta:
        managed = True
        db_table = 't_users_movies_rating'
        indexes = [
            models.Index(fields=['user', 'rating', '-rated_at'], name='umr_user_rating_idx'),
            models.Index(fields=['user', 'movie'], name='umr_user_movie_idx'),
        ]

    def __str__(self):
        return f'UserMovieRating {self.id}: User {self.user.user_id} - Movie {self.movie.movie_id}: {self.rating}'
# ↑追加

マイグレーション

(recsys_full) backend$ python manage.py makemigrations online
Migrations for 'online':
  api/online/migrations/0003_usermovierating.py
    + Create model UserMovieRating

(recsys_full) backend$ python manage.py migrate
Operations to perform:
  Apply all migrations: accounts, admin, auth, contenttypes, online, sessions, token_blacklist
Running migrations:
  Applying online.0003_usermovierating... OK

テーブルの確認

recsys_full=# \dt
                            List of relations
 Schema |                    Name                     |   Type   | Owner
--------+---------------------------------------------+----------+-------
...(略)...
 public | m_genres                                    | table    | rsl
 public | m_movies                                    | table    | rsl
 public | m_movies_genres                             | table    | rsl
 public | t_users                                     | table    | rsl
 public | t_users_movies_rating                       | table    | rsl
...(略)...
(17 rows)

recsys_full=# \d t_users_movies_rating
                 Table "public.t_users_movies_rating"
  Column  |           Type           | Collation | Nullable | Default
----------+--------------------------+-----------+----------+---------
 id       | character varying(43)    |           | not null |
 rating   | double precision         |           | not null |
 rated_at | timestamp with time zone |           | not null |
 movie_id | integer                  |           | not null |
 user_id  | uuid                     |           | not null |
Indexes:
    "t_users_movies_rating_pkey" PRIMARY KEY, btree (id)
    "t_users_movies_rating_id_c0a836c7_like" btree (id varchar_pattern_ops)
    "t_users_movies_rating_movie_id_e07e182f" btree (movie_id)
    "t_users_movies_rating_user_id_4f4abd07" btree (user_id)
    "umr_user_movie_idx" btree (user_id, movie_id)
    "umr_user_rating_idx" btree (user_id, rating, rated_at DESC)
Foreign-key constraints:
    "t_users_movies_rating_movie_id_e07e182f_fk_m_movies_movie_id" FOREIGN KEY (movie_id) REFERENCES m_movies(movie_id) DEFERRABLE INITIALLY DEFERRED
    "t_users_movies_rating_user_id_4f4abd07_fk_t_users_user_id" FOREIGN KEY (user_id) REFERENCES t_users(user_id) DEFERRABLE INITIALLY DEFERRED

マッパー

src/backend/api/online/mappers.py

......
# ↓追加
class RatingMapper:
    def __init__(self, obj):
        self.obj = obj

    def as_dict(self):
        rating = self.obj
        return {
            'rating': rating.rating,
            'rated_at': rating.rated_at,
        }
# ↑追加

ビュー

src/backend/api/online/views.py

from django.shortcuts import get_object_or_404
from django.db import transaction               # <- 追加

from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework.permissions import AllowAny, IsAuthenticated  # <- IsAuthenticatedを追加

from .models import Movie, UserMovieRating      # <- UserMovieRatingを追加
from .mappers import MovieMapper, RatingMapper  # <- RatingMapperを追加


# ↓追加
def _get_user(request):
    """
    認証済みユーザを取得する。

    Attributes
    ----------
    request : HttpRequest
        リクエストオブジェクト

    Returns
    -------
    User or None
        認証されたユーザオブジェクト、または認証されていない場合は None
    """
    if getattr(request, 'user', None) and request.user.is_authenticated:
        return getattr(request.user, 'user', None)
    return None
# ↑追加
......

# ↓追加
class RatingView(APIView):
    """評価値ビュー"""
    permission_classes = [IsAuthenticated]

    def get(self, request, format=None):
        """
        対象ユーザの対象映画に対する評価値を取得する。

        Requests
        --------
        user : User
            ユーザ
        movie_id : int
            映画ID

        Response
        --------
        rating : json
            評価値オブジェクト
        """
        # ユーザ認証
        user = _get_user(request)
        if user is None:
            return Response(
                {'detail': 'Authentication required.'},
                status=status.HTTP_401_UNAUTHORIZED,
            )

        # リクエストパラメタの取得
        movie_id = request.GET.get('movie_id')

        # オブジェクトの取得
        movie = get_object_or_404(Movie, pk=movie_id)
        rating = UserMovieRating.objects.filter(user=user, movie=movie).first()

        # レスポンス
        data = {}
        if rating:
            data = {
                'rating': RatingMapper(rating).as_dict(),
            }
        else:
            data = {
                'rating': None
            }

        return Response(data, status=status.HTTP_200_OK)

    def post(self, request, format=None):
        """
        対象ユーザの対象映画に対する評価値を登録する。

        Requests
        --------
        user : User
            ユーザ
        movie_id : int
            映画ID
        rating : float
            評価値
        """
        # ユーザ認証
        user = _get_user(request)
        if user is None:
            return Response(
                {'detail': 'Authentication required.'},
                status=status.HTTP_401_UNAUTHORIZED,
            )

        # リクエストパラメタの取得
        movie_id = request.data['movie_id']
        rating = request.data['rating']

        # オブジェクトの登録
        with transaction.atomic():
            id = f'{str(user.user_id)}_{str(movie_id).zfill(6)}'
            movie = get_object_or_404(Movie, pk=movie_id)
            UserMovieRating.objects.update_or_create(
                id=id,
                defaults={'user': user, 'movie': movie, 'rating': rating},
            )

        # レスポンス
        return Response({'detail': 'Created.'}, status.HTTP_201_CREATED)
# ↑追加

URL マッピング

src/backend/api/online/urls.py

from django.urls import path
from . import views

urlpatterns = [
    path('movies/', views.MoviesView.as_view()),
    path('movies/<int:movie_id>/', views.MovieView.as_view()),
    path('ratings/', views.RatingView.as_view()),  # <- 追加
]

実行確認

サインインビューの作成の手順にしたがってサインインし、アクセストークンを取得してください。取得したトークンを用いて、バックエンドサーバが起動している状態で、下記コマンドを実行してください。

$
 curl -X POST http://localhost:8000/api/online/ratings/ \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <ACCESS_TOKEN>" \
  -d '{"movie_id": 1, "rating": 4.5}'

{"detail":"Created."}

t_users_movies_ratingテーブルに下記のように評価値が登録されました。

recsys_full=# SELECT * FROM t_users_movies_rating ORDER BY rated_at DESC;
                     id                      | rating |           rated_at            | movie_id |               user_id
---------------------------------------------+--------+-------------------------------+----------+--------------------------------------
 <USER_ID>_000001                            |    4.5 | 2026-08-30 09:49:25.829405+09 |        1 | <USER_ID>
(1 row)

先と同一のトークンを用いて、下記コマンドを実行してください。

$
 curl -X GET http://localhost:8000/api/online/ratings/?movie_id=1 \
   -H "Content-Type: application/json" \
   -H "Authorization: Bearer <ACCESS_TOKEN>" \
   -d '{"movie_id": 1}'

{"rating":{"rating":4.5,"rated_at":"2026-08-30T00:49:25.829405Z"}}

登録した評価値のデータが取得できます。

同じ映画に対して、再度、評価値を変えて登録してみると、t_users_movies_ratingテーブルの内容が更新されます。

データの登録

一旦、t_users_movies_ratingテーブルの内容を削除したうえで、既存ユーザおよび評価値データを登録しておきます。

recsys_full=# TRUNCATE t_users_movies_rating;
offline$
 psql recsys_full -U postgres -c "\copy t_users (user_id, email_encrypted, email_hash) from 'data/users.csv' with delimiter E'\t' csv header encoding 'UTF8'"
 psql recsys_full -U postgres -c "\copy t_users_movies_rating (id, user_id, movie_id, rating, rated_at) from 'data/ratings.csv' with delimiter E'\t' csv header encoding 'UTF8'"

データの確認

recsys_full=#
 SELECT * FROM t_users;
 SELECT * FROM t_users_movies_rating;