├── .gitignore ├── LICENSE ├── api_1 ├── __init__.py ├── serializers.py ├── urls.py └── views.py ├── api_2 ├── __init__.py ├── serializers.py ├── urls.py └── views.py ├── api_3 ├── __init__.py ├── serializers.py ├── urls.py └── views.py ├── carmaker ├── __init__.py ├── apps.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_load_mock_data.py │ ├── 0003_alter_vehiclemodel_project.py │ ├── 0004_alter_project_code_name.py │ ├── 0005_alter_vehiclemodel_maker_alter_vehiclemodel_project.py │ └── __init__.py └── models.py ├── deep_dive_drf_model_serializer_relations ├── __init__.py ├── asgi.py ├── settings.py ├── urls.py └── wsgi.py ├── docs └── carmaker_erd.png ├── manage.py └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | db.sqlite3 2 | .idea/ 3 | .DS_Store 4 | __pycache__ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Oscar Y Chen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /api_1/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Basic API setup using Django REST Framework out-of-box functionalities 3 | """ 4 | -------------------------------------------------------------------------------- /api_1/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework.serializers import ModelSerializer 2 | 3 | from carmaker.models import VehicleModel 4 | 5 | 6 | class VehicleModelSerializer(ModelSerializer): 7 | class Meta: 8 | model = VehicleModel 9 | fields = "__all__" 10 | -------------------------------------------------------------------------------- /api_1/urls.py: -------------------------------------------------------------------------------- 1 | from . import views 2 | from django.urls import path 3 | 4 | urlpatterns = [ 5 | path("", views.VehicleModelListCreateView.as_view()), 6 | path("/", views.VehicleModelRetrieveUpdateDestroyView.as_view()) 7 | ] 8 | -------------------------------------------------------------------------------- /api_1/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework.generics import ListCreateAPIView, RetrieveUpdateDestroyAPIView 2 | from rest_framework.permissions import AllowAny 3 | from api_1.serializers import VehicleModelSerializer 4 | from carmaker.models import VehicleModel 5 | 6 | 7 | class VehicleModelListCreateView(ListCreateAPIView): 8 | permission_classes = [AllowAny] 9 | queryset = VehicleModel.objects.all() 10 | serializer_class = VehicleModelSerializer 11 | 12 | 13 | class VehicleModelRetrieveUpdateDestroyView(RetrieveUpdateDestroyAPIView): 14 | permission_classes = [AllowAny] 15 | queryset = VehicleModel.objects.all() 16 | serializer_class = VehicleModelSerializer 17 | -------------------------------------------------------------------------------- /api_2/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oscarychen/deep-dive-drf-model-serializer-relations/d5d7588b3723834f25e54be5232621d771e48619/api_2/__init__.py -------------------------------------------------------------------------------- /api_2/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework.serializers import ModelSerializer 2 | from django.db.models import Q 3 | from carmaker.models import VehicleModel, Project, Manufacturer, Engine, Vehicle, Engineer 4 | 5 | 6 | class ManufacturerSerializer(ModelSerializer): 7 | class Meta: 8 | model = Manufacturer 9 | fields = "__all__" 10 | 11 | 12 | class EngineSerializer(ModelSerializer): 13 | class Meta: 14 | model = Engine 15 | fields = "__all__" 16 | 17 | 18 | class ProjectSerializer(ModelSerializer): 19 | class Meta: 20 | model = Project 21 | fields = "__all__" 22 | 23 | 24 | class VehicleSerializer(ModelSerializer): 25 | class Meta: 26 | model = Vehicle 27 | fields = "__all__" 28 | 29 | 30 | class EngineerSerializer(ModelSerializer): 31 | class Meta: 32 | model = Engineer 33 | fields = ("id", "name",) 34 | 35 | 36 | class VehicleModelSerializer(ModelSerializer): 37 | project = ProjectSerializer() 38 | maker = ManufacturerSerializer() 39 | engine_options = EngineSerializer(many=True) 40 | vehicle_set = VehicleSerializer(many=True, read_only=True) 41 | engineers_responsible = EngineerSerializer(many=True, source="engineer_set") 42 | 43 | def to_internal_value(self, data): 44 | 45 | new_data = super().to_internal_value(data) 46 | 47 | new_data["maker"] = Manufacturer.objects.get(**new_data["maker"]) 48 | 49 | engine_options_q = Q() 50 | for engine in new_data["engine_options"]: 51 | engine_options_q |= Q(**engine) 52 | new_data["engine_options"] = Engine.objects.filter(engine_options_q) 53 | 54 | engineer_set_q = Q() 55 | for engineer in new_data["engineer_set"]: 56 | engineer_set_q |= Q(**engineer) 57 | new_data["engineer_set"] = Engineer.objects.filter(engineer_set_q) 58 | 59 | return new_data 60 | 61 | def update(self, instance, validated_data): 62 | project_data = validated_data.pop("project", None) 63 | if project_data is not None and instance.project.code_name != project_data["code_name"]: 64 | instance.project.delete() 65 | validated_data["project"] = Project.objects.create(**project_data) 66 | return super().update(instance, validated_data) 67 | 68 | def create(self, validated_data): 69 | validated_data["project"] = Project.objects.create(**validated_data.pop("project")) 70 | instance = super().create(validated_data) 71 | return instance 72 | 73 | class Meta: 74 | model = VehicleModel 75 | fields = "__all__" 76 | -------------------------------------------------------------------------------- /api_2/urls.py: -------------------------------------------------------------------------------- 1 | from . import views 2 | from django.urls import path 3 | 4 | urlpatterns = [ 5 | path("", views.VehicleModelListCreateView.as_view()), 6 | path("/", views.VehicleModelRetrieveUpdateDestroyView.as_view()) 7 | ] 8 | -------------------------------------------------------------------------------- /api_2/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework.generics import ListCreateAPIView, RetrieveUpdateDestroyAPIView 2 | from rest_framework.permissions import AllowAny 3 | from api_2.serializers import VehicleModelSerializer 4 | from carmaker.models import VehicleModel 5 | 6 | 7 | class VehicleModelListCreateView(ListCreateAPIView): 8 | permission_classes = [AllowAny] 9 | queryset = VehicleModel.objects.all() 10 | serializer_class = VehicleModelSerializer 11 | 12 | 13 | class VehicleModelRetrieveUpdateDestroyView(RetrieveUpdateDestroyAPIView): 14 | permission_classes = [AllowAny] 15 | queryset = VehicleModel.objects.all() 16 | serializer_class = VehicleModelSerializer 17 | -------------------------------------------------------------------------------- /api_3/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oscarychen/deep-dive-drf-model-serializer-relations/d5d7588b3723834f25e54be5232621d771e48619/api_3/__init__.py -------------------------------------------------------------------------------- /api_3/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework.serializers import ModelSerializer 2 | from rest_framework.serializers import RelatedField 3 | from carmaker.models import VehicleModel, Project, Manufacturer 4 | 5 | 6 | class ProjectCodeNameField(RelatedField): 7 | queryset = Project.objects.all() 8 | 9 | def to_internal_value(self, data): 10 | return data 11 | 12 | def to_representation(self, value): 13 | return getattr(value, "code_name") 14 | 15 | 16 | class ManufacturerNameField(RelatedField): 17 | queryset = Manufacturer.objects.all() 18 | 19 | def to_internal_value(self, data): 20 | return data 21 | 22 | def to_representation(self, value): 23 | return getattr(value, "name") 24 | 25 | 26 | class VehicleModelSerializer(ModelSerializer): 27 | maker = ManufacturerNameField() 28 | project_code_name = ProjectCodeNameField(source="project") 29 | 30 | def to_internal_value(self, data): 31 | new_data = super().to_internal_value(data) 32 | new_data["maker"] = Manufacturer.objects.get(name=new_data["maker"]) 33 | return new_data 34 | 35 | def create(self, validated_data): 36 | if validated_data.get("project"): 37 | validated_data["project"] = Project.objects.create(code_name=validated_data["project"]) 38 | return super().create(validated_data) 39 | 40 | def update(self, instance, validated_data): 41 | project_name = validated_data.pop("project", None) 42 | if project_name is not None and instance.project.code_name != project_name: 43 | instance.project.delete() 44 | validated_data["project"] = Project.objects.create(code_name=project_name) 45 | return super().update(instance, validated_data) 46 | 47 | class Meta: 48 | model = VehicleModel 49 | fields = "__all__" 50 | -------------------------------------------------------------------------------- /api_3/urls.py: -------------------------------------------------------------------------------- 1 | from . import views 2 | from django.urls import path 3 | 4 | urlpatterns = [ 5 | path("", views.VehicleModelListCreateView.as_view()), 6 | path("/", views.VehicleModelRetrieveUpdateDestroyView.as_view()) 7 | ] 8 | -------------------------------------------------------------------------------- /api_3/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework.generics import ListCreateAPIView, RetrieveUpdateDestroyAPIView 2 | from rest_framework.permissions import AllowAny 3 | from api_3.serializers import VehicleModelSerializer 4 | from carmaker.models import VehicleModel 5 | 6 | 7 | class VehicleModelListCreateView(ListCreateAPIView): 8 | permission_classes = [AllowAny] 9 | queryset = VehicleModel.objects.all() 10 | serializer_class = VehicleModelSerializer 11 | 12 | 13 | class VehicleModelRetrieveUpdateDestroyView(RetrieveUpdateDestroyAPIView): 14 | permission_classes = [AllowAny] 15 | queryset = VehicleModel.objects.all() 16 | serializer_class = VehicleModelSerializer 17 | -------------------------------------------------------------------------------- /carmaker/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oscarychen/deep-dive-drf-model-serializer-relations/d5d7588b3723834f25e54be5232621d771e48619/carmaker/__init__.py -------------------------------------------------------------------------------- /carmaker/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CarmakerConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'carmaker' 7 | -------------------------------------------------------------------------------- /carmaker/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1 on 2022-12-16 04:02 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Engine', 16 | fields=[ 17 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('name', models.CharField(max_length=128)), 19 | ('displacement', models.FloatField()), 20 | ], 21 | ), 22 | migrations.CreateModel( 23 | name='Manufacturer', 24 | fields=[ 25 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 26 | ('name', models.CharField(max_length=64)), 27 | ], 28 | ), 29 | migrations.CreateModel( 30 | name='Project', 31 | fields=[ 32 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 33 | ('code_name', models.CharField(max_length=128)), 34 | ], 35 | ), 36 | migrations.CreateModel( 37 | name='VehicleModel', 38 | fields=[ 39 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 40 | ('model', models.CharField(max_length=256)), 41 | ('year', models.IntegerField()), 42 | ('engine_options', models.ManyToManyField(to='carmaker.engine')), 43 | ('maker', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='carmaker.manufacturer')), 44 | ('predecessor', 45 | models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, 46 | to='carmaker.vehiclemodel')), 47 | ('project', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='carmaker.project')), 48 | ], 49 | ), 50 | migrations.CreateModel( 51 | name='Vehicle', 52 | fields=[ 53 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 54 | ('VIN', models.CharField(max_length=18)), 55 | ('model', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='carmaker.vehiclemodel')), 56 | ], 57 | ), 58 | migrations.CreateModel( 59 | name='Engineer', 60 | fields=[ 61 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 62 | ('name', models.CharField(max_length=128)), 63 | ('works_on', models.ManyToManyField(to='carmaker.vehiclemodel')), 64 | ], 65 | ), 66 | ] 67 | -------------------------------------------------------------------------------- /carmaker/migrations/0002_load_mock_data.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1 on 2022-12-16 04:04 2 | 3 | from django.db import migrations 4 | 5 | 6 | def make_data(apps, schema_editor): 7 | Manufacturer = apps.get_model('carmaker', 'Manufacturer') 8 | buick = Manufacturer.objects.create(name="Buick") 9 | 10 | Engine = apps.get_model('carmaker', 'Engine') 11 | engine_27 = Engine.objects.create(name='Model D Inline-4', displacement=2.7) 12 | engine_28 = Engine.objects.create(name='Chevrolet Inline-4', displacement=2.8) 13 | 14 | Project = apps.get_model('carmaker', 'Project') 15 | project = Project.objects.create(code_name="project-d-35-roadster") 16 | 17 | VehicleModel = apps.get_model('carmaker', 'VehicleModel') 18 | vm_d_35 = VehicleModel.objects.create(model='Buick D-35 Roadster', year=1917, project=project, maker=buick, 19 | predecessor=None) 20 | vm_d_35.engine_options.add(engine_27, engine_28) 21 | 22 | Vehicle = apps.get_model('carmaker', 'Vehicle') 23 | Vehicle.objects.create(VIN="A123456789", model=vm_d_35) 24 | 25 | Engineer = apps.get_model('carmaker', 'Engineer') 26 | engineer = Engineer.objects.create(name='Yoshida') 27 | engineer.works_on.add(vm_d_35) 28 | 29 | 30 | class Migration(migrations.Migration): 31 | dependencies = [ 32 | ('carmaker', '0001_initial'), 33 | ] 34 | 35 | operations = [ 36 | migrations.RunPython(make_data) 37 | ] 38 | -------------------------------------------------------------------------------- /carmaker/migrations/0003_alter_vehiclemodel_project.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1 on 2022-12-28 01:46 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('carmaker', '0002_load_mock_data'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='vehiclemodel', 16 | name='project', 17 | field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, to='carmaker.project'), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /carmaker/migrations/0004_alter_project_code_name.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1 on 2022-12-29 07:59 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('carmaker', '0003_alter_vehiclemodel_project'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='project', 15 | name='code_name', 16 | field=models.CharField(max_length=128, unique=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /carmaker/migrations/0005_alter_vehiclemodel_maker_alter_vehiclemodel_project.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1 on 2022-12-29 08:23 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('carmaker', '0004_alter_project_code_name'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='vehiclemodel', 16 | name='maker', 17 | field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='carmaker.manufacturer'), 18 | ), 19 | migrations.AlterField( 20 | model_name='vehiclemodel', 21 | name='project', 22 | field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.SET_NULL, to='carmaker.project'), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /carmaker/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oscarychen/deep-dive-drf-model-serializer-relations/d5d7588b3723834f25e54be5232621d771e48619/carmaker/migrations/__init__.py -------------------------------------------------------------------------------- /carmaker/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class Manufacturer(models.Model): 5 | name = models.CharField(max_length=64) 6 | 7 | 8 | class Engine(models.Model): 9 | name = models.CharField(max_length=128) 10 | displacement = models.FloatField() 11 | 12 | 13 | class Project(models.Model): 14 | code_name = models.CharField(max_length=128, unique=True) 15 | 16 | 17 | class VehicleModel(models.Model): 18 | model = models.CharField(max_length=256) 19 | year = models.IntegerField() 20 | project = models.OneToOneField(Project, on_delete=models.SET_NULL, null=True) 21 | maker = models.ForeignKey(Manufacturer, on_delete=models.SET_NULL, null=True) 22 | predecessor = models.ForeignKey("self", on_delete=models.SET_NULL, null=True) 23 | engine_options = models.ManyToManyField(Engine) 24 | 25 | 26 | class Vehicle(models.Model): 27 | VIN = models.CharField(max_length=18) 28 | model = models.ForeignKey(VehicleModel, on_delete=models.CASCADE) 29 | 30 | 31 | class Engineer(models.Model): 32 | name = models.CharField(max_length=128) 33 | works_on = models.ManyToManyField(VehicleModel) 34 | -------------------------------------------------------------------------------- /deep_dive_drf_model_serializer_relations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oscarychen/deep-dive-drf-model-serializer-relations/d5d7588b3723834f25e54be5232621d771e48619/deep_dive_drf_model_serializer_relations/__init__.py -------------------------------------------------------------------------------- /deep_dive_drf_model_serializer_relations/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for deep_dive_drf_model_serializer_relations project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.1/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'deep_dive_drf_model_serializer_relations.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /deep_dive_drf_model_serializer_relations/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for deep_dive_drf_model_serializer_relations project. 3 | 4 | Generated by 'django-admin startproject' using Django 4.1.3. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.1/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/4.1/ref/settings/ 11 | """ 12 | 13 | from pathlib import Path 14 | 15 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 16 | BASE_DIR = Path(__file__).resolve().parent.parent 17 | 18 | # Quick-start development settings - unsuitable for production 19 | # See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/ 20 | 21 | # SECURITY WARNING: keep the secret key used in production secret! 22 | SECRET_KEY = 'django-insecure-l8shlx@giwm+mzlmd^_qgoobh9ffhm%zbk$3luj#_ufs@9_tp6' 23 | 24 | # SECURITY WARNING: don't run with debug turned on in production! 25 | DEBUG = True 26 | 27 | ALLOWED_HOSTS = [] 28 | 29 | # Application definition 30 | 31 | INSTALLED_APPS = [ 32 | 'django.contrib.admin', 33 | 'django.contrib.auth', 34 | 'django.contrib.contenttypes', 35 | 'django.contrib.sessions', 36 | 'django.contrib.messages', 37 | 'django.contrib.staticfiles', 38 | 'carmaker' 39 | ] 40 | 41 | MIDDLEWARE = [ 42 | 'django.middleware.security.SecurityMiddleware', 43 | 'django.contrib.sessions.middleware.SessionMiddleware', 44 | 'django.middleware.common.CommonMiddleware', 45 | 'django.middleware.csrf.CsrfViewMiddleware', 46 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 47 | 'django.contrib.messages.middleware.MessageMiddleware', 48 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 49 | ] 50 | 51 | ROOT_URLCONF = 'deep_dive_drf_model_serializer_relations.urls' 52 | 53 | TEMPLATES = [ 54 | { 55 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 56 | 'DIRS': [], 57 | 'APP_DIRS': True, 58 | 'OPTIONS': { 59 | 'context_processors': [ 60 | 'django.template.context_processors.debug', 61 | 'django.template.context_processors.request', 62 | 'django.contrib.auth.context_processors.auth', 63 | 'django.contrib.messages.context_processors.messages', 64 | ], 65 | }, 66 | }, 67 | ] 68 | 69 | WSGI_APPLICATION = 'deep_dive_drf_model_serializer_relations.wsgi.application' 70 | 71 | # Database 72 | # https://docs.djangoproject.com/en/4.1/ref/settings/#databases 73 | 74 | DATABASES = { 75 | 'default': { 76 | 'ENGINE': 'django.db.backends.sqlite3', 77 | 'NAME': BASE_DIR / 'db.sqlite3', 78 | } 79 | } 80 | 81 | # Password validation 82 | # https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators 83 | 84 | AUTH_PASSWORD_VALIDATORS = [ 85 | { 86 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 87 | }, 88 | { 89 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 90 | }, 91 | { 92 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 93 | }, 94 | { 95 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 96 | }, 97 | ] 98 | 99 | # Internationalization 100 | # https://docs.djangoproject.com/en/4.1/topics/i18n/ 101 | 102 | LANGUAGE_CODE = 'en-us' 103 | 104 | TIME_ZONE = 'UTC' 105 | 106 | USE_I18N = True 107 | 108 | USE_TZ = True 109 | 110 | # Static files (CSS, JavaScript, Images) 111 | # https://docs.djangoproject.com/en/4.1/howto/static-files/ 112 | 113 | STATIC_URL = 'static/' 114 | 115 | # Default primary key field type 116 | # https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field 117 | 118 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 119 | -------------------------------------------------------------------------------- /deep_dive_drf_model_serializer_relations/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path, include 2 | 3 | urlpatterns = [ 4 | path("api_1/", include("api_1.urls")), 5 | path("api_2/", include("api_2.urls")), 6 | path("api_3/", include("api_3.urls")) 7 | ] 8 | -------------------------------------------------------------------------------- /deep_dive_drf_model_serializer_relations/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for deep_dive_drf_model_serializer_relations project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.1/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'deep_dive_drf_model_serializer_relations.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /docs/carmaker_erd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oscarychen/deep-dive-drf-model-serializer-relations/d5d7588b3723834f25e54be5232621d771e48619/docs/carmaker_erd.png -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'deep_dive_drf_model_serializer_relations.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Handling model relations with Django REST Framework ModelSerializer 2 | 3 | --- 4 | _Deep dive on using Django REST Framework ModelSerializer to read, create and update model relations_ 5 | 6 | [Postman](https://www.postman.com/quackyduck/workspace/deep-dive-drf-modelserializer-relations/collection/25111927-777d6f3f-08ab-4cdd-b4ba-abf5c50648fe?ctx=documentation) 7 | is used for developing the APIs. 8 | 9 | # Part I: The basics 10 | 11 | In Part I, we are going to set up a few Django models for us to play with, and then set up a couple of REST APIs using 12 | ModelSerializers; nothing special going on here if you are already familiar with how to use DRF generic views. 13 | ![image](docs/carmaker_erd.png) 14 | 15 | In the center of our ERD, the `VehicleModel` model has the following relations: 16 | 17 | - One-to-one relation with `Project` 18 | - Many-to-one (foreign key) relation with `Manufacturer` 19 | - Many-to-one (foreign key) relation with `VehicleModel` (itself) 20 | - Many-to-many relation with `Engine` 21 | 22 | As well as the following Django reverse relations: 23 | 24 | - Many-to-one (foreign key) relation from `Vehicle` 25 | - Many-to-many relation from `Engineer` 26 | 27 | See [carmaker.models](carmaker/models.py) for details on the model set up. 28 | 29 | For this part, I've also set up a couple of views using Django REST Framework generic views with ModelSerializers. 30 | Because I want to expand on the APIs later, I'm putting the APIs in Part I in their own module called [api_1](api_1). 31 | 32 | With a standard `ModelSerializer` for `VehicleModel` such as: 33 | 34 | ```python 35 | class VehicleModelSerializer(ModelSerializer): 36 | class Meta: 37 | model = VehicleModel 38 | fields = "__all__" 39 | ``` 40 | 41 | and a standard `ListCreateAPIView`: 42 | 43 | ```python 44 | class VehicleModelListCreateView(ListCreateAPIView): 45 | permission_classes = [AllowAny] 46 | queryset = VehicleModel.objects.all() 47 | serializer_class = VehicleModelSerializer 48 | ``` 49 | 50 | We can start testing these APIs. 51 | 52 | ## Basic read behavior 53 | 54 | We can now call the listing endpoint using: 55 | 56 | ```commandline 57 | curl --location --request GET 'http://localhost:8000/api_1/' 58 | ``` 59 | 60 | _you can also use 61 | this [Postman example](https://www.postman.com/quackyduck/workspace/deep-dive-drf-modelserializer-relations/request/25111927-987f0fef-d4df-4ffe-85a6-8f51fad1b421) 62 | ._ 63 | 64 | and get the following response: 65 | 66 | ```json 67 | [ 68 | { 69 | "id": 1, 70 | "model": "Buick D-35 Roadster", 71 | "year": 1917, 72 | "project": 1, 73 | "maker": 1, 74 | "predecessor": null, 75 | "engine_options": [ 76 | 1, 77 | 2 78 | ] 79 | } 80 | ] 81 | ``` 82 | 83 | We can make the following observations: 84 | > **TLDR** DRF `ModelSerializer`'s basic **read** behavior: 85 | > - Returns Django model attributes as they are defined on ORM model, including auto fields 86 | > - Returns value of appropriate type as defined by the ORM model field 87 | > - Includes all relations declared on the ORM model 88 | > - The related instances are returned as their primary keys 89 | > - Does not include any Django reverse relations 90 | 91 | ## Basic write behavior 92 | 93 | Similarly, we can call the same endpoint to create a new instance: 94 | 95 | ```commandline 96 | curl --location --request POST 'http://localhost:8000/api_1/' \ 97 | --header 'Content-Type: application/json' \ 98 | --data-raw '{ 99 | "model": "Buick D-35 Roadster 2", 100 | "year": 1919, 101 | "project": null, 102 | "maker": 1, 103 | "predecessor": 1, 104 | "engine_options": [ 105 | 1, 106 | 2 107 | ] 108 | }' 109 | ``` 110 | 111 | you can also use 112 | this [Postman example](https://www.postman.com/quackyduck/workspace/deep-dive-drf-modelserializer-relations/request/25111927-889545d1-03d0-47cd-965e-7bf49c832f26) 113 | . 114 | 115 | You can play around with this request data, and try to include some additional related fields, and make the following 116 | observations: 117 | 118 | > **TLDR** DRF `ModelSerializer`'s basic **write** behavior: 119 | > - Relations can be made using existing related instance primary key 120 | > - Django reverse relations are ignored 121 | > - Unrecognized and read-only attributes are ignored 122 | 123 | --- 124 | 125 | # Part II: Include related instance data 126 | 127 | Part II example code can be found in module [api_2](api_2). 128 | 129 | A common way of including related instance data is through nesting of the serializers: 130 | 131 | ```python 132 | class VehicleModelSerializer(ModelSerializer): 133 | project = ProjectSerializer() 134 | maker = ManufacturerSerializer() 135 | engine_options = EngineSerializer(many=True) 136 | vehicle_set = VehicleSerializer(many=True) # reverse relation 'vehicle_set' 137 | engineers_responsible = EngineerSerializer(many=True, source="engineer_set") # reverse relation 'engineer_set' 138 | 139 | class Meta: 140 | model = VehicleModel 141 | fields = "__all__" 142 | ``` 143 | 144 | ## Reading related model data using nested serializer 145 | 146 | With this serializer, we can hit 147 | the [listing endpoint](https://www.postman.com/quackyduck/workspace/deep-dive-drf-modelserializer-relations/request/25111927-6f367f5f-8209-4386-ad77-8ac08c8bed01) 148 | again. Note that the reverse relations will even work as 149 | long as the reverse attribute name declared on the serializer matches what is on the ORM model, or 150 | initialized with the `source` argument pointing to a matching ORM model attribute. 151 | You should see the response data containing a list of objects like the following: 152 | 153 | ```json 154 | { 155 | "id": 1, 156 | "project": { 157 | "id": 1, 158 | "code_name": "project-d-35-roadster" 159 | }, 160 | "maker": { 161 | "id": 1, 162 | "name": "Buick" 163 | }, 164 | "engine_options": [ 165 | { 166 | "id": 1, 167 | "name": "Model D Inline-4", 168 | "displacement": 2.7 169 | }, 170 | { 171 | "id": 2, 172 | "name": "Chevrolet Inline-4", 173 | "displacement": 2.8 174 | } 175 | ], 176 | "vehicle_set": [ 177 | { 178 | "id": 1, 179 | "VIN": "A123456789", 180 | "model": 1 181 | } 182 | ], 183 | "engineers_responsible": [ 184 | { 185 | "id": 1, 186 | "name": "Yoshida", 187 | "works_on": [ 188 | 1 189 | ] 190 | } 191 | ], 192 | "model": "Buick D-35 Roadster", 193 | "year": 1917, 194 | "predecessor": null 195 | } 196 | ``` 197 | 198 | > **TLDR** DRF nested serializer **read** behavior: 199 | > - Related instance nested serializer must be initialized with `many=True` if there are more than one instance expected 200 | > - Django ORM reverse relations also work if matching model attribute name or specified by `source` argument 201 | 202 | ## Writing related model data using nested serialier 203 | 204 | I think creating and updating related instance is probably not good RESTful design, but sometimes we may be asked to do 205 | so because the related model may be very small. 206 | 207 | Interestingly, if we try to call the creation endpoint, DRF will complain about not sure what to do with the related 208 | model data. 209 | 210 | To allow writing to related models, we will need to overwrite the `VehicleModelSerializer.to_internal_value()` method 211 | so that the related fields provided by the client is mapped back to related model instances: 212 | 213 | ```python 214 | class VehicleModelSerializer(ModelSerializer): 215 | project = ProjectSerializer() 216 | maker = ManufacturerSerializer() 217 | engine_options = EngineSerializer(many=True) 218 | vehicle_set = VehicleSerializer(many=True, read_only=True) 219 | engineers_responsible = EngineerSerializer(many=True, source="engineer_set") 220 | 221 | def to_internal_value(self, data): 222 | new_data = super().to_internal_value(data) 223 | 224 | new_data["maker"] = Manufacturer.objects.get(**new_data["maker"]) 225 | 226 | engine_options_q = Q() 227 | for engine in new_data["engine_options"]: 228 | engine_options_q |= Q(**engine) 229 | new_data["engine_options"] = Engine.objects.filter(engine_options_q) 230 | 231 | engineer_set_q = Q() 232 | for engineer in new_data["engineer_set"]: 233 | engineer_set_q |= Q(**engineer) 234 | new_data["engineer_set"] = Engineer.objects.filter(engineer_set_q) 235 | 236 | return new_data 237 | ``` 238 | 239 | and with this, we can call 240 | the [creation API](https://www.postman.com/quackyduck/workspace/deep-dive-drf-modelserializer-relations/request/25111927-1e60feff-c5ea-4732-86df-049c74a99586) 241 | with the following data to create a new instance of `VehicleModel` as 242 | well 243 | as the associated `Project` instance, relations to existing `Manufacturer`, `Engine` and `Engineer` instances will be 244 | made: 245 | 246 | ```json 247 | { 248 | "project": { 249 | "code_name": "project-d-35-roadster-3" 250 | }, 251 | "maker": { 252 | "name": "Buick" 253 | }, 254 | "engine_options": [ 255 | { 256 | "name": "Model D Inline-4", 257 | "displacement": 2.7 258 | }, 259 | { 260 | "name": "Chevrolet Inline-4", 261 | "displacement": 2.8 262 | } 263 | ], 264 | "vehicle_set": [ 265 | { 266 | "VIN": "A123456789", 267 | "model": 1 268 | } 269 | ], 270 | "engineers_responsible": [ 271 | { 272 | "name": "Yoshida" 273 | } 274 | ], 275 | "model": "Buick D-35 Roadster", 276 | "year": 1917, 277 | "predecessor": null 278 | } 279 | ``` 280 | 281 | Note that in this example, we are only handling the fields where we want the API to retrieve existing related instances, 282 | which are 'maker', 'engine_options', and 'engineers_responsible'. We did not handle any fields where the API may be 283 | required to create new instances of related model such as 'project'. (`Project` and `VehicleModel` have one-to-one 284 | relation, so we are pretending that one of the API requirements is that when creating a new `VehicleModel`, also 285 | create a `Project` at the same time). 286 | 287 | We could have created a new instance of `Project` in `.to_internal_value` method, but we elect not to do it here in case 288 | there is validation issue with the data from client later on, and we wouldn't want a `Project` instance to be created 289 | before data validation completes. Instead, we will handle related object creation by overwriting the `.create()` 290 | method: 291 | 292 | ```python 293 | class VehicleModelSerializer(ModelSerializer): 294 | ... # omitted, see above 295 | 296 | def create(self, validated_data): 297 | validated_data["project"] = Project.objects.create(**validated_data.pop("project")) 298 | instance = super().create(validated_data) 299 | return instance 300 | ``` 301 | 302 | Now, you may have noticed that the 'vehicle_set' attribute from the serializer is not being handled, because we have 303 | initialized this field as a read-only field, therefore the data is stripped by the `ModelSerializer.to_internal_value()` 304 | method. This would be based on the specific API design requirements. 305 | 306 | Similar to the handling of creation, we may be asked to handle updating of the related model as well, it can be done in 307 | very similar fashion by overwriting the `.update()` method: 308 | 309 | ```python 310 | class VehicleModelSerializer(ModelSerializer): 311 | ... # omitted, see above 312 | 313 | def update(self, instance, validated_data): 314 | project_data = validated_data.pop("project", None) 315 | if project_data is not None and instance.project.code_name != project_data["code_name"]: 316 | instance.project.delete() 317 | validated_data["project"] = Project.objects.create(**project_data) 318 | return super().update(instance, validated_data) 319 | 320 | ``` 321 | 322 | Note that we are checking `project_data is not None`, this may or may not be the desired behavior based on API design 323 | requirement. You may be asked to delete the related `Project` if the client passes `null` value or empty string, or not 324 | do anything with it, etc. Some requirements may not be good RESTful API design, but it can happen. 325 | 326 | By now you might have noticed that you cannot update a `VehicleModel` instance with the same `Project`, but you can 327 | update it to new `Project` with different 'code name'. This is because `Project` model's 'code name' field has unique 328 | constraint, and `.to_internal_value()` will validate client data against field level validation which includes ORM 329 | model field arguments. 330 | 331 | If for some reason you need to allow client to specify the `Project`'s 'code name' again in 332 | the body (such as in the case of a 'PUT' request where you are essentially replacing the current instance), you may 333 | consider bypassing some of the data from `super().to_internal_value()`: 334 | 335 | ```python 336 | class VehicleModelSerializer(ModelSerializer): 337 | ... 338 | 339 | def to_internal_value(self, data): 340 | project_data = data.pop("project", None) 341 | new_data = super().to_internal_value(data) 342 | if project_data: 343 | new_data["project"] = project_data 344 | ... 345 | return new_data 346 | ``` 347 | 348 | Note the project data is popped from `data` to circumvent being pushed into `super().to_internal_value()`, and then 349 | added back to the `new_data` before return. 350 | 351 | Another key thing to note is that if you put a debug breakpoint inside of the `.to_internal_value()` method, you would 352 | see that the `data` passed in has been changed by `super().to_internal_value(data)` (aka `new_data` in our example). 353 | While `data` has key 'engineers_responsible' which is how the client passes in, `new_data` change it to key 354 | 'engineer_set'. This is one of DRF `ModelSerializer.to_internal_value()` method's responsibility: to map raw data to 355 | ORM field data. 356 | 357 | Some takeaways: 358 | > **TLDR** DRF nested serializer **write** behavior: 359 | > - `.to_internal_value()` method is called before `.validate()` 360 | > - `.to_internval_value()` method validates against field-level constraints set by ORM model fields 361 | > - `.to_internal_value()` method maps raw client data to ORM field data, which may change data keys if a field's name 362 | is different from its `source` 363 | > - when nesting a related model serializer as a serializer field, we need to take care of mapping them back to Django 364 | ORM model instances, it is a good idea to: 365 | > > - Retrieve existing relations inside `.to_internal_value()` 366 | > > - Create/update new related instances inside `.create()`/`.update()` 367 | 368 | --- 369 | 370 | # Part III: Hoisting related model data 371 | 372 | Part III example code can be found in module [api_3](api_3). 373 | 374 | Often times we are asked to design an endpoint for a particular model that includes some attributes of a related model, 375 | and in away that the related data appears to be attributes of this model as far as the client is concerned. 376 | This can be implemented in a number of different ways with DRF, but I am only going to focus on achieving two-way data 377 | binding - so it works consistently in both **read** and **write** operations using a single mechanism. 378 | 379 | ## Reading related model data directly 380 | 381 | For this example, let's hoist the related `Project` instance's `code_name` attribute to our `VehicleModel` serializer 382 | so that it would appears as an attribute of the `VehicleModel`. We are going to use a custom field: 383 | 384 | ```python 385 | class ProjectCodeNameField(RelatedField): 386 | queryset = Project.objects.all() 387 | 388 | def to_internal_value(self, data): 389 | return data 390 | 391 | def to_representation(self, value): 392 | return getattr(value, "code_name") 393 | ``` 394 | 395 | and now we can use this field on our serializer for `VehicleModel`: 396 | 397 | ```python 398 | class VehicleModelSerializer(ModelSerializer): 399 | project_code_name = ProjectCodeNameField(source="project") 400 | 401 | class Meta: 402 | model = VehicleModel 403 | fields = "__all__" 404 | ``` 405 | 406 | We can hit 407 | the [listing endpoint](https://www.postman.com/quackyduck/workspace/deep-dive-drf-modelserializer-relations/request/25111927-b3a59e33-75c4-4b96-a85a-289f8a6b3d9e) 408 | again, and you should see the response containing list of `VehicleModel` objects 409 | such 410 | as this: 411 | 412 | ```json 413 | { 414 | "id": 1, 415 | "project_code_name": "project-d-35-roadster-x1", 416 | "model": "Buick D-35 Roadster2", 417 | "year": 1917, 418 | "project": 4, 419 | "maker": 1, 420 | "predecessor": null, 421 | "engine_options": [ 422 | 1, 423 | 2 424 | ] 425 | } 426 | ``` 427 | 428 | where the related `Project.code_name` has been made available as 'project_code_name'. 429 | 430 | ## Writing related model data directly 431 | 432 | Similar to what we did in Part II, we need to handle related model instance creation by overwriting `.create()` method: 433 | 434 | ```python 435 | class VehicleModelSerializer(ModelSerializer): 436 | maker = ManufacturerNameField() 437 | project_code_name = ProjectCodeNameField(source="project") 438 | 439 | def to_internal_value(self, data): 440 | new_data = super().to_internal_value(data) 441 | new_data["maker"] = Manufacturer.objects.get(name=new_data["maker"]) 442 | return new_data 443 | 444 | def create(self, validated_data): 445 | if validated_data.get("project"): 446 | validated_data["project"] = Project.objects.create(code_name=validated_data["project"]) 447 | return super().create(validated_data) 448 | 449 | class Meta: 450 | model = VehicleModel 451 | fields = "__all__" 452 | ``` 453 | 454 | I also intentionally added another field `maker` here to demonstrate how the it is handled a little bit 455 | different from `project_code_name` field. The `maker` field is pointing to the related `Manufacturer` instance's 'name', 456 | while the `project_code_name` is point to the related `Project` instance's 'code_name'. Just like what we did earlier in 457 | Part II, we are assuming the API requirement dictates that it only needs to associate the `VehicleModel` instance 458 | to be created with an **existing** `Manufacturer` instance, but associated with a **new** `Project` instance (because of 459 | the one-to-one relationship). 460 | 461 | For this reason, we are mapping the `maker` field to an existing `Manufacturer` instance inside `.to_internal_value()`, 462 | while creating a new `Project` instance inside the `.create()`. 463 | 464 | Some takeaways regarding accessing related model data directly: 465 | 466 | > **TLDR** DRF serializer accessing related model data: 467 | > - subclass `restframework.serializers.RelatedField` for accessing an attribute of a related model, this allows for 468 | reading and writing using the same mechanism 469 | > - whenever writing to related model, we need to handle write behavior, it is a good idea to: 470 | > > - Retrieve existing relations inside `.to_internal_value()` 471 | > > - Create/update new related instances inside `.create()`/ `.update()` 472 | 473 | --------------------------------------------------------------------------------