HPUNIX Сайт о ОС и не только!

Persistent и работа с базами данных в Yesod

5 февраля 2009 - unix
Persistent и работа с базами данных в Yesod

Предлагаю вашему вниманию перевод очередной главы из восхитительной книжки «Developing Web Applications with Haskell and Yesod». Эта глава, как и большая часть других, будет увлекательна даже тем, кто не желает ничего знать об этом нашем Yesod и вообщем когда-либо писать на Haskell. Правда-правда!

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

Внимание! Статья содержит сильно много букв. Если вы перебежали на эту страничку из Twitter либо Гугл Reader в разгаре рабочего денька, советую добавить ее в закладки и прочесть позднее.

Persistent

Формы представляют собой границу меж юзером и приложением. Другая граница, с которой нам приходится иметь дело, находится меж приложением и хранилищем.

Является ли это хранилище SQL базой данных, YAML- либо бинарным файлом, для вас с большой вероятностью придется попотеть, чтоб хранилище воспринимало типы данных вашего приложения. Persistent представляет собой ответ Yesod’а на делему хранения данных. Это универсальный типобезопасный интерфейс к хранилищу данных для Haskell.

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

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

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

Persistent следует принципам безопасности типов и лаконичного, декларативного синтексиса. Посреди других способностей необходимо подчеркнуть:

  • Независимость от базы данных. Имеется высококлассная поддержка PostgreSQL, SQLite и MongoDB, также эксперементальная поддержка CouchDB и находящаяся в разработке поддержка MySQL.
  • Будучи нереляционным по собственной природе, Persistent позволяет сразу поддерживать огромное количество слоев хранения данных и не обременен неуввязками производительности, связанными с внедрением JOIN’ов.
  • Главным источником расстройства при использовании SQL баз данных является попытка конфигурации схемы базы данных. Persistent позволяет автоматом делать обновление схемы базы данных.

Решение пограничной трудности

Допустим, вы храните информацию о людях в SQL базе данных. Соответственная таблица может смотреться как-то так:

CREATE TABLE Person(id SERIAL PRIMARY KEY, name VARCHAR NOT NULL, age INTEGER)

И если вы используете такую СУБД, как PostgreSQL, вы сможете быть убеждены, что СУБД никогда не сохранит какой-либо дополнительный текст в поле age. (Нельзя сказать то же самое в отношении SQLite, но пока забудем об этом.) Для отображения этой таблицы вы сможете возжелать сделать приблизительно таковой тип данных:

data Person = Person
  { personName :: Text
    , personAge :: Int
    }

Все смотрится полностью типобезопасно — схема базы данных соответствует типу данных в Haskell, СУБД гарантирует, что неправильные данные никогда не будут сохранены в таблице, и все в целом смотрится отлично. До поры до времени.

Вы желаете получить данные из СУБД, которая в свою очередь предоставляет их в нетипизированном формате.

  • Вы желаете отыскать все людей, старше 32-х лет, но по ошибке пишете «тридцать два» в SQL-запросе. И значете что? Все отлично скомпилируется и вы не узнаете о дилемме до того времени, пока не запустите программку.
  • Вы решили отыскать первых десятерых человек в алфавитном порядке. Нет проблем… до того времени, пока вы не сделаете опечатку в SQL-запросе. И опять, вы не узнаете об этом до того времени, пока не запустите программку.
  • В языках с динамической типизацией ответом на эти трудности является модульное тестирование. Проверьте, что для всего, что может пойти не так, вы не забили написать тест. Но как, я полагаю, вы уже понимаете, это не очень согласуется с подходом, принятом в Yesod. Мы предпочитаем использовать достоинства статической типизации языка Haskell для нашей своей защиты, как это может быть, и хранение данных не является исключением.

Итак, вопрос остается открытым: как мы можем использовать систему типов языка Haskell, чтоб поправить положение?

Типы

Как и в случае с маршрутами, нет ничего неописуемо сложного в типобезопасном доступе к данным. Он всего только просит написания однообразного, подверженного ошибкам лишнего шаблонного кода.

Как как правило это значит, что мы можем использовать систему типов для того, чтоб избежать излишних ошибок. А чтоб не заниматься нудной работой, мы вооружимся Template Haskell.

Примечание: В ранешних версиях Persistent очень интенсивно употреблялся Template Haskell. Начиная с версии 0.6 употребляется новенькая архитектура, позаимстованная из пакета groundhog. Благодаря новенькому подходу значимая часть нагрузки была переложена на фантомные типы.

PersistValue является главным строительным блоком в Persistent. Этот тип представляет данные, посылаемые базе данных либо получаемые от нее. Вот его определение:

data PersistValue = PersistText Text
                  | PersistByteString ByteString
                  | PersistInt64 Int64
                  | PersistDouble Double
                  | PersistBool Bool
                  | PersistDay Day
                  | PersistTimeOfDay TimeOfDay
                  | PersistUTCTime UTCTime
                  | PersistNull
                  | PersistList [PersistValue]
                  | PersistMap [(T.Text, PersistValue)]
                  | PersistForeignKey ByteString -- ^ предназначен сначала для MongoDB

Любой из бэкэндов Persistent должен знать, как переводить надлежащие значения во что-то, понятное СУБД. Но было бы неловко выражать все данные через эти базисные типы.

Последующим слоем является класс типов PersistField, определяющий, как случайный тип может быть преобразован в тип PersistValue либо назад. PersistField соответствует столбцами в SQL базах данных. В приведенном ранее примере с людьми name и age будут нашими PersistField’ами.

Чтоб связать пользовательский код, нам пригодится последний класс типов — PersistEntity. Экземпляр класса типов PersistEntity соответствует таблице в SQL базе данных. Этот класс типов определеяет несколько функций и связанные с ними типы. Таким макаром, имеет место последующее соответствие меж Persistent и SQL:

SQL Persistent
Тип (VARCHAR, INTEGER и тд) PersistValue
Столбец PersistField
Таблица PersistEntity

Генерация кода

Чтобы убедиться, что экземпляры класса PersistEntity корректно соответствуют нашим типам данных, Persistent берет на себя ответственность и за тех, и за других. Это отлично и исходя из убеждений принципа DRY (Не повторяйтесь, Don’t Repeat Yourslef): от вас требуется объявить сути только один раз. Разглядим последующий пример:

{-# LANGUAGE QuasiQuotes, TypeFamilies, GeneralizedNewtypeDeriving, TemplateHaskell, OverloadedStrings, GADTs #-}

import Database.Persist
import Database.Persist.TH
import Database.Persist.Sqlite
import Control.Monad.IO.Class (liftIO)

mkPersist sqlSettings [persist|
Person
  name String
    age Int
    deriving Show
|]
main = return ()

Тут мы используем комбинацию из Template Haskell и квазицитирования (как при определении маршрутов): persist является обработчиком квазицитирования, который конвертирует чувствительный к пробелам синтаксис в перечень определений сущностей. (Также вы сможете вынести определение сущностей в отдельный файл и пользоваться persistFile.) mkPersist воспринимает перечень этих сущностей и определяет:

  • По одному типу данных языка Haskell на суть;
  • Экземпляр класса PersistEntity для каждого определенного типа данных;

Приведенный выше пример генерирует код, который смотрится приблизительно последующим образом:

{-# LANGUAGE TypeFamilies, GeneralizedNewtypeDeriving, OverloadedStrings, GADTs #-}

import Database.Persist
import Database.Persist.Store
import Database.Persist.Sqlite
import Database.Persist.EntityDef
import Control.Monad.IO.Class (liftIO)
import Control.Applicative

data Person = Person
    { personName :: String
    , personAge :: Int
    }
  deriving (Show, Read, Eq)

type PersonId = Key SqlPersist Person

instance PersistEntity Person where
    -- Обобщенный алгебраический тип данных.
    -- Это дает нам типобезопасный подход к сравнению
    -- полей с их типами данных
    data EntityField Person typ where
        PersonId   :: EntityField Person PersonId
        PersonName :: EntityField Person String
        PersonAge  :: EntityField Person Int

    type PersistEntityBackend Person = SqlPersist

    toPersistFields (Person name age) =
        [ SomePersistField name
        , SomePersistField age
        ]

    fromPersistValues [nameValue, ageValue] = Person
        <$> fromPersistValue nameValue
        <*> fromPersistValue ageValue
    fromPersistValues _ = Left "Invalid fromPersistValues input"

    -- Информация о каждом поле для внутреннего использования
    -- при генерации SQL-выражений
    persistFieldDef PersonId = FieldDef
        (HaskellName "Id")
        (DBName "id")
        (FTTypeCon Nothing "PersonId")
        []
    persistFieldDef PersonName = FieldDef
        (HaskellName "name")
        (DBName "name")
        (FTTypeCon Nothing "String")
        []
    persistFieldDef PersonAge = FieldDef
        (HaskellName "age")
        (DBName "age")
        (FTTypeCon Nothing "Int")
        []
    data Unique Person typ = IgnoreThis
    entityDef = undefined
    halfDefined = undefined
    persistUniqueToFieldNames = undefined
    persistUniqueToValues = undefined
    persistUniqueKeys = undefined
    persistIdField = undefined
main :: IO ()
main = return ()

Как и следовало ждать, определение типа данных Person очень близко к определению, данному в уникальной версии кода, где употреблялся Template Haskell. Мы также имеем обощенный алгебраический тип данный (ОАТД), предоставляющий отдельный конструктор для каждого поля.

Этот ОАТД кодирует как тип сути, так и тип поля. Мы используем его конструкторы через модуль Persistent, к примеру, чтоб убедиться, что когда мы применяем фильтр, типы фильтруемого значения и поля совпадают.

Мы можем использовать сгенерированный тип Person как и хоть какой другой тип языка Haskell, а потом передать его в одну из функций модуля Persistent.

main = withSqliteConn ":memory:" $ runSqlConn $ do
    michaelId <- insert $ Person "Michael" 26
    michael <- get michaelId
    liftIO $ print michael

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

В приведенном примере мы лицезреем две функции. Функция insert делает новейшую запись в базе данных и возвращает ее ID. Как и все другое в модуле Persistent, ID являются типобезопасными. Более тщательно о том, как работают эти ID, мы узнаем позднее.

Итак, код insert $ Person "Michael" 25, возвращает значение типа PersonId.

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

Код, который пробует получить другую суть (к примеру, House), используя PersonId, никогда не будет скомпилирован.

PersistStore

Последний момент, который остался без разъяснения в прошлом примере: что делают функции withSqliteConn и runSqlConn? И что же это все-таки за монада, в какой производятся все наши деяния с базой данных?

Все деяния с базой данных должны производиться в экземпляре PersistStore. Как надо из его наименования, каждое хранилище (PostgreSQL, SQLite, MongoDB) имеет собственный экземпляр PersistStore. Конкретно с его помощью происходят генерация SQL-запросов, преобразования из PersistValue в значения, специфичные для определенной СУБД и т.д..

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

Функция withSqliteConn делает отдельное соединение с базой данных, используя предоставленную строчку. Для тестов мы воспользуемся строчкой «:memory:», которая значит использовать базу данных, расположенную в памяти.

Функция runSqlConn употребляет это соединение для выполнения действий над базой данных. SQLite и PostgreSQL употребляют один и тот же экземпляр PersistStore: SqlPersist.

Примечание: В реальности существует еще несколько классов типов — это PersistUpdate и PersistQuery. Разные классы типов предоставляют различную функциональность, что позволяет нам писать бэкенды, использующие более обыкновенные хранилища (к примеру, Redis) невзирая на то, что они не владеют всей высокоуровневой функциональностью, предоставляемой Persistent.

Принципиальный момент, который необходимо подчеркнуть, состоит в том, что каждый отдельный вызов runSqlConn производится в отдельной транзакции. Отсюда два следствия:

  • Для многих СУБД выполнение коммита может быть дорогой операцией. Помещая огромное количество запросов в одну транзакцию, вы сможете значительно ускорить выполнение кода.
  • Если где-либо снутри вызова runSqlConn кидается исключение, все выполненные деяния будут откачены (естественно, если применяемый бэкенд поддерживает транзакции).

Передвижения

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

При работе с реляционными СУБД, изменение схемы базы данных обычно является большой неувязкой. Заместо того, чтоб ложить эту делему на плечи юзера, Persistent делает шаг вперед и протягивает руку помощи. Только необходимо его об этом попросить. Ах так приблизительно это смотрится:

{-# LANGUAGE QuasiQuotes, TypeFamilies, GeneralizedNewtypeDeriving, TemplateHaskell, OverloadedStrings, GADTs, FlexibleContexts #-}

import Database.Persist
import Database.Persist.TH
import Database.Persist.Sqlite
import Control.Monad.IO.Class (liftIO)

share [mkPersist sqlSettings, mkSave "entityDefs"] [persist|
Person
  name String
    age Int
    deriving Show
|]

main = withSqliteConn ":memory:" $ runSqlConn $ do
    runMigration $ migrate entityDefs (undefined :: Person) -- добавлена эта строка, и только!
    michaelId <- insert $ Person "Michael" 26
    michael <- get michaelId
    liftIO $ print michael

Благодаря этому маленькому изменению, Persistent будет автоматом создавать вам таблицу Person. Разбиение меж функциями runMigration и migrate позволяет создавать передвижения огромного количества таблиц сразу.

Это отлично работает, когда идет речь о нескольких сущностях, но становится несколько мучительным при работе с десятками. Заместо того, чтоб повторяться, Persistent предлагает функцию mkMigrate:

{-# LANGUAGE QuasiQuotes, TypeFamilies, GeneralizedNewtypeDeriving, TemplateHaskell, OverloadedStrings, GADTs, FlexibleContexts #-}

import Database.Persist
import Database.Persist.Sqlite
import Database.Persist.TH

share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persist|
Person
    name String
    age Int
    deriving Show
Car
    color String
    make String
    model String
    deriving Show
|]

main = withSqliteConn ":memory:" $ runSqlConn $ do
       runMigration migrateAll

mkMigrate — это функция Template Haskell, которая делает новейшую функцию, что будет вызывать migrate для всех сущностей, объявленных в блоке persist. Функция share является маленьким хелпером, который передает информацию из блока persist каждой из функций Template Haskell, а потом соединяет воединыжды результаты их выполнения.

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

  • Поменялся тип данных поля. Но СУБД может возражать против такового конфигурации, если данные не могут быть преобразованы.
  • Было добавлено новое поле. Но если поле не может быть пустым (NULL), не было предоставлено значение по дефлоту (как это сделать, мы обсудим позднее), и в таблице уже есть какие-то даные, СУБД не дозволит добавить поле.
  • Некое поле с этого момента может быть пустым. В оборотном случае Persistent попробует выполнить преобразование, если СУБД дозволит это сделать.
  • Была добавлена совсем новенькая суть.

Но есть и случаи, которые Persistent не в состоянии обработать:

  • Переименование сущностей либо полей. У Persistent нет никакой способности выяснить, что поле «name» было переименовано в «fullName». Все, что он лицезреет — это старенькое поле с именованием «name» и новое поле с именованием «fullName».
  • Удаление полей. Так как это может привести к потере данных, по дефлоту Persistent отрешается делать такие преобразования. Вы сможете настоять на этом, воспользовавшись функцией runMigrationUnsafe заместо runMigration, но это не рекомендуется.

Функция runMigration выводит выполняемые передвижения в STDERR (если для вас не нравится такое поведение, воспользуйтесь функцией runMigrationSilent). По способности она употребляет запросы ALTER TABLE. Но в SQLite ALTER TABLE имеет очень малые способности, потому Persistent приходится прибегнуть к копированию данных из одной таблицы в другую.

В конце концов, если вы желаете, чтоб заместо выполнения миграций Persistent отдал для вас подсказку по самостоятельному выполнению этих миграцией, воспользуйтесь функцией printMigration. Эта функция выводит деяния, которые могли быть выполнены функцией runMigration. Это может быть полезно в случае выполнения миграций, на который Persistent не способен, выполнения дополнительных SQL-запросов во время миграций, либо же просто для логирования происходящих миграций.

Уникальность

Кроме объявления полей у сути мы также может объявлять ограничение уникальности. Обычный пример — это требование уникальности имени юзера:

User
    username Text
Persistent и работа с базами данных в Yesod
    UniqueUsername username

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

{-# LANGUAGE QuasiQuotes, TypeFamilies, GeneralizedNewtypeDeriving, TemplateHaskell, OverloadedStrings, GADTs, FlexibleContexts #-}
import Database.Persist
import Database.Persist.Sqlite
import Database.Persist.TH
import Data.Time
import Control.Monad.IO.Class (liftIO)
import Control.Monad.Trans.Resource (runResourceT)

share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persist|
Person
    firstName String
    lastName String
    age Int
    UniqueName firstName lastName
    deriving Show
|]

main = runResourceT $ withSqliteConn ":memory:" $ runSqlConn $ do
    runMigration migrateAll
    insert $ Person "Michael" "Snoyman" 26
    michael <- getBy $ UniqueName "Michael" "Snoyman"
    liftIO $ print michael

Чтоб сказать об уникальности композиции нескольких полей, добавим одну дополнительную строчку в наше определение. Persistent знает, что таким макаром определяется уникальный конструктор, так как строчка начинается с большей буковкы. Каждое следующее слово должно быть именованием поля в сути.

Главное ограничение, связанное с уникальностью, заключается в том, что она может употребляться только для непустых (non-null) полей. Причина состоит в том, что эталон SQL многозначен относительно уникальности пустых полей (к примеру, NULL=NULL является правдой либо ложью?). К тому же, в большинстве СУБД реализованы правила, которые не соответствуют правилам для соответственных типов данных в Haskell (к примеру, в PostgreSQL NULL=NULL — это ересь, а в Haskell Nothing=Nothing есть True).

В дополнение к предоставлению гарантий на уровне СУБД относительно согласованности даннных, ограничение уникальности также может быть применено для выполнения неких специфичных запросов из кода на Haskell, как, к примеру, в случае с getBy, продемонстрированом выше. Тут употребляется ассоциативный тип Unique. В конце приведенного выше примера употребляется последующий конструктор:

UniqueName :: String -> String -> Unique Person

Примечание: В случае использования MongoDB ограничение уникальности не может быть применено — вы должны сделать уникальный индекс по полю.

Запросы

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

Одни должны возвращать менее 1-го результата, другие же могут возвращать огромное количество результатов.

В связи с этим Persistent предоставляет множетсво разных функций для выполнения запросов. Как обычно, мы стараемся закодировать при помощи типов столько инвариантов, сколько может быть. К примеру, если запрос может возвращать или 0, или Один итог, употребляется обертка Maybe.

Если же запрос может возвратить много результатов, ворачивается перечень.

Подборка по ID

Простой запрос, который может быть выполнен в Persistent — это подборка по ID. Так как в данном случае значение может существовать либо не существовать, возвращаемое значение оборачивается в Maybe.

Внедрение функции get:

  personId <- insert $ Person "Michael" "Snoyman" 26
    maybePerson <- get personId
    case maybePerson of
        Nothing -> liftIO $ putStrLn "Ничего нет"
        Just person -> liftIO $ print person

Это может быть очень комфортно на веб-сайтах, предоставляющих URL типа /person/5. Но в таких случаях мы обычно не беспокоимся о Maybe, а просто желаем получить значение либо возвратить код 404, если оно не найдено. К счастью, есть функция get404, которая поможет нам в этом.

Мы разберемся с этим вопросом более детально, когда дойдем до интеграции с Yesod.

Подборка по уникальному ключу

Функция getBy практически схожа get, только заместо ID она воспринимает значение Unique.

Внедрение функции getBy:

    personId <- insert $ Person "Michael" "Snoyman" 26
    maybePerson <- getBy $ UniqueName "Michael" "Snoyman"
    case maybePerson of
        Nothing -> liftIO $ putStrLn "Ничего нет"
        Just person -> liftIO $ print person

Аналогично get404, также существует функция getBy404.

Другие функции подборки

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

Все эти функции имеют схожий интерфейс и малость различающиеся возвращаемые значения:

  • selectSource. Возвращает источник (source), содержащий все ID и значения из базы данных. Это позволяет писать поточный код. (*)
  • selectList. Возвращает перечень, содержащий все ID и значения из базы данных. Все записи будут помещены в память.
  • selectFirst. Просто возвращает 1-ый ID и 1-ое значение из базы данных, если они есть.
  • selectKeys. Возвращает только ключи, без значений, в качесте источника.

Примечание: (*) Мы более тщательно разглядим источники в приложении, посвященном кондуитам (conduits). Не считая того, есть и другая функция под заглавием selectSourceConn, которая предоставляет больше контроля над выделением соединений. Мы разглядим ее в главе, посвященной работае со Sphinx.

В большинстве случаев употребляется функция selectList, так что мы разглядим ее раздельно. После чего осознать другие функции будет проще обычного.

Функция selectList воспринимает два аргумента: перечень Filter’ов и перечень SelectOpt’ов. 1-ый из их определяет ограничения, накладываемые на характеристики сущностей, и позволяет использовать предикаты «равно», «меньше чем», «принадлежит множеству» и тп. SelectOpt’ы предоставляют три разных способности — сортировку, ограничение количества возвращаемых строк и смещение возвращаемого значения на данное количество строк.

Примечание: Композиция из ограничения и смещения очень принципиальна, она позволяет воплотить действенное разбиение на странички в вашем веб-приложении.

Сходу перейдем например с фильтрацией, а потом проанализируем его:

  people <- selectList [PersonAge >. 25, PersonAge <=. 30] []
    liftIO $ print people

Невзирая на простоту примера, стоит отметить три момента:

  • PersonAge является конструктором ассоциативного фантомного типа. Звучит ужасающе, но вправду принципиально только то, что он совершенно точно определяет столбец «age» таблицы «person», также знает, что возраст по сути является Int’ом. (В этом и состоит его фантомность.)
  • Мы имеем дело с группой фильтрующих операторов пакета Persistent. Они достаточно прямолинейны и делают в точности то, что вы от их ждете. Но здесь есть три тонких момента, которые я объясню ниже.
  • Перечень фильтров объядиняется логическим И, другими словами, ограничение следует читать, как «возраст больше 25-и И возраст меньше либо равен 30-и». Внедрение логического Либо мы разглядим ниже.

Также имеется оператор с необычным заглавием «не равно». Мы используем обозначение !=.', так как /=. применяется при UPDATE-запросах (ради «разделяй-и-устанавливай», о котором я расскажу позднее).

Не волнуйтесь, если вы воспользуетесь неправильным оператором, компилятор предупредит вас. Еще два умопомрачительных оператора — это «принадлежит множеству» и «не принадлежит множеству». Они обозначаются, соответственно, <-. и /<-.

(оба с точкой на конце).

Что все-таки касается логического Либо, для него есть оператор ||.. К примеру:

    people <- selectList
        (       [PersonAge >. 25, PersonAge <=. 30]
            ||. [PersonFirstName /<-. ["Adam", "Bonny"]]
            ||. ([PersonAge ==. 50] ||. [PersonAge ==. 60])
        )
        []
    liftIO $ print people

Этот (совсем несуразный) пример значит «найти людей, чей возраст составляет от 26-и до 30-и лет включительно Либо чье имя не Адам и не Бонни Либо чей возраст — 50 либо Шестьдесят лет».

SelectOpt

Все наши вызовы selectList имели пустой перечень в качестве второго аргумента. Это не задает никаких характеристик и значит «сортируй на усмотрение СУБД, возвращай все результаты, не пропускай никаких результатов». SelectOpt имеет четыре конструктора, которые могут быть применены для конфигурации этого поведения:

  • Asc. Сортировать по данному столбцу в неубывающем порядке. Здесь употребляется таковой же фантомный тип, как и при фильтрации, к примеру PersonAge.
  • Desc. Аналогично Asc, исключительно в невозрастающем порядке.
  • LimitTo. Воспринимает аргумент типа Int. Возвратить менее обозначенного количества результатов.
  • OffsetBy. Также воспринимает аргумент типа Int. Пропустить обозначенное количество результатов.

В последующем отрывке кода определяется функция, которая разбивает итог на странички. Она возвращает всех людей старше 18-и лет, отсортированных по возрасту (более старшие идут первыми). Люди с схожим возрастом сортируются по фамилиям, а потом по именам.

resultsForPage pageNumber = do
    let resultsPerPage = 10
    selectList
        [ PersonAge >=. 18
        ]
        [ Desc PersonAge
        , Asc PersonLastName
        , Asc PersonFirstName
        , LimitTo resultsPerPage
        , OffsetBy $ (pageNumber - 1) * resultsPerPage
        ]

main = withSqliteConn ":memory:" $ runSqlConn $ do
    runMigrationSilent migrateAll
    personId <- insert $ Person "Michael" "Snoyman" 26
    resultsForPage Один >>= liftIO . print

Другие деяния с данными

Извлечение данных — это только полдела. Нам также нужно иметь возможность добавлять данные и видоизменять данные, находящиеся в базе.

Вставка

Иметь возможность работать с данными из базы — это здорово и замечательно, но как эти данные туда попадут? Для этого есть функция insert. Вы просто передаете ей значение, а она возвращает ID.

В связи с этим имеет смысл мало объяснить философию Persistent. В почти всех ORM типы, применяемые для работы с данными, непрозрачны. Для вас приходится продираться через определяемый ими интерфейс, чтоб получить, а потом поменять данные.

Но в Persistent все по другому — для всего употребляются старенькые добрые алгебраические типы данных. Таким макаром, вы как и раньше сможете иметь большой выигрышь от использования сопастовления с прототипом, каррирования и всего остального, к чему вы привыкли.

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

Все же, есть момент, который нередко волнует новичков. Почему ID и значения совсем разбиты? Казалось бы, куда логичнее было бы включить ID в само значение. Другими словами, заместо:

data Person = Person { name :: String }

… мы имели бы:

data Person = Person { personId :: PersonId, name :: String }

Одна из заморочек сходу оказывается на виду. Как прикажете создавать вставку? Если Person требуется ID, а ID ворачивается функцией insert, которой в свою очередь требуется Person, мы получаем делему курицы и яичка.

Мы могли бы решить эту делему, используя неопределенный ID, но это верный метод напороться на проблемы.

Вы скажете, отлично, давайте попробуем что-то более неопасное:

data Person = Person { personId :: Maybe PersonId, name :: String }

Намного предочтительнее писать insert $ Person Nothing "Michael" заместо insert $ Person undefined "Michael". И наши типы стали намного проще, не так ли? К примеру, selectList сейчас может возвращать просто [Person] заместо уродливого [Entity SqlPersist Person].

Примечание: Entity представляет собой тип данных, который связывает ID и значение сути воедино. Так как ID могут быть различными зависимо от бэкенда, нужно также предоставить применяемый бэкенд пакета Persistent. Тип данных Entity SqlPersist Person следует читать как «ID и значение некоторого человека, хранящиеся в SQL базе данных».

Неувязка состоит в том, что «уродство» оказывается неописуемо полезным. Запись Entity SqlPersist Person делает естественным тот факт, что мы работаем со значением, которое существует в базе данных.

Допустим, мы желаем сделать URL, в каком пресутствует PersonId (не таковой уж редчайший случай, как мы скоро выясним). Entity SqlPersist Person недвусмысленно предоставляет доступ к требуемой инфы. Тем временем, внедрение обертки Maybe приводит к потребности в дополнительных проверках во время выполнения, заместо того, чтоб убедиться в правильности программки еще на шаге компиляции.

В конце концов, в случае присоединения ID к значению имеет место семантическое несоответствие. Person — это значение. Два человека являются схожими (исходя из убеждений базы данных), если все их поля схожи.

Присоединяя ID к значению, мы начинаем гласить не о человеке, а о строке в базе данных. Равенство перестает быть равентсвом, оно преобразуется в идентичность: «это тот же самый человек» заместо «это таковой же человек».

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

Обновление

Сейчас подумаем об обновлении в контексте нашего обсуждения. Вот простой метод сделать обновление:

let michael = Person "Michael" 26
  michaelAfterBirthday = michael { personAge = 20 семь }

Но в реальности этот код ничего не обновляет. Он просто делает новое значение типа Person, основанное на древнем значении. Когда мы говорим об обновлении, мы имеем ввиду не модификацию значений в Haskell. (И взаправду, не стоило бы этого делать, так как данные в Haskell неизменяемы.)

По сути, мы ищем метод поменять строчки в таблице. И проще всего сделать это при помощи функции update:

    personId <- insert $ Person "Michael" "Snoyman" 26
    update personId [PersonAge =. 27]
    resultsForPage Один >>= liftIO . print

Функция update воспринимает два аргумента: ID и перечень Update’ов. Простой метод обновления поля заключается в присвоении ему нового значения, но этот метод не наилучший. Что, если вы желаете прирастить чей-то возраст на единицу, но текущий возраст для вас не известен? В Persistent предвидено и это:

haveBirthday personId = update personId [PersonAge +=. 1]

main = withSqliteConn ":memory:" $ runSqlConn $ do
    runMigrationSilent migrateAll
    personId <- insert $ Person "Michael" "Snoyman" 26
    update personId [PersonAge =. 27]
    haveBirthday personId
    resultsForPage Один >>= liftIO . print

Как и следовало ждать, в нашем распоряжении все есть базисные математические операторы: +=., -=., *=. и /=..

Они не только лишь комфортны для обновления единичной записи, да и нужны для соблюдения гарантий ACID. Представьте, что бы мы делали без этих операторов. Нам приходилось бы извлекать из базы Person, наращивать возраст, а потом обновлять значение в базе данных на новое.

Как у вас возникает два процесса либо потока, сразу работающих с базой данных, вы попадаете в мир боли (подсказка: состоняие гонки).

Время от времени мы желаем обновить несколько строк сразу (к примеру, повысить заработную плату на 5% всем сотрудникам). Функция updateWhere воспринимает два аргумента: перечень фильтров и перечень обновлений, которые следует применить.

    updateWhere [PersonFirstName ==. "Michael"] [PersonAge *=. 2] -- это был длиннющий денек

Время от времени охото просто поменять одно значение в базе данных на другое. Для этого (сюрприз!) есть функция replace:

    personId <- insert $ Person "Michael" "Snoyman" 26
    replace personId $ Person "John" "Doe" 20
    update personId [PersonAge =. 27]
    haveBirthday personId
    updateWhere [PersonFirstName ==. "Michael"] [PersonAge *=. 2] -- это был длиннющий денек
    resultsForPage Один >>= liftIO . print

Удаление

Как ни грустно, время от времени мы обязаны расстаться с нашими данными. Для этого у нас есть аж три функции:

Функция Действие
delete Удалить по ID
deleteBy Удалить по уникальному ключу
deleteWhere Удалить по огромному количеству фильтров

    personId <- insert $ Person "Michael" "Snoyman" 26
    delete personId
    deleteBy $ UniqueName "Michael" "Snoyman"
    deleteWhere [PersonFirstName ==. "Michael"]

При помощи deleteWhere мы можем удалить вообщем все данные из таблицы. Необходимо только дать подсказку GHC, какая таблица нас интересует:

    deleteWhere ([] :: [Filter Person])

Атрибуты

До сего времени мы лицезрели базисный синтаксис для наших persist-блоков — строчка с именованием сути, за которой для каждого поля идет по одной строке с отступами, состоящей из 2-ух слов, имени поля и типа данных поля. Persistent поддерживает не только лишь это. После 2-ух слов в строке вы сможете указать случайный перечень атрибутов.

Допустим, вы желаете, чтоб суть Person имела необязательный возраст, также время прибавления его либо ее в систему. Для сущностей, уже находящихся в базе данных, в качестве сих пор мы желаем использовать текущее время.

{-# LANGUAGE QuasiQuotes, TypeFamilies, GeneralizedNewtypeDeriving, TemplateHaskell, OverloadedStrings, GADTs, FlexibleContexts #-}

import Database.Persist
import Database.Persist.Sqlite
import Database.Persist.TH
import Data.Time
import Control.Monad.IO.Class

share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persist|
Person
  name String
    age Int Maybe
    created UTCTime default=CURRENT_TIME
    deriving Show
|]

main = withSqliteConn ":memory:" $ runSqlConn $ do
    time <- liftIO getCurrentTime
    runMigration migrateAll
    insert $ Person "Michael" (Just 26) time
    insert $ Person "Greg" Nothing time

Maybe является интегрированным атрибутом из 1-го слова. Он делает поле необязательным. Это значит, что в Haskell данное поле будет обернуто в Maybe, а в SQL оно может иметь значение NULL.

Атрибут default находится в зависимости от применяемого бэкенда и может использовать хоть какой синтаксис, только бы он был понятен СУБД. В приведенном примере употребляется интегрированная функция СУБД CURRENT_TIME. Допустим, сейчас мы желаем добавить в суть Person поле с возлюбленным языком программирования:

{-# LANGUAGE QuasiQuotes, TypeFamilies, GeneralizedNewtypeDeriving, TemplateHaskell, OverloadedStrings, GADTs, FlexibleContexts #-}

import Database.Persist
import Database.Persist.Sqlite
import Database.Persist.TH
import Data.Time

share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persist|
Person
    name String
    age Int Maybe
    created UTCTime default=CURRENT_TIME
    language String default='Haskell'
    deriving Show
|]

main = withSqliteConn ":memory:" $ runSqlConn $ do
    runMigration migrateAll

Примечание: Атрибут default полностью никак не затрагивает код на Haskell, другими словами, для вас как и раньше придется заполнять все значения. Атрибут оказывает влияние лишь на схему базы данных и автоматические передвижения.

Мы должны окружить строчку в одинарные кавычки, чтоб СУБД могла верно интерпретировать ее. Также Persistent позволяет использовать двойные кавычки для строк, содержащих пробелы. К примеру, если мы желаем сделать государством по дефлоту Российскую Федерацию, то должны написать:

{-# LANGUAGE QuasiQuotes, TypeFamilies, GeneralizedNewtypeDeriving, TemplateHaskell, OverloadedStrings, GADTs, FlexibleContexts #-}

import Database.Persist
import Database.Persist.Sqlite
import Database.Persist.TH
import Data.Time
Persistent и работа с базами данных в Yesod

share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persist|
Person
    name String
    age Int Maybe
    created UTCTime default=CURRENT_TIME
    language String default='Haskell'
    country String "default='Русская Федерация'"
    deriving Show
|]

main = withSqliteConn ":memory:" $ runSqlConn $ do
    runMigration migrateAll

Последний трюк, который вы сможете сделать с атрибутами — это указать имена таблиц и столбцов, применяемые в SQL. Это может понадобиться при содействии с существующими базами данных.

share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persist|
Person sql=the-person-table
    firstName String sql=first_name
    lastName String sql=fldLastName
    age Int Gt Desc "sql=The Age of the Person"
    UniqueName firstName lastName
    deriving Show
|]

resultsForPage pageNumber = do
    let resultsPerPage = 10
    selectList
        [ PersonAge >=. 18
        ]
        [ Desc PersonAge
        , Asc PersonLastName
        , Asc PersonFirstName
        , LimitTo resultsPerPage
        , OffsetBy $ (pageNumber - 1) * resultsPerPage
        ]

main = withSqliteConn ":memory:" $ runSqlConn $ do
    runMigration migrateAll
    personId <- insert $ Person "Michael" "Snoyman" 26
    resultsForPage Один >>= liftIO . print

Дела

Persistent поддерживает ссылки меж типами данных, таким макаром, что они остаются согласованными в поддерживаемых NoSQL базах данных. Ссылка создается методом прибавления ID в связанную суть. Ах так смотрится пример для человека с огромным количеством машин:

{-# LANGUAGE QuasiQuotes, TypeFamilies, GeneralizedNewtypeDeriving, TemplateHaskell, OverloadedStrings, GADTs, FlexibleContexts #-}

import Database.Persist
import Database.Persist.Sqlite
import Database.Persist.TH
import Control.Monad.IO.Class (liftIO)
import Data.Time

share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persist|
Person
    name String
    deriving Show
Car
    ownerId PersonId Eq
    name String
    deriving Show
|]

main = withSqliteConn ":memory:" $ runSqlConn $ do
    runMigration migrateAll
    bruce <- insert $ Person "Bruce Wayne"
    insert $ Car bruce "Bat Mobile"
    insert $ Car bruce "Porsche"
    -- это может занять много времени
    cars <- selectList [CarOwnerId ==. bruce] []
    liftIO $ print cars

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

{-# LANGUAGE QuasiQuotes, TypeFamilies, GeneralizedNewtypeDeriving, TemplateHaskell, OverloadedStrings, GADTs, FlexibleContexts #-}

import Database.Persist
import Database.Persist.Sqlite
import Database.Persist.TH
import Data.Time

share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persist|
Person
    name String
Store
    name String
PersonStore
    personId PersonId
    storeId StoreId
    UniquePersonStore personId storeId
|]

main = withSqliteConn ":memory:" $ runSqlConn $ do
    runMigration migrateAll

    bruce <- insert $ Person "Bruce Wayne"
    michael <- insert $ Person "Michael"

    target <- insert $ Store "Target"
    gucci <- insert $ Store "Gucci"
    sevenEleven <- insert $ Store "7-11"

    insert $ PersonStore bruce gucci
    insert $ PersonStore bruce sevenEleven

    insert $ PersonStore michael target
    insert $ PersonStore michael sevenEleven

Примечание: Так как суффикс Id в имени типа употребляется в Persistent для обозначения связи по наружному ключу, в реальный момент не представляется вероятным использовать неключевые типы, чье имя завершается на Id. Обычное решение этой препядствия заключается в определении синонима типа с другим суффиксом, к примеру:

data MyExistingTypeEndingInId = ...
type IdIsNotTheSuffix = MyExistingTypeEndingInId
[persist|
Person
    someField IdIsNotTheSuffix

Подробнее о типах

До сего времени мы гласили о Person и PersonId без особенного разъяснения, чем они по сути являются. В простом случае, если мы говорим только о реляционных базах данных, PersinId мог бы быть просто type PersonId = Int64.

Но в данном случае на уровне типов ничто не связывало бы PersinId и суть Person. В итоге вы могли бы по ошибке пользоваться PersonId для получения Car. Для моделирования таких отношений мы используем фантомные типы. Итак, наш последующий доверчивый шаг был бы последующим:

newtype Key entity = Key Int64
type PersonId = Key Person

Примечание: До Persistent 0.6 мы использовали ассоциативные типы заместо фантомных типов. Таким методом также можно решить делему, но фантомы управляются с ней лучше.

И это вправду отлично работает до того времени, пока вы не сталкнетесь с бэкендом, который не употребляет Int64 в качестве ID. И это далековато не теоретический вопрос, так как MongoDB для этих целей употребляет тип ByteString. Итак, нам необходимо значение ключа, которое может содержать или Int64, или ByteString. Кажется, настало хорошее время для внедрения суммарных типов:

data Key entity = KeyInt Int64 | KeyByteString ByteString

Но по сути мы только ищем проблемы. В последующий раз нам попадется бэкенд, который употребляет в качестве ключа временные метки и нам придется ввести дополнительный конструктор для Key. Так может длиться какое-то время. К счастью, у нас уже есть суммарный тип, созданный для представления случайных данных, PersistValue:

newtype Key entity = Key PersistValue

Но здесь есть другая неувязка. Скажем, у нас есть веб-приложение, которое воспринимает ID в качестве параметра от юзера. Этому приложению придется принимать параметр, как Text, а потом пробовать конвертировать его в Key.

Нет заморочек, давайте напишем функцию, которая преобразовывает Text в PersistValue, а потом передадим возвращаемое ее значение в конструктор Key. Верно?

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

К примеру, если мы имеем дело с SQL, ключ должен быть целым числом. Но при подходе, описанной чуть повыше, в качестве ключа мы обязаны принимать произвольные текстовые данные. В итоге мы получали 500-ые ошибки, так как СУБД была в шоке от попыток ассоциировать целочисленные поля с текстом.

Что нам вправду необходимо, это метод преобразования текста в Key, но с учетом правил применяемого бэкенда. И как вопрос становится сформулирован таким макаром, мы здесь же получаем ответ — добавить еще 1-го фантома. В реальности, определение Key в Persistent последующее:

newtype Key backend entity = Key { unKey :: PersistValue }

И это отлично работает. Сейчас мы можем получить функцию Text -> Key MongoDB entity и функцию Text -> Key SqlPersist entity, после этого все работает, как по маслу. Но сейчас есть другая неувязка — дела.

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

Blog
  title Text
Post
    title Text
    blogId BlogId

Но как это будет смотреться исходя из убеждений типа данных Key?

data Blog = Blog { blogTitle :: Text }
data Post = Post { postTitle :: Text, postBlogId :: Key <что должно быть тут?> Blog }

Мы должны указать некий бэкенд. В теории, мы можем захардкодить SqlPersist либо Mongo, но тогда наши типы данных будут работать только с одним бэкендом. Для 1-го приложения это может быть применимым, но как насчет библиотек с определениями типов данных, которые могут употребляться в разных приложениях с разными бэкендами?

Так что, все становится чуток труднее. По сути, наши типы такие:

data BlogGeneric backend = Blog { blogTitle :: Text }
data PostGeneric backend = Post { postTitle :: Text, postBlogId :: Key backend (BlogGeneric backend) }

Направьте внимание, что мы все еще используем недлинные имена для конструкторов и записей. В конце концов, чтоб предоставить обычной интерфейс для обычного кода, мы определяем кое-какие синонимы типов:

type Blog = BlogGeneric SqlPersist
type BlogId = Key SqlPersist Blog
type Post = PostGeneric SqlPersist
type PostId = Key SqlPersist Post

И нет, SqlPersist не захардкожен где бы то ни было в Persistent. Это параметр sqlSettings, что вы передали в mkPersist, гласит нам использовать SqlPersist. В коде, использующем MongoDB, заместо него будет применен параметр mongoSettings.

Описанное выше может показаться несколько сложным, но при написании пользовательского кода все это чуть ли когда-нибудь для вас пригодится. Просмотрите снова эту главу — нам никогда не приходилось работать с Key либо Generic впрямую.

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

Поля случайного типа

У вас может появиться желание использовать в вашем хранилище поля случайного типа. Более обычный случай — это перечисление, к примеру состояние найма служащих. Для этого Persistent предоставляет специальную функцию Template Haskell:

{-# LANGUAGE QuasiQuotes, TypeFamilies, GeneralizedNewtypeDeriving, TemplateHaskell, OverloadedStrings, GADTs, FlexibleContexts #-}

import Database.Persist
import Database.Persist.Sqlite
import Database.Persist.TH

data Employment = Employed | Unemployed | Retired
    deriving (Show, Read, Eq)
derivePersistField "Employment"

share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persist|
Person
    name String
    employment Employment
|]

main = withSqliteConn ":memory:" $ runSqlConn $ do
    runMigration migrateAll

    insert $ Person "Bruce Wayne" Retired
    insert $ Person "Peter Parker" Unemployed
    insert $ Person "Michael" Employed

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

Persistent: сырой SQL

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

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

В данном случае в SQL-запросе требуется использовать LIKE, который не моделируется в Persistent. Давайте попробуем отыскать всех людей с фамилией «Snoyman» и вывести отысканные записи.

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

{-# LANGUAGE OverloadedStrings, TemplateHaskell, QuasiQuotes, TypeFamilies, GeneralizedNewtypeDeriving, GADTs, FlexibleContexts #-}

import Database.Persist.Sqlite (withSqliteConn)
import Database.Persist.TH (mkPersist, persist, share, mkMigrate, sqlSettings)
import Database.Persist.GenericSql (runSqlConn, runMigration, SqlPersist)
import Database.Persist.GenericSql.Raw (withStmt)
import Data.Text (Text)
import Database.Persist
import Database.Persist.Store (PersistValue)
import Control.Monad.IO.Class (liftIO)
import qualified Data.Conduit as C
import qualified Data.Conduit.List as CL

share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persist|
Person
  name Text
|]

main :: IO ()
main = withSqliteConn ":memory:" $ runSqlConn $ do
    runMigration migrateAll
    insert $ Person "Michael Snoyman"
    insert $ Person "Miriam Snoyman"
    insert $ Person "Eliezer Snoyman"
    insert $ Person "Gavriella Snoyman"
    insert $ Person "Greg Weber"
    insert $ Person "Rick Richardson"

    -- Persistent не предоставляет ключевика LIKE, но нам
    -- хотелось бы получить всю семью Snoyman'ов...
    let sql = "SELECT name FROM Person WHERE name LIKE '%Snoyman'"
    C.runResourceT $ withStmt sql []
                C.$$ CL.mapM_ $ liftIO . print

Также существует высокоуровневая поддержка сериализации. Подробности вы сможете отыскать в Haddock-документации по API.

Интеграция с Yesod

Итак, вы прониклись всей мощью Persistent. Как сейчас интегрировать его с Yesod-приложением? Если вы использовали автоматическую генерацию шаблона приложения, большая часть работы уже была проделана за вас.

Но, по традиции, на данный момент мы проделаем все вручную, чтоб лучше осознать, как все устроено.

Пакет yesod-persistent представляет собой «клей» меж Persistent и Yesod. Он предоставляет класс типов YesodPersist, который стандартизует доступ к базе данных при помощи runDB. Ах так это смотрится в действии:

{-# LANGUAGE QuasiQuotes, TypeFamilies, GeneralizedNewtypeDeriving, FlexibleContexts, TemplateHaskell, OverloadedStrings, GADTs, MultiParamTypeClasses #-}

import Yesod
import Database.Persist.Sqlite

-- Определяем наши сути, как обычно
share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persist|
Person
    firstName String
    lastName String
    age Int Gt Desc
    deriving Show
|]

-- Мы являемся держателями пула соединений. Когда программка
-- инициализируется, мы создаем исходный пул, и всякий раз, когда
-- нам необходимо произвести действие, мы выделяем соединение из пула.
data PersistTest = PersistTest ConnectionPool

-- Мы создаем единственный маршрут для доступа к человеку. Это
-- достаточно всераспространенная практика, когда в маршрутах
-- употребляется Id.
mkYesod "PersistTest" [parseRoutes|
/person/#PersonId PersonR GET
|]

-- Здесь ничего такого особенного
instance Yesod PersistTest

-- Сейчас нам необходимо найти экземпляр класса Yesod Persist, который
-- будет смотреть за тем, какой бэкенд мы используем и как надо
-- делать деяния
instance YesodPersist PersistTest where
    type YesodPersistBackend PersistTest = SqlPersist

    runDB action = do
        PersistTest pool <- getYesod
        runSqlPool action pool

-- Мы просто возвращаем строковое представление челавека
-- либо ошибку 404, если таковой Person не существует
getPersonR :: PersonId -> Handler RepPlain
getPersonR personId = do
    person <- runDB $ get404 personId
    return $ RepPlain $ toContent $ show person

openConnectionCount :: Int
openConnectionCount = 10

main :: IO ()
main = withSqlitePool "test.db3" openConnectionCount $ \pool -> do
    runSqlPool (runMigration migrateAll) pool
    runSqlPool (insert $ Person "Michael" "Snoyman" 26) pool
    warpDebug Три тыщи $ PersistTest pool

Здесь есть два принципиальных момента. Для выполнения действий над базой данных в обработчике употребляется runDB. Снутри runDB вы сможете использовать все те функции, о которых шла речь выше, к примеру, insert и selectList.

Примечание: runDB имеет тип runDB :: YesodDB sub master a -> GHandler sub master a. YesodDB определен, как: type YesodDB sub master = YesodPersistBackend master (GHandler sub master) Так как он построен на ассоциативном типе YesodPersistBackend, употребляется подходящий для текущего веб-сайта бэкенд.

Другая новенькая фишка — это get404. Эта функция работает в точности, как get, только заместо того, чтоб возвращать Nothing, когда итог не может быть найден, она возвращает страничку с сообщением об ошибке 404. В функции getPersonR применен очень всераспространенный в реальных приложениях на Yesod подход — значение выходит через функцию get404, а потом зависимо от результата ворачивается ответ.

Заключение

Persistent приносит строгую типизацию языка Haskell в ваш слой доступа к данным. Заместо того, чтоб писать склонный к появлению ошибок, нетипизированный доступ к данным, либо вручную писать шаблонный код сериализации, вы сможете заавтоматизировать работу, используя Persistent.

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

Persistent впрямую встраивается в общий рабочий процесс с Yesod. И речь здесь идет не только лишь о маленьких пакетах вроде yesod-persistent — пакеты вроде yesod-form и yesod-auth также отлично ведут взаимодействие с Persistent.

Примечание: Репозиторий, в каком проходят работы над переводом книжки, вы сможете отыскать на BitBucket. Как я вижу, все уже практически готово. Осталась всякая рутина типа перепроверки неких глав и остального в этом духе.

Издательство ДМК-Пресс уже выразило свою заинтригованность в печати книжки.

Похожие статьи

  • Работа с XML в Haskell и фреймворке Yesod

    Будет преувеличением сказать, что работы над переводом книжки о веб-фреймворке Yesod близятся к окончанию, но большая часть пути уже точно пройдена. В текущее время не переведено неско...

  • Интернационализация в Yesod

    А тем временем работы над русским переводом книжки «Developing Web Applications with Haskell and Yesod» идут с обезумевшой скоростью. В этой заметке я предлагаю вашему вниманию предварительный вариант перевод...

  • Разработка веб-приложений на Yesod Восемь тыщ двести двенадцать Введение

    Позвольте громозвучно объявить о том, что не так давно маленькая группа энтузиастов, посреди которых есть и я, начала работу над русским переводом книжки Developing Web Applications with Haskell and Yesod...

  • Поиск по веб-сайту с внедрением Sphinx в Yesod

    Признайтесь, вы было решили, что работы над русским переводом красивого тома «Developing Web Applications with Haskell and Yesod» в один момент тормознули? А ах так бы не так! Сейчас мне хоте...

  • Шпаргалка по работе с DBIxClass

    Отлично обмысленный ORM может значительно упростить жизнь программеру. Но если это так, то откуда берутся клики, что «ORM — это антипаттерн»? Думается, дело в том, что не все ORM идиентично неплохи...

Теги:
Рейтинг: +19 Голосов: 219 1458 просмотров
Комментарии (0)

Нет комментариев. Ваш будет первым!

Найти на сайте: параметры поиска

Windows 7

Среда Windows 7 на первых порах кажется весьма непривычной для многих.

Windows 8

Если резюмировать все выступления Microsoft на конференции Build 2013.

Windows XP

Если Windows не может корректно завершить работу, в большинстве случаев это

Windows Vista

Если к вашему компьютеру подключено сразу несколько мониторов, и вы регулярно...