I have a model called "Phones" which has: screen size, RAM, etc. I have another one called "Laptops" which has: screen size, RAM, and Keyboard (QWERTZ, QWERTY, etc.). I could make a main model with basic fields like Name and Price. I want to just select a "Laptop" or a "Phone", without having unnecessary fields (e.g.: keyboard type for phones, or rear camera for laptops).
Should I make all fields and leave unneeded ones empty (would look silly to have "RAM" and "Keyboard type" and "Rear camera mpx" for a Mug)? Or should I make separate models for each? But then how could I combine query results (search for "Xiaomi" returning from the different models like phones, laptops, bikes, vacuum cleaners, etc.)?
I'm not sure what is bad practice, but I'll throw you some potential ideas on how you could do this:
#1 Abstract Model
class BaseProduct(models.Model):
name = models.CharField(max_length=200)
cost = models.DecimalField(max_digits=10, decimal_places=2, default=0)
class Meta:
abstract = True
# all models below will have a name + cost attibute
# django might even throw them in the save table in the backend (not 100% sure)
class Phone(BaseProduct):
rear_camera_mpx = models.CharField(max_length=200)
# ..etc
class Laptop(BaseProduct):
ram = models.CharField(max_length=200)
# ..etc
###############################
# Example Query:
Laptop.objects.filter(name__icontains='MSI', ram='8gb')
# Filter Multiple Products
from itertools import chain
queryset_chain = chain(
Phone.objects.filter(name__icontains=query),
Laptop.objects.filter(name__icontains=query),
)
for i in queryset_chain
if type(i) == Laptop:
print(i.ram)
# elif
# .. etc
#2 Foreign Key Pointing back from Attributes
class BaseProduct(models.Model):
name = models.CharField(max_length=200)
cost = models.DecimalField(max_digits=10, decimal_places=2, default=0)
# could add a type
product_type = models.CharField(max_length=2, choices=PRODUCTTYPE_CHOICES, default='NA')
# Extra attributes, points back to base
class Phone(models.Model):
product = models.ForeignKey(BaseProduct, on_delete=models.PROTECT)
rear_camera_mpx = models.CharField(max_length=200)
# ..etc
class Laptop(models.Model):
product = models.ForeignKey(BaseProduct, on_delete=models.PROTECT)
ram = models.CharField(max_length=200)
# ..etc
###############################
# Example Query:
Laptop.objects.filter(product__name__icontains='MSI', ram='8gb')
# Search All Products
BaseProduct.objects.filter(name__icontains='MSI')
# then when you click on, use type to grab the correct full class based on "product_type"
if product_type == '01':
return Laptop.objects.filter(product__pk=clickedOnDetailPk).first()
#3 GenericForeign Key Pointing to Attributes
Note: I find generic keys very clumsy and hard to use (that's just me tho)
class BaseProduct(models.Model):
name = models.CharField(max_length=200)
cost = models.DecimalField(max_digits=10, decimal_places=2, default=0)
# could add a type
product_type = models.CharField(max_length=2, choices=PRODUCTTYPE_CHOICES, default='NA')
# Below the mandatory fields for generic relation
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey()
# Extra attributes, points back to base
class Phone(models.Model):
rear_camera_mpx = models.CharField(max_length=200)
# ..etc
class Laptop(models.Model):
ram = models.CharField(max_length=200)
# ..etc
###############################
# Example Query:
# Search All Products
l = BaseProduct.objects.filter(name__icontains='MSI')
for i in l:
print(i.name, i.cost)
print('Generic Key Obj:', i.content_object)
print('Generic Key PK:', i.content_id)
print('Generic Key Type:', i.content_type) # is number / can change if re-creating db (not fun)
if i.product_type == '01': # Laptop Type / could also go by content_type with some extra logic
print('RAM:', i.content_object.ram)
# to do stuff like \/ you need extra stuff (never sat down to figure this out)
BaseProduct.objects.filter(content_object__ram='8gb')
#4 Json Field + Cram it all into a Single Table
Requires newer version of DBs + Django
This could be abstracted out with proxy models + Managers pretty crazily. I've done it for a table for a similar use case, except imagine creating a laptop & including all the components that themselves are products :D. Not sure if it's bad practice, it's ALOT of custom stuff, but I've really liked my results.
class BaseProduct(models.Model):
name = models.CharField(max_length=200)
cost = models.DecimalField(max_digits=10, decimal_places=2, default=0)
# could add a type
product_type = models.CharField(max_length=2, choices=PRODUCTTYPE_CHOICES, default='NA')
# cram all the extra stuff as JSON
attr = models.JSONField(null=True)
###############################
# laptop search
l = BaseProduct.objects.filter(name__icontains='MSI', attr__ram='8gb')
for i in l:
print(i.name, i.cost, i.attr['ram'])
Overall
Overall I think #1 or #2 are the ways to go..
Unless you want to go wild and pretty much write everything, forms, admins, etc, then go #4
"Abstract Model" as Nealium mentioned works,it slipped my attention when I ran through the documentation.
Related
There are a lot of questions along these lines, but so far as I can tell, but many are quite old and I haven't found one that helped me understand my use case. I think I want to be looking at signals, but I'm not clear on exactly what I should be doing in terms of Django patterns and best practices.
Here's my Model code:
from django.db import models
from django.db.models.fields import SlugField, TextField
from django.utils import timezone
# Hexes have Terrains, which impart intrinsic properties, like base speed and .png image
# They're NOT 1:1 with Hexes IE) You might want Slow and Normal Grasslands with the same image
class Terrain(models.Model):
FAST = 'F'
NORMAL = 'N'
SLOW = 'S'
SPEED_CHOICES = [
(FAST, 'Fast'),
(NORMAL, 'Normal'),
(SLOW, 'Slow'),
]
name = models.CharField(max_length=64)
speed = models.CharField(max_length=6, choices=SPEED_CHOICES, default=NORMAL)
image = models.ImageField(upload_to='images/', blank=True)
def __str__(self):
return self.name
# Grids establish the dimensions of a HexMap. Grids may be used in many HexMaps
class Grid(models.Model):
name = models.CharField(max_length=64)
size = models.IntegerField(default=72)
num_rows = models.IntegerField(default=10)
num_cols= models.IntegerField(default=10)
def __str__(self):
return self.name
# Hexes represent the "tiles" on a Grid. A single Hex may appear in many Grids
class Hex(models.Model):
name = models.CharField(max_length=64, blank=True, null=True)
terrain = models.ForeignKey(Terrain, on_delete=models.CASCADE)
grids = models.ManyToManyField(Grid)
def __str__(self):
return self.id
class Meta:
verbose_name_plural = "Hexes"
# Locations are coordinal points on a HexMap. They may contain many Annotations
class Location(models.Model):
# HexMap Name + row + col, for a consistent way to ID locations
name = models.CharField(max_length=64, blank=True, null=True)
friendly_name = models.CharField(max_length=64, blank=True, null=False)
# TODO constrain these to the sizes specified in the grids? Sane defaults?
row = models.PositiveIntegerField()
col = models.PositiveIntegerField()
# Authors create Annotations
# TODO create Players and make Authors extend it so we can do rights and perms
class Author(models.Model):
first_name = models.CharField(max_length=64, blank=False, null=False)
last_name = models.CharField(max_length=64, blank=False, null=False)
join_date = timezone.now()
def __str__(self):
return self.first_name + " " + self.last_name
# Annotations are entries created by Authors. A location may have many Annotations
class Annotation(models.Model):
name = models.CharField(max_length=64, blank=False, null=False)
pubdate = join_date = timezone.now()
content = TextField()
# Hexmaps put it all together and allow fetching of Locations by (r,c) coords
class HexMap(models.Model):
name = models.CharField(max_length=64, blank=True, null=True)
rows = {}
cols = {}
#TODO - how do I create grid.num_rows * grid.num_cols locations and put them into the dicts?
I am probably making a ton of mistakes as I've not worked in Django in close to a decade. Any constructive feedback is appreciated in the comments. Most of the relationships are not coded in, as I'm not sure how they should look.
My specific question, however, is related the HexMap and Location objects.
When a HexMap is created, I want to create a set of empty Locations with a default name value matching a naming convention like HexMap.name + row + col and fill the row{} and col{} dicts of the new HexMap with them.
My instinct is that a Hexmap needs a Grid in order to be created, so I could create a nested loop inside HexMap with grid.num_rows as as the outer loop and num_cols as the inner loop. Each iteration through the inner loop builds a row of Locations as a dict. Each iteration of the outer loop adds that row to the rows{} dict. One thing I'm not certain about is how I'd get the grid.num_rows and grid.num_cols when the HexMap isn't even created yet, and doesn't have a grid associated.
Is this anywhere near the right way to do this in Django, or am I off the mark? Many of the answers and online tutorials seem to indicate that I need to look at signals, but I'm just not really clear on whether or not that's the case. Also, if so, I could use some similar examples to review. The Django docs are great and helpful, but when you're "new" to it all, it can be hard to parse and adopt the examples.
Found this question, which is the question I really should have asked in the first place. While a few years old, I think it's still the right answer and helped direct me to the appropriate documentation. Still not sure what my next step is, thinking I need some sane defaults and an override of the save method to use those if the instances of the other fields don't exist yet. I'm still working out wether or not I have a valid use case for signals here. I think that I will eventually when I add more apps to the project and actions in those apps need to interact with the models in this app.
I have some models in Django:
# models.py, simplified here
class Category(models.Model):
"""The category an inventory item belongs to. Examples: car, truck, airplane"""
name = models.CharField(max_length=255)
class UserInterestCategory(models.Model):
"""
How interested is a user in a given category. `interest` can be set by any method, maybe a neural network or something like that
"""
user = models.ForeignKey(User, on_delete=models.CASCADE) # user is the stock Django user
category = models.ForeignKey(Category, on_delete=models.CASCADE)
interest = models.PositiveIntegerField(default=0, validators=[MinValueValidator(0)])
class Item(models.Model):
"""This is a product that we have in stock, which we are trying to get a User to buy"""
model_number = models.CharField(max_length=40, default="New inventory item")
product_category = models.ForeignKey(Category, null=True, blank=True, on_delete=models.SET_NULL, verbose_name="Category")
I have a list view showing items, and I'm trying to sort by user_interest_category for the currently logged in user.
I have tried a couple different querysets and I'm not thrilled with them:
primary_queryset = Item.objects.all()
# this one works, and it's fast, but only finds items the users ALREADY has an interest in --
primary_queryset = primary_queryset.filter(product_category__userinterestcategory__user=self.request.user).annotate(
recommended = F('product_category__userinterestcategory__interest')
)
# this one works great but the baby jesus weeps at its slowness
# probably because we are iterating through every user, item, and userinterestcategory in the db
primary_queryset = primary_queryset.annotate(
recommended = Case(
When(product_category__userinterestcategory__user=self.request.user, then=F('product_category__userinterestcategory__interest')),
default=Value(0),
output_field=IntegerField(),
)
)
# this one works, but it's still a bit slow -- 2-3 seconds per query:
interest = Subquery(UserInterestCategory.objects.filter(category=OuterRef('product_category'), user=self.request.user).values('interest'))
primary_queryset = primary_queryset.annotate(interest)
The third method is workable, but it doesn't seem like the most efficient way to do things. Isn't there a better method than this?
Some background
I am considering rebuilding an existing Laravel website with Django. It's a website that allows sharing benchmark data from drone/UAV propulsion components. Some benchmarks are done while testing multiple motors and propellers at the same time, which means a battery would be under the load of multiple motors, but it also means the airflow from one propeller has an impact on the data captured on the other propeller. This means the data is physically coupled. Here is an example. Right now I am trying to structure this project to allow upcoming features, and to see if Django ORM is a good fit.
Simplified Django models
class Benchmark(models.Model):
title = models.CharField()
private = models.BooleanField(default=False)
hide_torque = models.BooleanField(default=False)
class AbstractComponent(models.Model):
brand = models.CharField()
name = models.CharField()
class Meta:
abstract = True
class Motor(AbstractComponent):
shaft_diameter_mm = models.FloatField()
class Propeller(AbstractComponent):
diameter_in = models.FloatField()
class Battery(AbstractComponent):
capacity_kwh = models.FloatField()
class Powertrain(models.Model):
benchmark = models.ForeignKey(Benchmark, on_delete=models.CASCADE, related_name='powertrains')
motor = models.ForeignKey(Motor, on_delete=models.CASCADE)
propeller = models.ForeignKey(Propeller, on_delete=models.CASCADE, blank=True, null=True)
battery = models.ForeignKey(Battery, on_delete=models.CASCADE, blank=True, null=True)
class DerivedDataManager(models.Manager):
def get_queryset(self):
return super().get_queryset()\
.annotate(electrical_power_w=F('voltage_v') * F('current_a'))\
.annotate(mechanical_power_w=F('torque_nm') * F('rotation_speed_rad_per_s'))\
.annotate(motor_efficiency=F('mechanical_power_w') / F('electrical_power_w'))
class DataSample(models.Model):
powertrain = models.ForeignKey(Powertrain, on_delete=models.CASCADE, related_name='data')
time_s = models.FloatField()
voltage_v = models.FloatField(blank=True, null=True)
current_a = models.FloatField(blank=True, null=True)
rotation_speed_rad_per_s = models.FloatField(blank=True, null=True)
torque_nm = models.FloatField(blank=True, null=True)
thrust_n = models.FloatField(blank=True, null=True)
objects = models.Manager()
derived = DerivedDataManager()
class Meta:
constraints = [
models.UniqueConstraint(fields=['powertrain', 'time_s'], name='unique temporal sample')
]
Question
I was able to add "derived" measurements, like electrical_power_w to each row of the data, but I have no clue on how can I add derived measurements that combines the data of multiple drive trains within the same benchmark:
Assuming 3 powertrains, each with their own voltage and current data, how can I do:
Total_power = powertrain1.power + powertrain2.power + powertrain3.power
for each individual timestamp (time_s)? A total power is only meaningul if the Sum is made on simultaneously taken samples.
Goal
Without loading all the database data in Django, I would eventually want to get the 5 top benchmarks in terms of maximum total power, taking into account some business logic:
benchmarks marked as private are automatically excluded (until auth comes in)
benchmarks that opt to hide the torque automatically make the torque data, along as all the derived mechanical power and motor efficiency values go to None.
I would like to recreate this table, but with extra columns appended, like 'maximum thrust', etc... This table is paginated from within the database itself.
Hmmmm, this is quite a tricky one to navigate. I am going to start by adding the following annotation model method:
from django.db.models import Sum
I guess then it would be a case of adding:
.annotate(total_power=Sum(electrical_power_w))
But I think the issue is that each row in your DerivedDataManager queryset represents one DataSample which in turn links to one Powertrain via the ForeignKey field.
It would be better to do this in the business logic layer, grouping by the powertrain's UUID (you need to add this to your Powertrain model - see https://docs.djangoproject.com/en/3.0/ref/models/fields/#uuidfield for details of how to use this. Then, because you have grouped by, you can then apply the Sum annotation to the queryset.
So, I think you want to navigate down this path:
DataSample.objects.order_by(
'powertrain'
).aggregate(
total_price=Sum('electrical_power_w')
)
I have been trying to figure out the best way (or correct) way to set up models for our PIM/PriceModel app in Django.
Example models (stripped):
class ProductItem(models.Model):
"""
An actual item/product, what it all revolves around.
"""
part_number = models.CharField(unique=True, max_length=50, help_text='')
internal_part_number = models.CharField(primary_key=True, max_length=50, help_text='') # prefilled by partnumber
type = models.ForeignKey('Type', null=True, on_delete=models.SET_NULL)
attributes = JSONField() # Or another dynamic field
# and more ....
class Type(models.Model):
"""
Product type i.e. camera-dome, camera-bullet, pir, etc.
"""
pass
class Segment(models.Model):
"""
A segment of the company like Industry, Retail, Guarding etc.
"""
pass
class ProductCategory(models.Model):
"""
Supposedly a child category of TopCategory.
"""
pass
class InstallPrice(models.Model):
"""
Product item installation prices based on Type, Product Category and Segment.
"""
install_price = models.DecimalField(max_digits=8, decimal_places=2, help_text='')
type = models.ForeignKey('Type', null=True, on_delete=models.SET_NULL)
product_category = models.ForeignKey('ProductCategory', null=True, on_delete=models.SET_NULL)
segment = models.ForeignKey('Segment', null=True, on_delete=models.SET_NULL)
Take a look at "attributes = HStoreField(db_index=True)" in the ProductItem model.
The main thing i need to store in a product item is attributes like how many inputs/outputs/connection-options does it have. I need to store this for testing products against each other further down the line in the price-model app. This to make sure you have the right amount of products with matching attributes like inputs or outputs. I also need the User/Admin to be able to add this attributes dynamically so the app becomes self sustainable and not requires a migration id there is a new attribute I dont yet know about.
As I could not figure out a reasonable model configuration I ended up looking at postgres specific fields. This is not a must!
ideally when selecting type in the admin section i would like a "preset" of attributes to be presented based on the type.
Attributes could be:
inputs # number
outputs # number
channels # number
analysis # Boolean
Is this achievable? Any suggestions are welcome as I have limited Data Base experience. I need help figuring out the models.
I've two Django models:
class Person(CommonModel):
""" Person """
person_type = models.ForeignKey(PersonType)
ciam_id = models.PositiveIntegerField() # ForeignKey('ciam.person_id')
first_name = models.CharField(max_length=50)
last_name = models.CharField(max_length=50)
middle_name = models.CharField(max_length=50)
empl_id = models.CharField(max_length=8)
pcn = models.CharField(max_length=50)
...
and
class PositionHierarchy(CommonModel):
""" Position Hierarchy """
pcn = models.CharField(max_length=50)
title = models.CharField(max_length=100)
level = models.CharField(max_length=25)
reports_to_pcn = models.CharField(max_length=50, db_index=True) # FK?
Unfortunately, for reasons beyond my control (and beyond reason), the only way to tell if a person is a manager or not is to look at Person.pcn, use that look up their PositionHierarchy by pcn, and then look at their level in PositionHierarchy. Similarly, the only way to find out their manager's name (which I need a lot) is to use their Person.pcn, look up their PositionHierarchy by pcn, look up their manager's PositionHierarchy by reports_to_pcn, then use that pcn to look up the manager's Person record. And none of these PCNs are allowed to be replaced by a Foreign Key to the PositionHeirarchy model. What a convoluted mess, right?
So I need to be able to quickly look up a person, and see their level and manager name.
The solution is a custom manager based on this blog post:
class PersonWithManagerManager(CommonManager):
""" Manager to get a query that adds the level and manager name
to the person record """
def get_query_set(self):
qs = super(PersonWithManagerManager, self).get_query_set()
cp_tab = qs.query.get_initial_alias()
# Join to the PCN table to find the position
pcn_tab = qs.query.join(
(
cp_tab,
PositionHierarchy._meta.db_table,
'pcn',
'pcn'
), promote=True)
# Find the manager's PCN
man_pcn_tab = qs.query.join(
(
pcn_tab,
PositionHierarchy._meta.db_table,
'reports_to_pcn',
'pcn'
), promote=True)
# Find the manager's person record
man_per_tab = qs.query.join(
(
man_pcn_tab,
'cart_person',
'pcn',
'pcn'
), promote=True)
return qs.extra(
select={
'level': '%s.level' % pcn_tab,
'manager_last_name': '%s.last_name' % man_per_tab,
'manager_first_name': '%s.first_name' % man_per_tab,
}
)
Now I can do print Person.extra_objects.filter(last_name='Tomblin').manager_last_name and get my manager's last name. One thing I can't do yet is filter on them.