Use of nested serializers with explicit ForeignKey binding replaced by raw IntegerField

I am trying to get rid of the explicit binding between my models. Thus, instead of ForeignKey , I use IntegerField to simply save the primary key of the target model as a field. This way I will handle the relationship manually at the code level. This is because I will have to move some schemas to different database instances. Therefore, they cannot have a connection.

Now I am facing a problem with my nested serializer. I am trying to instantiate a model below:

  17 class Customer(models.Model): 18 id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) 19 phone_no = models.CharField(max_length=15, unique=True) 

And this is my CustomerAddress model that references the Customer model:

  47 class CustomerAddress(models.Model): 48 # customer = models.ForeignKey(Customer, related_name='cust_addresses') 49 customer_id = models.UUIDField(default=uuid.uuid4) 50 address = models.CharField(max_length=1000) 

Following are my serializers:

  7 class CustomerAddressSerializer(serializers.ModelSerializer): 8 9 class Meta: 10 model = CustomerAddress 11 depth = 1 31 class CustomerSerializer(serializers.ModelSerializer): 32 cust_addresses = CustomerAddressSerializer(many=True) 33 34 class Meta: 35 model = Customer 36 depth = 1 37 fields = ('id', 'phone_no', 'cust_addresses',) 38 39 def create(self, validated_data): 40 cust = Customer.objects.create(id=uuid.uuid4()) 41 42 for addr in validated_data['cust_addresses']: 43 address = addr['address'] 44 cust_addr = CustomerAddress.objects.create(address=address, customer_id=cust.id) 45 46 return cust 

My views:

  12 class CustomerView(generics.RetrieveAPIView, generics.CreateAPIView): 13 serializer_class = CustomerSerializer 14 22 def get_object(self): 23 session = self.request.session 24 if session.has_key('uuid'): 25 id = session['uuid'] 26 cust = Customer.objects.get(pk=uuid.UUID(id)) 27 return cust 28 return None 

When I try to run the send request above, from my test:

  71 def test_create_customer_address(self): 72 cust_url = reverse('user_v1:customer') 73 # Now we create a customer, and use it UID in the "customer" data of /cust-address/ 74 cust_data = {"first_name": "Rohit", "last_name": "Jain", "phone_no": "xxxxxx", "email_id": " test@gmail.com ", "cust_addresses": [{"city_id": 1, "address": "addr", "pin_code": "123124", "address_tag": "XYZ"}]} 75 cust_response = self.client.post(cust_url, cust_data, format='json') 76 print 'Post Customer' 77 print cust_response 78 self.assertEqual(cust_response.data['id'], str(cust_id)) 

My test does not work with the following error:

 ====================================================================== ERROR: test_create_customer_address (app.tests.CustomerViewTest) ---------------------------------------------------------------------- Traceback (most recent call last): File "/home/ubuntu/src/django-proj/app/tests.py", line 75, in test_create_customer_address cust_response = self.client.post(cust_url, cust_data, format='json') File "/home/ubuntu/Envs/rj-venv/local/lib/python2.7/site-packages/rest_framework/test.py", line 168, in post path, data=data, format=format, content_type=content_type, **extra) File "/home/ubuntu/Envs/rj-venv/local/lib/python2.7/site-packages/rest_framework/test.py", line 90, in post return self.generic('POST', path, data, content_type, **extra) File "/home/ubuntu/Envs/rj-venv/local/lib/python2.7/site-packages/rest_framework/compat.py", line 231, in generic return self.request(**r) File "/home/ubuntu/Envs/rj-venv/local/lib/python2.7/site-packages/rest_framework/test.py", line 157, in request return super(APIClient, self).request(**kwargs) File "/home/ubuntu/Envs/rj-venv/local/lib/python2.7/site-packages/rest_framework/test.py", line 109, in request request = super(APIRequestFactory, self).request(**kwargs) File "/home/ubuntu/Envs/rj-venv/local/lib/python2.7/site-packages/django/test/client.py", line 466, in request six.reraise(*exc_info) File "/home/ubuntu/Envs/rj-venv/local/lib/python2.7/site-packages/django/core/handlers/base.py", line 132, in get_response response = wrapped_callback(request, *callback_args, **callback_kwargs) File "/home/ubuntu/Envs/rj-venv/local/lib/python2.7/site-packages/django/views/decorators/csrf.py", line 58, in wrapped_view return view_func(*args, **kwargs) File "/home/ubuntu/Envs/rj-venv/local/lib/python2.7/site-packages/django/views/generic/base.py", line 71, in view return self.dispatch(request, *args, **kwargs) File "/home/ubuntu/Envs/rj-venv/local/lib/python2.7/site-packages/rest_framework/views.py", line 456, in dispatch response = self.handle_exception(exc) File "/home/ubuntu/Envs/rj-venv/local/lib/python2.7/site-packages/rest_framework/views.py", line 453, in dispatch response = handler(request, *args, **kwargs) File "/home/ubuntu/Envs/rj-venv/local/lib/python2.7/site-packages/rest_framework/generics.py", line 190, in post return self.create(request, *args, **kwargs) File "/home/ubuntu/Envs/rj-venv/local/lib/python2.7/site-packages/rest_framework/mixins.py", line 21, in create headers = self.get_success_headers(serializer.data) File "/home/ubuntu/Envs/rj-venv/local/lib/python2.7/site-packages/rest_framework/serializers.py", line 470, in data ret = super(Serializer, self).data File "/home/ubuntu/Envs/rj-venv/local/lib/python2.7/site-packages/rest_framework/serializers.py", line 217, in data self._data = self.to_representation(self.instance) File "/home/ubuntu/Envs/rj-venv/local/lib/python2.7/site-packages/rest_framework/serializers.py", line 430, in to_representation attribute = field.get_attribute(instance) File "/home/ubuntu/Envs/rj-venv/local/lib/python2.7/site-packages/rest_framework/fields.py", line 317, in get_attribute raise type(exc)(msg) AttributeError: Got AttributeError when attempting to get a value for field `cust_addresses` on serializer `CustomerSerializer`. The serializer field might be named incorrectly and not match any attribute or key on the `Customer` instance. Original exception text was: 'Customer' object has no attribute 'cust_addresses'. ---------------------------------------------------------------------- 

All this worked fine when I had a CustomerAddress binding on ForeignKey on Customer . I don’t understand how to fix this. I tried to look at the source code to make sure that some tweaking was needed. But I'm at a loss. I feel that I need to somehow configure my serializer, maybe override the to_representation method, but I'm not sure.

BTW, an error occurs only when creating an instance of the model. For a GET request, I get the correct json with a nested serializer.

Has anyone else tried to do something similar and succeeded? What needs to be done to make this work? And yes, I have to remove the explicit foreign key binding.

+5
source share
2 answers

I would take the REST structure from the equation and initially just look at how you want to manage these relationships at the model level and manager / request level.

I would start with what you have in your own answer, but maybe make cust_addresses cached property, so you can prevent multiple searches.

 class Customer(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) phone_no = models.CharField(max_length=15, unique=True) # Only make the relationship query once. @cached_property def cust_addresses(self): return CustomerAddress.objects.filter(customer_id=self.id) class CustomerAddress(models.Model): customer_id = models.UUIDField(default=uuid.uuid4) address = models.CharField(max_length=1000) 

You can then consider adding pre-caching behavior in the Client Manager / Request class. For example, you can pre-cache relationships when creating both the client and their set of addresses:

 class CustomerManager(models.Manager): def create(phone_no, cust_addresses): """ Customers and addresses are always created together. Usage: CustomerManager.objects.create( phone_no='123', cust_addresses=[{'address': 'abc'}, {'address': 'def'}] ) """ customer = super(CustomerManager, self).create(phone_no=phone_no) addresses = [ CustomerAddress.objects.create( customer_id=instance.id address=item['address'] ) for item in cust_addresses ] # When creating both Customer and Address instances, # we can pre-cache the relationship. customer.__dict__['cust_addresses'] = addresses return customer 

Be sure to add objects = CustomerManager() to the Customer model class.

Then you have integration with REST serializers to work with them.

Note that I refused to use ModelSerializer in favor of the simple Serializer class. For everything except quick prototyping, I would prefer this - what you lose in duplication, you get in simplicity and clarity.

 class CustomerAddressSerializer(serializers.Serializer): address = serializers.CharField(max_length=1000) class CustomerSerializer(serializers.Serializer): id = serializers.UUIDField(read_only=True) phone_no = serializers.CharField(max_length=15, validators=[UniqueValidator(queryset=Customer.objects.all())]) cust_addresses = CustomerAddressSerializer(many=True) def create(self, validated_data): return Customer.objects.create(**validated_data) 
+2
source

So, for now, I got it by adding cust_addresses as a property in the Customer model:

 class Customer(models.Model): ... other fields @property def cust_addresses(self): return CustomerAddress.objects.filter(customer_id = self.id) 

So now I get the correct JSON response, since now field.get_attribute(instance) works fine for the cust_addresses field in the serializer.

But I doubt that it is even remotely effective. For each access, it runs a database query. Hope someone can come up with a better way.

0
source

All Articles