How to fill test DB in Django with objects of models with ManyToManyFields?
Question:
I want to test speed of my project, so I need to fill my DB with test data.
I have a model with lots of ManyToManyField
s (later I will create more models):
class Deciduous(PlantBasicCharacteristics):
usda_zone = models.ManyToManyField(UsdaZone)
soil_moisture = models.ManyToManyField(SoilMoisture)
soil_fertility = models.ManyToManyField(SoilFertility)
soil_ph = models.ManyToManyField(SoilPh)
And I am trying to create utility to fill DB with test data:
from random import randint
from django.contrib.auth.models import User
from django.core.management.base import BaseCommand
from django.db.models.fields import CharField, DecimalField
from django.db.models.fields.related import ManyToManyField
from django.shortcuts import get_object_or_404
from plants.models import Deciduous
from plants.web_page_filter_fields import get_plant_class_fields
PLANT_CLASSES = [Deciduous, ]
TEST_DATA_AMOUNT = 3
class Command(BaseCommand):
def handle(self, *args, **options):
for plant_class in PLANT_CLASSES:
fields_and_names = get_plant_class_fields(plant_class)
data = []
for _ in range(TEST_DATA_AMOUNT):
data.append(self.create_data(fields_and_names))
print(data)
plant_class.objects.bulk_create([
plant_class(**values) for values in data
])
def create_data(self, fields_and_names):
data = {}
for field_name, field_model in fields_and_names.items():
if ('ptr' not in field_name
and 'synonym' not in field_name
and field_name != 'id'):
if isinstance(field_model, CharField):
data[field_name] = self.get_string_random()
elif isinstance(field_model, DecimalField):
data[field_name] = self.get_number_random()
elif isinstance(field_model, ManyToManyField):
data[field_name] = [self.get_choice_random(field_model)]
return data
def get_string_random(self):
letters = [chr(randint(97, 122)) for _ in range(randint(5, 20))]
return ''.join(letters).capitalize()
def get_number_random(self):
return randint(10, 15000) / 100
def get_choice_random(self, model):
field_model = model.related_model
field_choices = field_model._meta.get_field('name').choices
choice_number = randint(0, len(field_choices) - 1)
choice = field_choices[choice_number][0]
return get_object_or_404(field_model, name=choice)
But I get:
File "manage.py", line 22, in <module>
main()
File "manage.py", line 18, in main
execute_from_command_line(sys.argv)
File "/usr/local/lib/python3.7/site-packages/django/core/management/__init__.py", line 419, in execute_from_command_line
utility.execute()
File "/usr/local/lib/python3.7/site-packages/django/core/management/__init__.py", line 413, in execute
self.fetch_command(subcommand).run_from_argv(self.argv)
File "/usr/local/lib/python3.7/site-packages/django/core/management/base.py", line 354, in run_from_argv
self.execute(*args, **cmd_options)
File "/usr/local/lib/python3.7/site-packages/django/core/management/base.py", line 398, in execute
output = self.handle(*args, **options)
File "/app/plants/management/commands/add_test_data.py", line 30, in handle
plant_class(**values) for values in data
File "/app/plants/management/commands/add_test_data.py", line 30, in <listcomp>
plant_class(**values) for values in data
File "/usr/local/lib/python3.7/site-packages/django/db/models/base.py", line 498, in __init__
_setattr(self, prop, kwargs[prop])
File "/usr/local/lib/python3.7/site-packages/django/db/models/fields/related_descriptors.py", line 547, in __set__
% self._get_set_deprecation_msg_params(),
TypeError: Direct assignment to the forward side of a many-to-many set is prohibited. Use usda_zone.set() instead.
Is it possible to create objects using a for loop with set() and not to write code for every ManyToManyField?
Answers:
Try to use the set()
method to create objects for ManyToManyFields
so:
def create_data(self, fields_and_names):
data = {}
for field_name, field_model in fields_and_names.items():
if ('ptr' not in field_name
and 'synonym' not in field_name
and field_name != 'id'):
if isinstance(field_model, CharField):
data[field_name] = self.get_string_random()
elif isinstance(field_model, DecimalField):
data[field_name] = self.get_number_random()
elif isinstance(field_model, ManyToManyField):
choices = field_model.related_model.objects.all()
data[field_name] = set(choices.order_by('?')[:randint(1, len(choices))])
return data
Here, order_by('?')
is used to randomly order the choices and select a random number of choices using slicing.
Edit
Try this:
from random import sample
# ...
if isinstance(field_model, ManyToManyField):
choices = field_model.related_model.objects.all()
count = randint(1, len(choices))
data[field_name] = set(sample(choices, count))
Edit 2
Try to first create the object without the many-to-many relationships, then add the related objects using the set
method so:
def create_data(self, fields_and_names):
data = {}
many_to_many_fields = []
for field_name, field_model in fields_and_names.items():
if ('ptr' not in field_name
and 'synonym' not in field_name
and field_name != 'id'):
if isinstance(field_model, CharField):
data[field_name] = self.get_string_random()
elif isinstance(field_model, DecimalField):
data[field_name] = self.get_number_random()
elif isinstance(field_model, ManyToManyField):
many_to_many_fields.append(field_name)
obj = Deciduous.objects.create(**data)
for field_name in many_to_many_fields:
choices = [self.get_choice_random(Deciduous._meta.get_field(field_name)) for _ in range(randint(1, 3))]
getattr(obj, field_name).set(choices)
return data
I want to test speed of my project, so I need to fill my DB with test data.
I have a model with lots of ManyToManyField
s (later I will create more models):
class Deciduous(PlantBasicCharacteristics):
usda_zone = models.ManyToManyField(UsdaZone)
soil_moisture = models.ManyToManyField(SoilMoisture)
soil_fertility = models.ManyToManyField(SoilFertility)
soil_ph = models.ManyToManyField(SoilPh)
And I am trying to create utility to fill DB with test data:
from random import randint
from django.contrib.auth.models import User
from django.core.management.base import BaseCommand
from django.db.models.fields import CharField, DecimalField
from django.db.models.fields.related import ManyToManyField
from django.shortcuts import get_object_or_404
from plants.models import Deciduous
from plants.web_page_filter_fields import get_plant_class_fields
PLANT_CLASSES = [Deciduous, ]
TEST_DATA_AMOUNT = 3
class Command(BaseCommand):
def handle(self, *args, **options):
for plant_class in PLANT_CLASSES:
fields_and_names = get_plant_class_fields(plant_class)
data = []
for _ in range(TEST_DATA_AMOUNT):
data.append(self.create_data(fields_and_names))
print(data)
plant_class.objects.bulk_create([
plant_class(**values) for values in data
])
def create_data(self, fields_and_names):
data = {}
for field_name, field_model in fields_and_names.items():
if ('ptr' not in field_name
and 'synonym' not in field_name
and field_name != 'id'):
if isinstance(field_model, CharField):
data[field_name] = self.get_string_random()
elif isinstance(field_model, DecimalField):
data[field_name] = self.get_number_random()
elif isinstance(field_model, ManyToManyField):
data[field_name] = [self.get_choice_random(field_model)]
return data
def get_string_random(self):
letters = [chr(randint(97, 122)) for _ in range(randint(5, 20))]
return ''.join(letters).capitalize()
def get_number_random(self):
return randint(10, 15000) / 100
def get_choice_random(self, model):
field_model = model.related_model
field_choices = field_model._meta.get_field('name').choices
choice_number = randint(0, len(field_choices) - 1)
choice = field_choices[choice_number][0]
return get_object_or_404(field_model, name=choice)
But I get:
File "manage.py", line 22, in <module>
main()
File "manage.py", line 18, in main
execute_from_command_line(sys.argv)
File "/usr/local/lib/python3.7/site-packages/django/core/management/__init__.py", line 419, in execute_from_command_line
utility.execute()
File "/usr/local/lib/python3.7/site-packages/django/core/management/__init__.py", line 413, in execute
self.fetch_command(subcommand).run_from_argv(self.argv)
File "/usr/local/lib/python3.7/site-packages/django/core/management/base.py", line 354, in run_from_argv
self.execute(*args, **cmd_options)
File "/usr/local/lib/python3.7/site-packages/django/core/management/base.py", line 398, in execute
output = self.handle(*args, **options)
File "/app/plants/management/commands/add_test_data.py", line 30, in handle
plant_class(**values) for values in data
File "/app/plants/management/commands/add_test_data.py", line 30, in <listcomp>
plant_class(**values) for values in data
File "/usr/local/lib/python3.7/site-packages/django/db/models/base.py", line 498, in __init__
_setattr(self, prop, kwargs[prop])
File "/usr/local/lib/python3.7/site-packages/django/db/models/fields/related_descriptors.py", line 547, in __set__
% self._get_set_deprecation_msg_params(),
TypeError: Direct assignment to the forward side of a many-to-many set is prohibited. Use usda_zone.set() instead.
Is it possible to create objects using a for loop with set() and not to write code for every ManyToManyField?
Try to use the set()
method to create objects for ManyToManyFields
so:
def create_data(self, fields_and_names):
data = {}
for field_name, field_model in fields_and_names.items():
if ('ptr' not in field_name
and 'synonym' not in field_name
and field_name != 'id'):
if isinstance(field_model, CharField):
data[field_name] = self.get_string_random()
elif isinstance(field_model, DecimalField):
data[field_name] = self.get_number_random()
elif isinstance(field_model, ManyToManyField):
choices = field_model.related_model.objects.all()
data[field_name] = set(choices.order_by('?')[:randint(1, len(choices))])
return data
Here, order_by('?')
is used to randomly order the choices and select a random number of choices using slicing.
Edit
Try this:
from random import sample
# ...
if isinstance(field_model, ManyToManyField):
choices = field_model.related_model.objects.all()
count = randint(1, len(choices))
data[field_name] = set(sample(choices, count))
Edit 2
Try to first create the object without the many-to-many relationships, then add the related objects using the set
method so:
def create_data(self, fields_and_names):
data = {}
many_to_many_fields = []
for field_name, field_model in fields_and_names.items():
if ('ptr' not in field_name
and 'synonym' not in field_name
and field_name != 'id'):
if isinstance(field_model, CharField):
data[field_name] = self.get_string_random()
elif isinstance(field_model, DecimalField):
data[field_name] = self.get_number_random()
elif isinstance(field_model, ManyToManyField):
many_to_many_fields.append(field_name)
obj = Deciduous.objects.create(**data)
for field_name in many_to_many_fields:
choices = [self.get_choice_random(Deciduous._meta.get_field(field_name)) for _ in range(randint(1, 3))]
getattr(obj, field_name).set(choices)
return data