測試平臺系列(79) 編寫Redis配置功能(下)

米洛丶發表於2021-11-22

大家好~我是米洛

我正在從0到1打造一個開源的介面測試平臺, 也在編寫一套與之對應的完整教程,希望大家多多支援。

歡迎關注我的公眾號測試開發坑貨,獲取最新文章教程!

回顧

上一節我們提出了優化Dao邏輯的想法,那今天就試著來兌現之,並運用到Redis配置管理的開發中去。

初步構思list方法

我們在dao/init.py新建類: Mapper,以後所有的dao類都繼承自它。

想想list需要什麼,一般需要,欄位引數是like還是等於這3個重要的資訊。

明白這個以後,我們的虛擬碼就好編寫了:

# 1. 獲取session
async with async_session() as session:
  condition = [model.deleted_at == 0]
  # 2. 根據引數裡的欄位,欄位值構造查詢條件
  DatabaseHelper.where(欄位,欄位值,condition)
  # 3. 查詢出結果,後續與原來的方式一致,就不寫了

可以看到,這邊除了需要上面的3個資訊以外,還需要try去寫入日誌,也就是說需要我們平時經常建立的log成員變數,還需要model這個類,否則你不知道改的是什麼表。

思考

我們平時的引數,都是key-value的形式,一旦value不為空,那麼說明我們要根據這個條件來查詢。

而model和log,我們可以通過類的裝飾器,由子類傳遞給父類(這裡會用到setattr和getattr)

class Mapper(object):
    log = None
    model = None

    @classmethod
    async def list_data(cls, **kwargs):
        try:
            async with async_session() as session:
                # 構造查詢條件,預設是資料未被刪除
                condition = [getattr(cls.model, "deleted_at") == 0]
                # 遍歷引數,當引數不為None的時候傳遞
                for k, v in kwargs.items():
                    # 判斷是否是like的情況 TODO: 這裡沒支援in查詢
                    like = v is not None and len(v) > 2 and v.startswith("%") and v.endswith("%")
                    # 如果是like模式,則使用Model.欄位.like 否則用 Model.欄位 等於
                    DatabaseHelper.where(v, getattr(cls.model, k).like(v) if like else getattr(cls.model, k) == v,
                                         condition)
                result = await session.execute(select(cls.model).where(*condition))
                return result.scalars().all()
        except Exception as e:
            # 這邊呼叫cls本身的log引數,寫入日誌+丟擲異常
            cls.log.error(f"獲取{cls.model}列表失敗, error: {e}")
            raise Exception(f"獲取資料失敗")

註釋寫的比較詳細,由於現在欄位是個變數,所以我們不能用model.欄位來取值,所以取而代之的是getattr(model, 欄位)。不熟悉的朋友可以去搜尋下getattr

這樣,一個粗略的list方法就寫好了,但是這個是不帶分頁的,所以我們還需要補充一個分頁的模式,其實也很簡單。

    @classmethod
    async def list_data_with_pagination(cls, page, size, **kwargs):
        try:
            async with async_session() as session:
                condition = [getattr(cls.model, "deleted_at") == 0]
                for k, v in kwargs.items():
                    like = v is not None and len(v) > 2 and v.startswith("%") and v.endswith("%")
                    sql = DatabaseHelper.where(v, getattr(cls.model, k).like(v) if like else getattr(cls.model, k) == v,
                                               condition)
                return await DatabaseHelper.pagination(page, size, session, sql)
        except Exception as e:
            cls.log.error(f"獲取{cls.model}列表失敗, error: {e}")
            raise Exception(f"獲取資料失敗")

基本上長的差不多,只是最後返回那裡,呼叫了pagination相關方法。

看看dao裝飾器

這也是為什麼要用classmethod而不是staticmethod的原因

我們很壞,把這個裝飾器套到子類上,並把model和log傳給父類。畢竟方法都是在呼叫父類的方法,父類無法直接拿到子類的資料

用的話,就是把model和log傳入dao引數

這樣就避免了在呼叫list方法的時候,還需要傳入model和log的尷尬情況。

完善其他方法

list搞定以後,其他的還會遠嗎?但還真的好像還有點問題,因為我們一般會有一些重名判斷,但沒關係,我們可以編寫一個query方法。

    @classmethod
    def query_wrapper(cls, **kwargs):
        condition = [getattr(cls.model, "deleted_at") == 0]
        # 遍歷引數,當引數不為None的時候傳遞
        for k, v in kwargs.items():
            # 判斷是否是like的情況 TODO: 這裡沒支援in查詢
            like = v is not None and len(v) > 2 and v.startswith("%") and v.endswith("%")
            # 如果是like模式,則使用Model.欄位.like 否則用 Model.欄位 等於
            DatabaseHelper.where(v, getattr(cls.model, k).like(v) if like else getattr(cls.model, k) == v,
                                 condition)
        return select(cls.model).where(*condition)

    @classmethod
    async def query_record(cls, **kwargs):
        try:
            async with async_session() as session:
                sql = cls.query_wrapper(**kwargs)
                result = await session.execute(sql)
                return result.scalars().first()
        except Exception as e:
            cls.log.error(f"查詢{cls.model}失敗, error: {e}")
            raise Exception(f"查詢資料失敗")

由於查詢方法太過於通用,所以抽成了query_wrapper方法。

  • 正常編寫insert介面
    @classmethod
    async def insert_record(cls, model):
        try:
            async with async_session() as session:
                async with session.begin():
                    session.add(model)
                    await session.flush()
                    session.expunge(model)
                    return model
        except Exception as e:
            cls.log.error(f"新增{cls.model}記錄失敗, error: {e}")
            raise Exception(f"新增記錄失敗")

這邊返回了model,如果不需要也可以不用,但我們還是給返回。

  • insert介面層

和以前一樣,先查,如果沒有再關掉。

但這樣會產生2個session,開->關->開->關

但也解耦了查詢和插入2個操作。

  • 編寫刪除和修改方法
    @classmethod
    async def update_record_by_id(cls, user, model, not_null=False):
        try:
            async with async_session() as session:
                async with session.begin():
                    query = cls.query_wrapper(id=model.id)
                    result = await session.execute(query)
                    original = result.scalars().first()
                    if original is None:
                        raise Exception("記錄不存在")
                    DatabaseHelper.update_model(original, model, user, not_null)
                    await session.flush()
                    session.expunge(original)
                    return original
        except Exception as e:
            cls.log.error(f"更新{cls.model}記錄失敗, error: {e}")
            raise Exception(f"更新記錄失敗")

    @classmethod
    async def delete_record_by_id(cls, user, id):
        """
        邏輯刪除
        :param user:
        :param id:
        :return:
        """
        try:
            async with async_session() as session:
                async with session.begin():
                    query = cls.query_wrapper(id=id)
                    result = await session.execute(query)
                    original = result.scalars().first()
                    if original is None:
                        raise Exception("記錄不存在")
                    DatabaseHelper.delete_model(original, user)
        except Exception as e:
            cls.log.error(f"刪除{cls.model}記錄失敗, error: {e}")
            raise Exception(f"刪除記錄失敗")

    @classmethod
    async def delete_by_id(cls, id):
        """
        物理刪除
        :param id:
        :return:
        """
        try:
            async with async_session() as session:
                async with session.begin():
                    query = cls.query_wrapper(id=id)
                    result = await session.execute(query)
                    original = result.scalars().first()
                    if original is None:
                        raise Exception("記錄不存在")
                    session.delete(original)
        except Exception as e:
            cls.log.error(f"邏輯刪除{cls.model}記錄失敗, error: {e}")
            raise Exception(f"刪除記錄失敗")

刪除這邊支援了物理刪除和邏輯刪除,當然我們一般是用軟刪除

完善其他介面

可以看到刪除和修改和以前是差不多的(也是內部進行資料是否存在判斷),這些步驟做完。redis的管理工作就可以順利進行了,接著我們需要為之編寫頁面咯!

由於是很基礎的表格頁面,所以我們不贅述。下一節我們會利用配置的redis連線編寫redisManager,管理我們的連線資料,為之後線上執行redis以及將它作為前置條件打下基礎。

相關文章