評価値の登録と取得
モデルの定義
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;