Django抽象基类模型场景下URL、视图与模板的正确使用及优化方法
Django重复视图与路由的DRY优化方案
问题背景
现有Django项目中Contact类模型继承自同一基类,对应的视图、路由代码重复度极高,违反DRY原则,使用版本为Django 3.2.9 + Python 3.9.6。
原有代码结构如下:
模型代码(models/contact.py)
class BaseContact(AbstractUser): other_name = models.CharField(max_length=50, null=True, blank=True) contact_number = models.CharField(max_length=10, null=True, blank=True) email = models.EmailField(unique=True) postal_address = models.ForeignKey(location.Address, on_delete=models.CASCADE, null=True, blank=True) tax_id = models.CharField(max_length=11, null=True, blank=True) USERNAME_FIELD = 'email' REQUIRED_FIELDS = ['first_name'] class Meta: default_related_name = 'base' verbose_name = 'Contact' verbose_name_plural = 'Contacts' def get_absolute_url(self): return reverse("%s:detail-%s" % (str(type(self).__name__.lower()), str(type(self).__name__.lower())), kwargs={"pk": self.pk}) class Customer(BaseContact): class Meta: default_related_name = 'customer' class Supplier(BaseContact): class Meta: default_related_name = 'supplier' class Employee(BaseContact): class Meta: default_related_name = 'employee' verbose_name = 'Employee' verbose_name_plural = 'Employees'
原有路由代码(urls/contact.py)
from django.urls import path from main.views.contact import UpdateEmployeeView, DetailEmployeeView, CreateEmployeeView, DeleteEmployeeView from main.views.contact import UpdateSupplierView, DetailSupplierView, CreateSupplierView, DeleteSupplierView from main.views.contact import UpdateCustomerView, DetailCustomerView, CreateCustomerView, DeleteCustomerView urlpatterns = [ path('employee/create/', CreateEmployeeView.as_view(), name='create-employee'), path('employee/detail/<int:pk>', DetailEmployeeView.as_view(), name='detail-employee'), path('employee/update/<int:pk>', UpdateEmployeeView.as_view(), name='update-employee'), path('employee/delete/<int:pk>', DeleteEmployeeView.as_view(), name='delete-employee'), path('supplier/create/', CreateSupplierView.as_view(), name='create-supplier'), path('supplier/detail/<int:pk>', DetailSupplierView.as_view(), name='detail-supplier'), path('supplier/update/<int:pk>', UpdateSupplierView.as_view(), name='update-supplier'), path('supplier/delete/<int:pk>', DeleteSupplierView.as_view(), name='delete-supplier'), path('customer/create/', CreateCustomerView.as_view(), name='create-customer'), path('customer/detail/<int:pk>', DetailCustomerView.as_view(), name='detail-customer'), path('customer/update/<int:pk>', UpdateCustomerView.as_view(), name='update-customer'), path('customer/delete/<int:pk>', DeleteCustomerView.as_view(), name='delete-customer'), ]
原有视图代码(views/contact.py)
class CreateEmployeeView(CreateView): model = Employee template_name_suffix = '_create_form' fields = ['__all__'] class DetailEmployeeView(DetailView): model = Employee class UpdateEmployeeView(UpdateView): model = Employee form_class = UpdateEmployeeForm template_name_suffix = '_update_form' class DeleteEmployeeView(DeleteView): model = Employee success_url = reverse_lazy('home') class CreateSupplierView(CreateView): model = Supplier template_name_suffix = '_create_form' fields = ['__all__'] class DetailSupplierView(DetailView): model = Supplier class UpdateSupplierView(UpdateView): model = Supplier form_class = UpdateSupplierForm template_name_suffix = '_update_form' class DeleteSupplierView(DeleteView): model = Supplier success_url = reverse_lazy('home') class CreateCustomerView(CreateView): model = Customer template_name_suffix = '_create_form' fields = ['__all__'] class DetailCustomerView(DetailView): model = Customer class UpdateCustomerView(UpdateView): model = Customer form_class = UpdateCustomerForm template_name_suffix = '_update_form' class DeleteCustomerView(DeleteView): model = Customer success_url = reverse_lazy('home')
优化方案
第一步:优化视图层代码
通过封装基础通用视图类,消除重复的属性和方法定义:
# 优化后的views/contact.py代码 from django.views.generic import CreateView, DetailView, UpdateView, DeleteView from django.urls import reverse_lazy from .models import Employee, Supplier, Customer from .forms import UpdateEmployeeForm, UpdateSupplierForm, UpdateCustomerForm # 封装通用基础视图 class BaseContactCreateView(CreateView): template_name_suffix = '_create_form' fields = '__all__' class BaseContactDetailView(DetailView): pass class BaseContactUpdateView(UpdateView): template_name_suffix = '_update_form' class BaseContactDeleteView(DeleteView): success_url = reverse_lazy('home') # 各模型对应视图仅需指定model即可,无需重复定义属性 class CreateEmployeeView(BaseContactCreateView): model = Employee class DetailEmployeeView(BaseContactDetailView): model = Employee class UpdateEmployeeView(BaseContactUpdateView): model = Employee form_class = UpdateEmployeeForm class DeleteEmployeeView(BaseContactDeleteView): model = Employee class CreateSupplierView(BaseContactCreateView): model = Supplier class DetailSupplierView(BaseContactDetailView): model = Supplier class UpdateSupplierView(BaseContactUpdateView): model = Supplier form_class = UpdateSupplierForm class DeleteSupplierView(BaseContactDeleteView): model = Supplier class CreateCustomerView(BaseContactCreateView): model = Customer class DetailCustomerView(BaseContactDetailView): model = Customer class UpdateCustomerView(BaseContactUpdateView): model = Customer form_class = UpdateCustomerForm class DeleteCustomerView(BaseContactDeleteView): model = Customer
如果Form类命名严格遵循Update{模型名}Form的规则,还可以进一步简化Update视图的form_class定义,通过动态获取省略每个子类的form_class指定:
class BaseContactUpdateView(UpdateView): template_name_suffix = '_update_form' def get_form_class(self): return globals()[f'Update{self.model.__name__}Form'] # 后续Update子类无需再指定form_class class UpdateEmployeeView(BaseContactUpdateView): model = Employee class UpdateSupplierView(BaseContactUpdateView): model = Supplier class UpdateCustomerView(BaseContactUpdateView): model = Customer
第二步:优化路由层代码
通过循环动态生成路由,无需手动编写12条重复的path规则:
# 优化后的urls/contact.py代码 from django.urls import path from main.views.contact import * CONTACT_TYPES = ['employee', 'supplier', 'customer'] # 配置操作映射:路由后缀、视图类前缀、路由name前缀 OPERATIONS = [ ('create/', 'Create', 'create'), ('detail/<int:pk>', 'Detail', 'detail'), ('update/<int:pk>', 'Update', 'update'), ('delete/<int:pk>', 'Delete', 'delete'), ] urlpatterns = [] for contact_type in CONTACT_TYPES: for path_suffix, view_prefix, name_prefix in OPERATIONS: view_cls = globals()[f'{view_prefix}{contact_type.capitalize()}View'] urlpatterns.append( path(f'{contact_type}/{path_suffix}', view_cls.as_view(), name=f'{name_prefix}-{contact_type}') )
模板适配
原有模板命名规则完全适配优化后的代码,无需修改,模板后缀遵循{模型名}_{操作}_form.html的约定即可正常渲染。
补充:模型方法优化
原有get_absolute_url方法可以改用f-string简化写法,可读性更强:
def get_absolute_url(self): model_name = type(self).__name__.lower() return reverse(f"{model_name}:detail-{model_name}", kwargs={"pk": self.pk})
后续扩展
如果后续新增其他继承自BaseContact的模型,仅需完成3步即可:
- 在models中定义新的模型子类
- 在views中新增4行对应视图类(仅指定model)
- 在urls的CONTACT_TYPES列表中新增模型小写名称
无需重复编写大量重复逻辑。
内容的提问来源于stack exchange,提问作者Rishu
相关产品推荐
相关产品推荐

