【Day 10】Repository 设计模式(Python)

前言

Repository 设计模式主要是要分离商业逻辑与资料存取的逻辑,希望开发者专注在商业逻辑的设计,不必担心如何与资料库介接。
https://ithelp.ithome.com.tw/upload/images/20211023/20001976LDnzoo5rsg.png
图一. DDD 分层

另外,领域驱动设计(DDD)强调领域物件(Domain Object)的重要性,资料库的存取逻辑应在领域物件类别内实践,微软的DDD文件指引主张一个Aggregate应该定义一个Repository,个别管理Aggregate内所有的实体(Entity),架构如下图。
https://ithelp.ithome.com.tw/upload/images/20211023/20001976DUJwgWsbh1.png
图二. Aggregate 的 Repository 设计模式,图片来源:微软的DDD文件指引

上图的Data Tier对应ORM模型,透过模型物件存取资料库,而Repository就架在这一层之上,进行相关的 Domain Object 操作,一个Domain Object通常会包含多个资料表的操作。例如输入一张订单,至少包括两个资料表--表头(Order)、表身(Order details),如下图,表头包括客户名称、送货地址、订单日期、交货日期等,表身包括多笔资料,每一笔含商品名称、单价、数量、折扣等。
https://ithelp.ithome.com.tw/upload/images/20211023/20001976cy1hxeQfqb.png
图三. 订单样本,图片来源:手写统一发票计算机 - 工具邦

以下我们就以Django套件来实践Repository设计模式。

实践

Django 安装非常简单,执行下列指令:

pip install django

测试专案可至这里下载,专案目录为test1,资料库为db.sqlite3,预设为SQLite资料库。

Django要求每个资料表都要建立一个对应的类别,例如客户资料表:

from django.db import models

class Customer(models.Model):
    customer_id = models.CharField(max_length=20)
    customer_name = models.CharField(max_length=100)
    contact = models.CharField(max_length=30, blank=True, null=True)
    address = models.CharField(max_length=120, blank=True, null=True)

每个应用系统大概都会有数十个,乃至数百个资料表,若要赋予每个资料表『新增/查询/更正/删除(CRUD)』功能,如果每个类别分别撰写,不仅费时费工,而且不易维护,因此,我们可以应用Repository设计模式,完成上述的功能,程序码如下,档名为repository.py:

class Repository:
    # 初始化,指定类别名称
    def __init__(self, class1):
        self.class1 = class1
    
    # 单笔新增
    def create(self, **dict1):
        obj = self.class1()
        for key in dict1.keys():
            setattr(obj, key, dict1[key])
        obj.save()
        return obj

    # 多笔新增
    def bulk_create(self, objs):
        return self.class1.objects.bulk_create(objs)

    # 存档(新增或更新)
    def save(self, obj):
        return obj.save()

    # 删除
    def delete(self, pk):
        return self.class1.objects.filter(pk=pk).delete()

    # 删除2
    def delete_object(self, obj):
        return obj.delete()

    # 单笔查询
    def get_one(self, pk):
        return self.class1.objects.get(pk=pk)
        
    # 多笔查询
    def get_many(self, **filter):
        return self.class1.objects.filter(**filter)

测试

使用 Django shell 测试每一个功能,先在专案目录test1,启动 shell:

python manage.py shell

单笔查询

from sales.models import *
from repository import *

# 指定类别名称
repo = Repository(Customer)

# 单笔查询,query by pk
cust = repo.get_one(2)
print(cust.customer_name)

顺利取得 pk=2 的资料列,显示customer_00002。pk为资料表主键(Primary Key),为自动给号。

多笔查询

查询customer_name含0000,而且id>=5。

obj_list = repo.get_many(customer_name__contains='0000', id__gte=5)
for obj in obj_list:
    print(obj.customer_name)

显示5笔资料。
customer_00005
customer_00006
customer_00007
customer_00008
customer_00009

单笔新增

i=11        
obj = Customer()
obj.customer_id=f'{i:05d}'
obj.customer_name=f'customer_{i:05d}'
obj.contact=f'contact_{i:05d}'
obj.address=f'address_{i:05d}'
repo.save(obj)
print(obj.id)

显示物件的pk=11。

单笔删除

repo.delete(11)

另一种单笔新增

i=12        
obj=repo.create(customer_id=f'{i:05d}',
    customer_name=f'customer_{i:05d}',
    contact=f'contact_{i:05d}',
    address=f'address_{i:05d}',
    )
print(obj.id)

显示物件的pk=12。

另一种单笔删除

repo.delete_object(obj)

更新

obj = repo.get_one(1)
obj.customer_id=f'XXX'
repo.save(obj)

pk=1的customer_id被改为'XXX',可再次查询求证。

多笔新增

一次新增十笔资料。

obj_list = []
for i in range(11, 21):
    obj = Customer()
    obj.customer_id=f'{i:05d}'
    obj.customer_name=f'customer_{i:05d}'
    obj.contact=f'contact_{i:05d}'
    obj.address=f'address_{i:05d}'
    obj_list.append(obj)
repo.bulk_create(obj_list)

为简化说明,repository.py 并未作例外控制及检查,读者可自行添加,例如添加一笔资料前先检查资料是否重覆。

多资料表处理

一个Domain Object通常会包含多个资料表的操作。例如输入一张订单,至少包括两个资料表 -- 表头(Order)、表身(Order_detail),必须作好交易管理(Transaction),Django 提供很好的机制,可参考官网说明

建立测试函数如下,可新增一笔订单,存档名称为 repository_do.py:

from sales.models import *
from repository import *
from django.db import transaction

# order save
@transaction.atomic
def create_order(cust):
    repo = Repository(Order)
    ord=repo.create(order_id='so_0001',
    customer_id=cust,
    order_date='20210801',
    required_date='20210901',
    )
    
    # 多笔新增
    repo_detail = Repository(Order_Detail)
    repo_product = Repository(Product)
    obj_list = []
    for i in range(1, 4):
        obj = Order_Detail()
        obj.order_id=ord
        obj.product_id=repo_product.get_one(i)
        obj.unit_price=100
        obj.quantity=5
        obj.discount=0
        obj_list.append(obj)
    repo_detail.bulk_create(obj_list)

其中 @transaction.atomic 可以控制交易,如果 create_order() 内有任何错误,系统会自动 rollback,全部资料均不会写入资料库。

测试程序码如下:

from sales.models import *
from repository import *
from repository_do import *

# 取得客户物件
repo = Repository(Customer)
cust = repo.get_one(2)

# 新增一笔订单,含表头(Order)、表身(Order_detail)
create_order(cust)

检查资料库,sales_order、sales_order_detail 确实都写入资料了。

结语

Repository 设计模式结合 ORM,可以大幅提高开发的速度,笔者曾经开发一个购物平台的前/後台,只花了两个月的时间,而且案主也非常满意,再结合 DDD 的设计概念,就更给力了。


<<:  【课程推荐】2021/11/6-11/7软件需求塑模与需求规格文件撰写实务班

>>:  用 Python 畅玩 Line bot - 21:LIFF(一)

【D21】修改食谱#2:根据市价,模拟小台改价

前言 昨天已经模拟出改价了,现在更进阶,使用小台的现价来改价。 参考网站:Futures 本日程序码...

Day 31 - Redux vs XState (英文原文改作)

本篇主要论述源自於 XState 作者本人在 stackoverflow 的解释,并辅以一点我的补充...

04 - Tmux - 终端机管理工具

在开发时,常常需要多个指令同时运作(例如一个启动前端专案、一个启动後端专案),因此会需要同时开启多个...

18.unity实例化(上)(Instantiate)

Instantiate实例化,用於将物件生成至场景中。(参考unity手册) 适合用在复制一样的物件...

D13 删除特定的使用者文件

已经先有测试资料了 来试试看删除文件的方法 doc_info/views.py 一样使用修饰器来验证...