👻

Djangoで認証情報を持つテーブルのシリアライザの実装方法について

2021/09/26に公開

結論

Django で API を作る際の実装としてリレーションテーブルを用意した実装を基本とすると、フレームワークを十分に利用した実装ができるはず。

検証環境について

  • python 3.9.4
  • Django 3.2.5
  • djangorestframework 3.12.4

概要

Django を利用したアプリケーションを作成している際に、ついつい外部キーを含むモデルを作成してしまうことがあります。

それ自体は大きな問題にはならないのですが、外部キーが多くなってくると1つあたりのAPIで行う処理が複雑になってきます。
また、仕様変更時の工数が増えたりテーブル自体を直さなければならなくなったりと苦労することになります。

例として、以下のようなユーザを管理するUserModelと、そのユーザのプロフィールを管理するProfileModelモデルがあったとします。

class UserModel(models.Model):
    id = models.BigAutoField(primary_key=True)

class ProfileModel(models.Model):
    id = models.BigAutoField(primary_key=True)
    user = models.OneToOneField()
    age = models.IntegerField(min_value=0)

このときの ModelSerializer を使った実装として以下のような書き方ができます。

class ProfileModelSerializer(serializers.ModelSerializer):
    class Meta:
        model = ProfileModel
        fields = '__all__'
    
    def create(self, validated_data):
        validated_data_ = copy.deepcopy(validated_data)
        validated_data_.update({
            'user': self.context.get('request').user
        })
        return super().create(validated_data_)

もしくは別の方法として view で実装する方法も考えられます。

class ProfileModelSerializer(serializers.ModelSerializer):
    class Meta:
        model = ProfileModel
        fields = '__all__'


class ProfileAPI(mixins.CreateModelMixin, GenericAPIView):
    authentication_classes = [SessionAuthentication]
    permission_classes = [IsAuthenticated]
    serializer_class = ProfileModelSerializer

    def get_serializer(self, *args, **kwargs):
        self.request.data.update({
            'creator': self.context.get('request').user
        })
        return super().get_serializer(*args, **kwargs)

行っていることはどちらも同じですが、どちらも実装に対して違和感があります。

何に違和感を感じているのか考えたところ、各関数の命名と実際に行われている処理が異なっていることだと考えました。

1つめの例では create 関数内で validated_data の値を更新し、親クラスの create 関数を実行しています。
create する関数なのに、値の更新を行っていることから、親クラスが用意した関数の持つ役割を書き換えてしまっていると考えられます。

2つめの例でも同じように get_serializer 関数内で self.request.dataを更新しています。
getter 内でリクエストデータを書き換えていることで、シリアライザ以外の箇所に影響を与える可能性もあります。

どちらも関数名から推測できる処理とは別の役割をもたせることで、レコード作成時の user フィールドを追加しています。

これぐらいの処理であれば大きな問題にはならないと思いますが、一度この実装をしてしまうとテーブルのリファクタリングは難しいため回避したほうが良いでしょう。

改善方法

ProfileModel クラス内の user フィールドを取り除くことで解決できます。

user フィールドを取り除くために ProfileModel と UserModel のリレーションテーブルを用意します。

class ProfileModel(models.Model):
    id = models.BigAutoField(primary_key=True)
    age = models.IntegerField(min_value=0)


class UserProfileModel(models.Model):
    id = models.BigAutoField(primary_key=True)
    user = models.ForeignKey()
    profile = models.ForeignKey()
    
    class Meta:
        unique_together = [['user', 'profile']]
    
    @classmethod
    def create(cls, user_model, profile_model):
        return cls.objects.create(
            user=user_model,
            profile=profile_model,
        )

次に view を書き換えます。

class ProfileAPI(mixins.CreateModelMixin, GenericAPIView):
    authentication_classes = [SessionAuthentication]
    permission_classes = [IsAuthenticated]
    serializer_class = ProfileModelSerializer

    def perform_create(self, serializer):
        profile_model = serializer.save()
        UserProfileModel.create(self.request.user, profile_model)

これで関数名に合った実装になります。

また、モデルに外部キーがなくなることで修正時の影響範囲も抑えることができます。

例えば、「ユーザには複数のプロフィールを設定できる」といった仕様が追加されたとしても対応に苦労しません。

Django で API を作る際の実装としてリレーションテーブルを用意した実装を基本とすると、フレームワークを十分に利用した実装ができるはずです。

Discussion