Принятие шаблона репозитория для расширенной серверной разработки с помощью FastAPI

Принятие шаблона репозитория для расширенной серверной разработки с помощью FastAPI

10 февраля 2023 г.

В этой статье вы не узнаете, как настроить виртуальную среду и установить FastAPI для создания приложения для работы с задачами. Вместо этого он предложит новую перспективу, если у вас есть опыт использования FastAPI для создания серверных систем. В прошлом вы могли выполнять запросы к базе данных в своем представлении API или модуле бизнес-домена. Однако рекомендуется прекратить эту практику. Шаблон репозитория — лучший вариант, поскольку он способствует написанию более чистого кода и позволяет избежать потенциальных проблем. В этой статье я объясню причины использования этого шаблона проектирования и преимущества, которые он дает.

Введение

Шаблон репозитория — это интерфейс, в котором хранится логика доступа к данным. Проще говоря, прекрасная идея использования шаблона репозитория состоит в том, чтобы отделить уровень доступа к данным (базу данных) от уровня доступа к сервису (домена) приложения. Операции CRUD (создание, чтение, обновление, удаление и т. д.) выполняются с помощью методов, не беспокоясь о соединениях с базой данных, командах и т. д.

Ради этой статьи мы создадим простой сокращатель URL. Полный код проекта вы найдете на моем GitHub; вы должны внимательно следить за тем, насколько невероятно крут RP (шаблон репозитория).

В наших моделях у нас есть следующий код:

# Stdlib Imports
from datetime import datetime

# SQLAlchemy Imports
from sqlalchemy import Column, String, Integer, DateTime

# Own Imports
from config.database import Base, DATABASE_ENGINE


async def create_tables():
    Link.metadata.create_all(bind=DATABASE_ENGINE)


class Link(Base):
    __tablename__ = "links"

    id = Column(Integer, primary_key=True, index=True)
    original = Column(String)
    shortened = Column(String(4))
    date_created = Column(DateTime, default=datetime.now)
    date_modified = Column(DateTime, onupdate=datetime.now)

    def __str__(cls) -> str:
        """
        `__str__` is a special method that returns a string representation of an object.

        :param cls: The class object
        :return: The shortened version of the original link
        """
        return cls.shortened.__str__()

Для читателей, которые плохо знакомы с FastAPI, я быстро расскажу, что делают create_tables и Link. Асинхронный метод create_tables отвечает за создание таблиц базы данных определенного класса ссылок. Мы определяем необходимые поля (столбцы), которые мы хотим иметь в нашей таблице ссылок.

Следующим будет модуль с именем repository.py, здесь мы собираемся создать интерфейс, в котором хранится наша логика доступа к данным. У нас есть следующий код:

# Stdlib Imports
from typing import List

# SQLAlchemy Imports
from sqlalchemy.orm import Session

# Own Imports
from config.deps import get_db
from shortener.models import Link


class LinkRepository:
    """Repository responsible for performing operations (CRUD, etc) on links table."""

    def __init__(self) -> None:
        self.db: Session = get_db().__next__()

    async def create(self, original: str, shortened: str) -> Link:
        """
        This method is responsible for creating a new link object.

        :param original: original url link
        :param shortened: shortened url link

        :return: the link object
        """

        link = Link(original=original, shortened=shortened)

        self.db.add(link)
        self.db.commit()
        self.db.refresh(link)

        return link

    async def get(self, skip: int, end: int) -> List[Link]:
        """
        This method retrieves a list of links objects.

        :param skip: The number of links to skip
        :type skip: int

        :param end: The number of links to retrieve
        :type end: int

        :return: A list of link objects
        """

        links = self.db.query(Link).offset(skip).limit(end).all()
        return links

    async def get_code(self, code: str) -> Link:
        """
        This method retrieves a link object that matches the code.

        :param code: The shortened code of the link
        :type code: str

        :return: The link object
        """

        link = self.db.query(Link).filter_by(shortened=code).first()
        return link


link_repository = LinkRepository()

Чтобы начать использовать наш класс репозитория; нам нужно его инициализировать.

def __init__(self) -> None:
    self.db: Session = get_db().__next__()

Цель этого метода — инициализировать экземпляр класса путем создания сеанса базы данных и сохранения его в переменной экземпляра self.db. При этом сеанс базы данных будет создан и сохранен в экземпляре класса при создании экземпляра класса и будет доступен для использования в течение всего времени существования экземпляра.

get_db — это функция, которая отвечает за создание сеанса базы данных, его выход и откат транзакции. Если во время работы с базой данных возникает исключение, функция перехватывает исключение и вызывает db .rollback() для отката транзакции. Это гарантирует, что любые изменения, внесенные в базу данных во время транзакции, будут отменены в случае ошибки; и наконец блок функции закроет сеанс, используя метод db.close(), независимо от того, было ли возбуждено исключение или нет.

Код этой функции выглядит следующим образом:

# Own Imports
from config.database import SessionLocal


def get_db():
    """
    This function creates a database session,
    yield it to the get_db function, rollback the transaction
    if there's an exception and then finally closes the session.

    Yields:
        db: scoped database session
    """

    db = SessionLocal()
    try:
        yield db
    except Exception:
        db.rollback()
    finally:
        db.close()

Метод create отвечает за создание нового объекта Link с заданной исходной ссылкой URL и сокращенным кодом. После чего сохраняет его в переменной link. Затем метод добавляет ссылку в базу данных, вызывая self.db.add(link).

Затем метод фиксирует изменения в базе данных, вызывая self.db.commit(). Затем вызывается метод refresh с объектом link в качестве аргумента для обновления состояния ссылки в базе данных. Наконец, метод возвращает вызывающей стороне объект link.

Метод get отвечает за получение списка объектов Link из базы данных. Метод создает запрос для извлечения объектов Link из базы данных с помощью оператора self.db.query(Link). Затем в запросе вызывается метод offset, чтобы указать количество ссылок, которые нужно пропустить, и метод limit, чтобы указать количество ссылок, которые необходимо получить. Наконец, в запросе вызывается метод all для получения всех ссылок, соответствующих критериям.

get_code отвечает за получение одного объекта Link. Метод создает запрос для извлечения объекта Link из базы данных с помощью оператора self.db.query(Link). Затем в запросе вызывается метод filter_by, чтобы указать критерии получения ссылки, в данном случае сокращенного кода. Наконец, в запросе вызывается метод first для получения первой ссылки, соответствующей критериям.

Таким образом, методы обеспечивают удобный и асинхронный способ выполнения определенных действий в базе данных, будь то; создание и сохранение новых объектов Link в базе данных, или получение списка объектов Link из базы данных, или получение одного объекта Link во время убедиться, что сеанс базы данных правильно управляется и транзакция фиксируется, если нет исключений.

Теперь, когда вы понимаете, что происходит, давайте перейдем к уровню доступа к нашим сервисам; здесь хранится логика домена. У нас есть следующий код:

# Stdlib Imports
import random
import string

# FastAPI Imports
from fastapi.responses import RedirectResponse

# Own Imports
from shortener.repository import link_repository, Link


async def shorten_link() -> str:
    """
    This function returns a random string of 4 characters.

    :return: A string of 4 random letters.
    """

    shrt_str = "".join(random.choice(string.ascii_letters) for i in range(4))
    return shrt_str


async def create_shortened_link(original: str) -> Link:
    """
    This function creates a shortened link for the given original link.

    :param original: str - the original link that we want to shorten
    :type original: str

    :return: A Link object
    """

    shortened_link = await shorten_link()
    link = await link_repository.create(original, shortened_link)
    return link


async def redirect_to_original_link(code: str) -> str:
    """
    This function takes a code and returns the original link.

    :param code: str - the code that was generated by the shortener
    :type code: str

    :return: redirect to original link
    """

    link = await link_repository.get_code(code)
    return RedirectResponse(link.original)

Если вы посмотрите на функции create_shortened_link и redirect_to_original_link, мы получим доступ к методам create и get_code в нашем интерфейс репозитория. Мы отказались от написания запросов к базе данных на нашем сервисном уровне, что сделало наш код чистым и быстрым для отладки.

Преимущества использования шаблона репозитория

Использование шаблонов проектирования репозитория дает несколько преимуществ, в том числе:

  1. Облегчение тестирования логики вашего приложения
  2. Уменьшение количества повторяющихся операций с базой данных
  3. Централизованное управление уровнем базы данных, позволяющее легко внедрять политики доступа
  4. Возможность определять надежные аннотации для объектов домена.

Заключительные мысли

В этой статье мы объяснили преимущества использования шаблона проектирования репозитория при разработке программного обеспечения. Шаблон репозитория отделяет уровень доступа к данным от уровня доступа к службе в приложении. Это приводит к более чистой и организованной кодовой базе, а также уменьшает количество дублирующихся операций с базой данных.

Мы использовали простой сокращатель URL-адресов в качестве примера, чтобы проиллюстрировать реализацию шаблона репозитория. Код демонстрирует, как создать таблицу базы данных и как сохранить логику доступа к данным в классе репозитория. Полный код этого примера можно найти здесь.


Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE