D E F I N I N G O U R P R O J E C T • Real world motivation • Car dealership administration system • New car sales, used car sales, servicing, repairs… • LOTS of deliberate naivety!
M O D E L S • Department, Employee, Customer • NewVehicle, UsedVehicle, OwnedVehicle • No core Vehicle model • Task • Many small models for specific processes
D E F I N E T H E L O O K U P class RegistrationPlateYearLookup(Lookup): lookup_name = 'year' def as_sql(self, compiler, connection): lhs_sql, lhs_args = self.process_lhs( compiler, connection, ) rhs1 = str(self.rhs)[-2:] rhs2 = str(int(rhs1) + 50)
D E F I N E T H E L O O K U P return ( ( "(%s ~ '^\w\w%s\w\w\w$'" " OR %s ~ '^\w\w%s\w\w\w$')" ) % ( lhs_sql, rhs1, lhs_sql, rhs2, ), list(lhs_args) + list(lhs_args), )
A D D I T I O N A L VA L I D AT I O N >>> UsedVehicle.objects.filter( ... registration_plate__year=27, ... ) Traceback (most recent call last): File "", line 1, in ValueError: Year "27" is not valid. Did you mean to query Chariot?
O R D E R C A R S B Y R E G I S T R AT I O N Y E A R • 17, 66, 16, 65, 15, 64, 14, …, 52, 02, 51, 01 • Z\d\d\d?\w\w\w, Y\d\d\d?\w\w\w, X\d\d\d?\w\w\w… • \w\w\w\d\d\d?Z, \w\w\w\d\d\d?Y, \w\w\w\d\d\d?X…
W E H AV E T H E T E C H N O L O G Y >>> UsedVehicle.objects.annotate( ... year=Case( When( registration_plate__regex='…', SubString(…), ), When(…), … ), ... ).order_by('year')
N I C E R A P I # Hire purchase vehicle.transaction.complete_purchase() vehicle.transaction.perform_credit_check() # Part exchange vehicle.transaction.complete_purchase() vehicle.transaction.process_part_exchange()
S E N D VA L U E S T O T H E D ATA B A S E class TransactionTypeField(CharField): … def get_prep_value(self): if value is None: return value return value.slug
G E T B A C K F R O M T H E D ATA B A S E class TransactionTypeField(CharField): … def from_db_value(self, value, expression, connection, context): if value is None: return value return TransactionDescriptor(value)
T H E D E S C R I P T O R class TransactionDescriptor(object): def __init__(self, value): self.value = value def __get__(self, instance, cls=None): cls = get_class(self.value) return cls(instance)
U S E I T >>> type(vehicle.transaction) HirePurchase >>> vehicle.transaction.vehicle is vehicle True >>> vehicle.transaction = PartExchange # Could be implemented via the __set__
W H Y B O T H E R • Won't need it for the first 1-2 logical chunks • Encapsulates complex, different processes applicable to instances of the same model • Testable • Keeps model class size free from large numbers of methods full of elifs
M O D E L class Task(models.Model): employee = models.ForeignKey(Employee) vehicle = RegistrationPlateField() action = models.CharField() notes = models.TextField() timestamp = models.DateTimeField()
A P I S department.employee_set.prefetch_related( PrefetchUserTasks(today_only=True), ) OwnedVehicle.objects.filter( service_date=today, ).prefetch_related( PrefetchVehicleTasks(), )
T H E U S E R C A S E def PrefetchUserTasks(Prefetch): def __init__(self, today_only=False): qs = Task.objects.all() if today_only: qs = qs.filter(…) super().__init__( 'prefetched_tasks', qs, 'task_set', )
T H E V E H I C L E C A S E • This one is harder! • Vehicle history may come from the time the vehicle was in different tables, and there is no foreign key • We look up by registration plate which is shared between those tables
S E T T I N G U P T H E R E L AT E D M A N A G E R >>> used_vehicle.task_set.order_by('-timestamp') [, , …] >>> owned_vehicle.task_set.order_by('-timestamp') [, , …]
T H E M A N A G E R class RelatedTaskManager(Manager): def get_queryset(self): plate = self.instance.registration_plate return self.model.objects.filter( registration_plate=plate, )
T H E D E S C R I P T O R class TasksDescriptor( ReverseManyToOneDescriptor): related_manager_cls = RelatedTaskManager def __init__(self): """Don't call super as we don't have a rel object""" pass
A D D I N G P R E F E T C H I N G class RelatedTaskManager(Manager): def get_prefetch_queryset( self, instances, queryset): • Instances is a concrete list of Vehicle objects • Queryset is the base queryset of Tasks to start from (e.g. Task.objects.all())
A D D I N G P R E F E T C H I N G class RelatedTaskManager(Manager): def get_prefetch_queryset( self, instances, queryset): plates = [ instance.registration_plate for instance in instances ] queryset = queryset.filter( registration_plate__in=plates, )
A D D I N G P R E F E T C H I N G return ( queryset, # Queryset of Tasks # Task value matcher lambda result: result.registration_plate, # Vehicle value matcher lambda inst: inst.registration_plate, False, # Many objects not one 'task_set', # cache name )
C U S T O M P R E F E T C H O B J E C T def PrefetchVehicleTasks(Prefetch): def __init__(self): qs = Task.objects.select_related( 'employee', ) super().__init__( 'prefetched_tasks', qs, 'task_set', )
B E A U T I F U L A P I S A R E P O S S I B L E • Expressions, lookups and transforms are a powerful collection of tools to create domain specific logic • Descriptors are magical • Prefetching can apply to any way you can connect the two objects together
T H E F U T U R E • Subquery expressions (Django 1.11+) • Functional indexes from expressions (Custom indexes Django 1.11+, functional 2.0+ hopefully) • Virtual fields resolving to an expression (TODO) • Lazily evaluated prefetching across multiple querysets (TODO)