После статьи прошлой недели у нас есть начальный API для нашего билетного сервиса. Теперь мы хотим взять этот API и начать выяснять, как будет выглядеть наш внутренний сервер.

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

Многие наши конечные точки будут выполнять какие-либо вызовы базы данных. Мы хотим избежать большого количества шаблонов в каждой конечной точке для подключения к базе данных. Чтобы решить эту проблему, мы собираемся создать монадный класс, который обертывает все наши вызовы базы данных. Добавление этого уровня абстрагирует фактическую механику базы данных от нашего сервера. Это упростило бы замену реализаций базы данных, если бы мы захотели!

Если вы когда-нибудь захотите поменять местами библиотеки баз данных, вам нужно сначала изучить пару из них! Загрузите наш Контрольный список производства для некоторых идей. Если вы хотите получить более подробное руководство только по Persistent, просмотрите часть 1 нашей серии Real World Haskell Series!

Монада для вызовов базы данных

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

data KeyVal a = KeyVal
  { kvKey :: Int64
  , kvVal :: a
  }

Но для конкретных функций запроса мы пока будем использовать Persistent with SQL. И большинство наших вызовов базы данных будут использовать монаду SqlPersistT. Например, это будет тип функции для создания пользователя:

-- Take user data and a password as inputs, produce the User ID
createUserSql :: (User, Text) -> SqlPersistT (LoggingT IO) Int64

Но если эта функция — наш единственный способ выполнить задачу, у нас будет много запахов кода. Нам всегда нужно будет явно подниматься в SqlPersistT, чтобы вызвать эту функцию. Это также означало бы разоблачение IO во всех этих местах. Мы хотели бы избежать этого.

Вместо этого мы можем обернуть этот и другие наши вызовы базы данных в монадный класс! Давайте рассмотрим 10 различных вызовов базы данных, которые нам нужно сделать с нашим API в его текущем состоянии.

  1. Создать пользователя
  2. Попытка авторизоваться пользователем
  3. Получить текущие события в продаже
  4. Посмотрите, на какие мероприятия пользователь купил билеты
  5. Создать событие
  6. Создать билеты на мероприятие
  7. Просмотреть все события, созданные администратором
  8. Получить сводку по конкретному событию
  9. Удалить событие
  10. Купить билет на мероприятие

Мы создадим класс монады с именем MonadDatabase. У него будет одна функция для каждого из этих разных вызовов.

class (Monad m) => MonadDatabase m where
  createUser :: (User, Text) -> m Int64
  loginUser :: LoginInfo -> m (Either LoginError Int64)
  getCurrentEvents :: m [KeyVal Event]
  getPurchasedEvents :: Int64 -> m [(Event, [EventTicket])]
  createEvent :: Event -> m Int64
  createTicketsForEvent ::
    Int64 -> [(Text, Double, Int64)] -> m [Int64]
  getCreatedEvents :: Int64 -> m [KeyVal Event]
  getEventSummary :: Int64 -> m EventSummary
  deleteEvent :: Int64 -> m ()
  purchaseTicket :: Int64 -> Int64 -> m ()

Идея заключается в том, что нам не всегда нужно использовать SqlPersistT. Мы можем сделать любую монаду экземпляром MonadDatabase и вызывать эти функции.

Но для начала, конечно, нам нужен экземпляр для SqlPersistT. Мы будем использовать написанные нами функции прямого запроса, использующие библиотеку Persistent.

instance (MonadLogger m, MonadIO m) => MonadDatabase (SqlPersistT m) where
  createUser = createUserSql
  loginUser = loginUserSql
  ...

Теперь будет легче замаскировать нашу конкретную реализацию базы данных. Это, в свою очередь, облегчит изменение библиотек, если мы захотим!

Серверная монада

Теперь, чтобы начать процесс «маскировки» наших эффектов, мы создадим новую монаду. Мы будем использовать это в наших функциях обработчика сервера. На данный момент он будет обертывать только SqlPersistT, но позже мы добавим к нему дополнительные эффекты:

newtype ServerMonad a = ServerMonad
  (SqlPersistT (LoggingT IO) a)
  deriving (Functor, Applicative, Monad)

Теперь нашей целью должно стать создание экземпляра MonadDatabase для этой монады. Таким образом, мы можем использовать наши функции базы данных без какого-либо подъема, например:

createEventFromSummary :: EventSummary -> ServerMonad Int64
createEventFromSummary (EventSummary event tiers) = do
  eventId <- createEvent event
  void $ createTicketsForEvent eventId tiers
  return eventId

Чтобы начать этот процесс, нам нужна простая функция, которая превращает действие SQL в действие ServerMonad. Сейчас это тривиально, но в будущем это может быть сложнее.

liftSqlToServer :: SqlPersistT (LoggingT IO) a -> ServerMonad a
liftSqlToServer = ServerMonad

Теперь мы можем создать наш экземпляр MonadDatabase! Каждая реализация функции будет использовать нашу вспомогательную функцию. Они также будут использовать существующую реализацию MonadDatabase для SqlPersistT! Так что это легко!

instance MonadDatabase ServerMonad where
  createUser = liftSqlToServer . createUser
  loginUser = liftSqlToServer . loginUser
  ...

Надеюсь, теперь вы понимаете, почему полезно иметь помощника! Если наша монада изменится, нам нужно будет изменить только функцию lift. Нам не нужно будет трогать экземпляр! Теперь мы готовы выполнять вызовы нашей базы данных из ServerMonad!

Вывод

У нас есть кое-какая работа с базой данных, но еще есть над чем поработать. На следующей неделе мы сосредоточимся на аутентификации, которая будет включать некоторые работы на этой неделе. Затем мы будем готовы создать наши функции-обработчики.

Как всегда, взгляните на наш репозиторий Github, если вы хотите увидеть этот код вблизи и лично! Код этой недели вы можете найти на ветке monad-database.

Если вам понравилась эта серия, обязательно подпишитесь на нашу ежемесячную рассылку! Вы узнаете обо всех последних действиях в блоге и получите доступ к нашим ресурсам для подписчиков!