用 FastAPI 理解 MVCS
Model-Controller-Service (MCS) 软件架构模式是 MVCS 在前后端分离的模式下,去掉前端负责的 View, 关注于后端逻辑而产生的。在这种架构下,后端的代码被分成了三部分,每部分都有自己的目标。
- 控制层 (Controller):负责定义路由,接收请求和返回响应,将请求转发给业务逻辑层进行处理。
- 业务逻辑层 (Service):负责处理业务逻辑和规则,定义各种业务操作的接口。
- 数据访问层 (Models):负责与数据库的交互,包括数据持久化和查询等操作。
假设我们在后端进行一个语音转文本的操作,首先由客户端发起请求,可能是用户进行了点击页面上一个按钮的操作,此时:
- 客户端发送一个 HTTP 请求
- Controller 接收请求并决定这一请求应当被哪个 Service 处理
- Service 处理具体的业务逻辑,例如使用机器学习 API 将音频转为文本
- Model 将转录好的文本存储到数据库中
Model
定义
模型层是应用程序的核心数据结构。它充当我们程序中使用的对象的蓝图。这些对象主要指数据库表模式 (schema),我们在其中设置了数据库中项目应如何呈现的预定义规则,比如哪些字段需要填充等。
模型层还负责与数据库交互,以实现应用程序内的任何业务逻辑。这可能意味着为其他层提供接口,以便获取或插入数据库中的数据。在现代应用程序中,开发人员可能会使用ORM(对象关系映射器)来简化模型层与数据库之间的交互。
实现
假设我们需要将转录结果存储在数据库中,我们可以定义一个转录对象,首先在 src/models
中建立一个 transcription.py
,在其中写如下代码:
# src/models/transcription.py
from src.utils.db import Base
from src.models.base import DBBase
from src.models.users import User
from sqlalchemy import (
Column,
UUID,
String,
Float,
Boolean,
ForeignKey,
)
class Transcription(Base, DBBase):
__tablename__ = "transcription"
## Foreign Relationships
# Foreign key to User
owner_id = Column(UUID, ForeignKey(User.id), nullable=False)
# NOTE in case User gets a `transcriptions` field
# owner = relationship("User", back_populates="transcriptions")
title = Column(String(255), nullable=False, unique=False)
tags = Column(ARRAY(String), nullable=True, unique=False)
chunks = relationship("TranscriptionChunk", back_populates="transcription_parent")
duration = Column(Float(precision=1), default=0, nullable=False)
在上述代码片段中,我们定义了一个继承自 Base
和 DBBase
的 Transcription
模型。其中,Base
只是一个用于构建基类的工厂函数。DBBase
是一个自定义基类模型,包含一些通用字段,如 "id"、"created_at"、"updated_at" 和 "is_deleted"。
我们定义了 Transcription
模型,该模型具有一个外键 owner_id
,以及 title
、tags
、chunks
和 duration
等字段。chunks
字段指的是 TranscriptionChunk
,它是 Transcription
的一个小部分(例如 5 个单词长),这些小部分组合在一起构成完整的 Transcription
对象。我们在下面按照 TranscriptionChunk
模型定义了这些 chunks
。
class TranscriptionChunk(Base, DBBase):
__tablename__ = "transcription_chunk"
# Extra fields
duration = Column(Float(precision=1), default=0, nullable=False)
is_edited = Column(Boolean, default=False)
## Foreign Relationships
# Foreign key to Transcription
transcription_id = Column(UUID, ForeignKey(Transcription.id), nullable=False)
# One to many relationship with Transcription
transcription_parent = relationship("Transcription", back_populates="chunks")
Controller
定义
控制器层(Controller Layer)充当程序的大脑。它负责协调为用户执行的业务操作。在 FastAPI 中,控制器也被称为路由器(routers)。它们是带有路径操作装饰器(Path Operation Decorators)的 Python 函数,用于将它们路由到应用程序中的特定端点。
@app.get("/")
def root():
return "Hello World!"
当用户触发一个 GET 请求的时候,上面这个函数就会返回 "Hello World".
实现
在语音转录的应用中,我们首先建立一个 src/controllers/transcription.py
文件,然后在主文件 src/main.py
中添加如下内容:
# src/main.py
from fastapi import FastAPI
from src.controllers import transcription
# Create FastAPI server
app = FastAPI()
# Include routing to access domain-specific controllers
app.include_router(transcription.transcription_router)
之后,在刚刚创建的文件中添加如下内容:
# src/controllers/transcription.py
import http
from fastapi import APIRouter
from botocore.exceptions import ClientError
from src.utils.settings import AWS_BUCKET_NAME
from src.schemas.base import GenericResponseModel # 引入 Model
from src.schemas.transcription import (
TranscribeAudioRequestSchema,
PollTranscriptionRequestSchema
)
from src.services.transcription import TranscriptionService # 引入 service
...
return GenericResponseModel(
status_code=http.HTTPStatus.CREATED,
message="Successfully created audio transcription",
error="",
data=response,
)
...
上面的函数是一个更复杂的控制器示例,适用于 FastAPI 后端应用程序。它使用 transcription_router.post("/create", ...)
装饰器定义为 /v1/transcription/create
URL 的 POST 端点。每当客户端向该 URL 发送 POST 请求时,都会调用该函数,通过 Service 层执行必要的逻辑,并将适当的值返回给客户端。
在这个示例中,我们看到了一些导入的模块,例如 TranscribeAudioRequestSchema
、GenericResponseModel
和 TranscriptionService
。它们分别用于定义控制器的输入、输出和所使用的操作。现在,我们需要知道这些模块是程序的一部分,并在控制器层中将它们组合在一起。
Service
定义
我们还有服务层(Service layer),它充当我们应用程序业务逻辑的执行者。服务层是API(控制器)和数据(模型)层之间的中间角色。它可能用于执行计算、调用外部API等任务。服务层保证了控制层不需要直接处理复杂的业务逻辑和数据访问.
与控制器和数据层的关系:服务层位于控制器层和数据层之间。控制器层将用户请求传递给服务层,服务层处理业务逻辑后,与数据层交互来获取或存储数据,然后将结果返回给控制器层。
实现
在 mdoel 层中的使用如上面所示:
# src/controllers/transcription.py
response = await service.transcribe_file(
transcribe_client=transcribe_client,
job_name=job_name,
file_uri=file_uri,
file_format=file_format,
language_code=language_code
)
在 services 中定义的格式如下面所示:
# src/services/transcription.py
from fastapi import FastAPI
from src.controllers import transcription
# Create Transcription Service
class TranscriptionService:
async def transcribe_file(self, transcribe_client: any,
job_name: str, file_uri: str,
file_format: str, language_code = "id-ID"):
try:
...
# Store to database
await self.store_transcription(item=job_result)
...
return job_result
...
def poll_transcription_job(...):
...
我们看到 transcribe_file
方法执行了完成特定任务所需的大部分逻辑,即使用 AWS 机器学习服务对音频文件进行转录。该方法依赖于类中定义的其他 TranscriptionService
方法,例如 poll_transcription_job()
和 store_transcription()
方法。像 store_transcription()
这样的服务方法可能通过执行 ORM 操作与 model 层进行交互.
通过将服务逻辑与模型和控制器解耦,我们可以在开发应用程序的各个部分时不用担心其他部分会出现问题。因为应用程序被分成了小而定义明确的部分,实施优化变得更加简单。我们可以在不需要花费数小时寻找与其他组件连接的情况下,修改这些小部分。
总结
我们已经了解了如何在 FastAPI 后端应用程序中实现模型-控制器-服务(Model-Controller-Service)模式。随着产品复杂度的增加,导航、维护和扩展代码库变得越来越具有挑战性。为了提高开发效率,我们需要通过使用这种模式来给代码的各个部分之间解耦。