iT邦幫忙

2021 iThome 鐵人賽

1
Software Development

淺談中台架構、DDD與Python實踐系列 第 10

【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 的設計概念,就更給力了。


上一篇
【Day 09】配接器 設計模式(Python)
系列文
淺談中台架構、DDD與Python實踐10

尚未有邦友留言

立即登入留言