Persistent

表单处理用户与应用间的边界问题。另一个需要我们处理的边界是应用与存储层之间的。 不管是SQL数据库、YAML文件、或是二进制blob,大部分情况下你的存储层都无法原生的 理解你应用中的数据类型,你会需要执行一些数据编组(marshaling)操作。Persistent是 Yesod针对数据存储的解决方案,它是一个用Haskell写的类型安全、通用的数据存储接口 。

Haskell有很多不同的数据库绑定(binding)库。然而,它们大部分都不掌握数据模型 (schema)信息,因此不能提供有用的静态类型保证。它们还强制程序员对不同数据库使用 不同的API和数据类型。

有些Haskell程序员尝试了一种更具革命性的方法:创建Haskell专用的数据存储,能够容 易的存储任何强类型Haskell数据。这种方法对某些特定的应用场景很好,但它将程序员 限制在其类库所提供的存储技术中,而且与其它编程语言的交互不友好。

相比之下,Persistent允许我们在现有的数据库中选择,这些数据库针对不同的数据存储 应用场景做了优化、能与其它编程语言交互、能使用安全高效的查询接口,同时仍能保持 Haskell数据的类型安全。

Persistent遵循的指导原则是类型安全和简洁、声明式的语法。其它很棒的特性包括:

  • 与数据库无关。对PostgreSQL、SQLite、MySQL和MongoDB提供一流的支持,对Redis的 支持还是实验性的。

  • 方便的数据建模。 Persistent允许你以类型安全的方式建模和使用数据关系。Persistent默认的类型安全 API不支持join操作,这样能支持更广泛的存储层。 Join和其它SQL专用功能可以通过原始SQL层(只有极少的类型安全)完成。 另一个库,Esqueleto,构建 在Persistent的数据模型之上,为join和SQL语句提供了类型安全

  • 自动执行数据库迁移(migration)。

Persistent与Yesod能很好配好,但它也完全可以作为单独的类库使用。本章大部分时候 都只讲Persistent本身。

概要

  1. {-# LANGUAGE EmptyDataDecls #-}
  2. {-# LANGUAGE FlexibleContexts #-}
  3. {-# LANGUAGE GADTs #-}
  4. {-# LANGUAGE OverloadedStrings #-}
  5. {-# LANGUAGE QuasiQuotes #-}
  6. {-# LANGUAGE TemplateHaskell #-}
  7. {-# LANGUAGE TypeFamilies #-}
  8. import Control.Monad.IO.Class (liftIO)
  9. import Database.Persist
  10. import Database.Persist.Sqlite
  11. import Database.Persist.TH
  12. share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
  13. Person
  14. name String
  15. age Int Maybe
  16. deriving Show
  17. BlogPost
  18. title String
  19. authorId PersonId
  20. deriving Show
  21. |]
  22. main :: IO ()
  23. main = runSqlite ":memory:" $ do
  24. runMigration migrateAll
  25. johnId <- insert $ Person "John Doe" $ Just 35
  26. janeId <- insert $ Person "Jane Doe" Nothing
  27. insert $ BlogPost "My fr1st p0st" johnId
  28. insert $ BlogPost "One more for good measure" johnId
  29. oneJohnPost <- selectList [BlogPostAuthorId ==. johnId] [LimitTo 1]
  30. liftIO $ print (oneJohnPost :: [Entity BlogPost])
  31. john <- get johnId
  32. liftIO $ print (john :: Maybe Person)
  33. delete janeId
  34. deleteWhere [BlogPostAuthorId ==. johnId]

解决边界问题

假设我们在一个SQL数据库中存储了人员信息。你的表可能是像这样的:

  1. CREATE TABLE person(id SERIAL PRIMARY KEY, name VARCHAR NOT NULL, age INTEGER)

如果你用的是PostgreSQL数据库,可以保证数据库绝不会在age字段存储任意的文本值。 (SQLite就不能保证这一点,不过让我们暂时忽略这点。)为了映射这个数据库表,你需要 创建这样的Haskell数据类型:

  1. data Person = Person
  2. { personName :: Text
  3. , personAge :: Int
  4. }

看起来所有数据都类型安全了:数据库模型与我们的Haskell数据类型相匹配、数据库能 保证无效数据不会被存储、一切都很棒。然而,直到:

  • 你想从数据库取出数据,数据库层给你返回的是无类型格式的数据。你想查询年龄在32

  • 岁以上的人,而你不小心在SQL语句中写成了“三十二”。猜猜会怎样 :编译没问题,你直到运行时都不会发现问题。

  • 你决定按字母顺序查询前10个人。没问题…直到你的SQL有拼写错误。再一次,你只有 运行时才能发现。

在动态语言中,对这些问题的解决方法是单元测试。对任何可能出错的地方,你都要 写一个测试用例。但我敢肯定你已经察觉到了,Yesod并不是这样来处理问题的。我们更 愿意利用Haskell的强类型来尽可能多的帮助我们,数据存储问题也不例外。

所以问题是:如何用Haskell的类型系统来拯救我们?

类型

像路由那样,要做到类型安全的数据访问本质上并不困难。它只是需要大量单调、容易出 错、样板式的代码。与往常一样,这意味着我们可以用类型系统来保证正确性。同时为了 避免重复代码,我们要用一些Haskell模板(Template Haskell)。

注意 早期版本的Persistent更大范围的使用了Haskell模板。从0.6版本开始,参照 groundhog包,persistent有了一个新架构。这种方法用影子类型(phantom types)来减轻 很多负担。

PersistValue是Persistent的基本单元。它是一个汇总类型(sum type),可以表示发 往数据库或从数据库读取的数据。它的定义是:

  1. data PersistValue = PersistText Text
  2. | PersistByteString ByteString
  3. | PersistInt64 Int64
  4. | PersistDouble Double
  5. | PersistRational Rational
  6. | PersistBool Bool
  7. | PersistDay Day
  8. | PersistTimeOfDay TimeOfDay
  9. | PersistUTCTime UTCTime
  10. | PersistZonedTime ZT
  11. | PersistNull
  12. | PersistList [PersistValue]
  13. | PersistMap [(Text, PersistValue)]
  14. | PersistObjectId ByteString -- ^ MongoDB后端专用

每一个Persistent后端都需要知道如何将相关的值转换成数据库所能理解的值。尽管如此 ,如果需要用这些基础类型来表达我们所有的数据会有点笨拙。下一个层次是 PersistField型类,它定义了任意Haskell数据与PersistValue相互转换的方法 。PersistField对应的是SQL数据库中的一列。在上面人员表的例子中,名字和年龄 都是PersistField。

为了与用户侧的代码关联起来,最后还有一个PersistEntity型类。 PersistEntity的实例对应的是SQL数据库中的一个表。这个类定义了很多函数和一些 关联类型。回顾一下,我们在Persistent和SQL数据库间有这样的对应关系:

SQL Persistent
Datatypes (VARCHAR, INTEGER, etc) PersistValue
Column PersistField
Table PersistEntity

代码生成(Code Generation)

为了保证PersistEntity的实例能正确与你的Haskell数据类型匹配,Persistent会负责( 实例化及生成Haskell数据类型)。从不要重复自己(DRY: Don’t Repeat Yourself)的角度 :你只需要定义一次实体。让我们看一个简单的例子:

  1. {-# LANGUAGE QuasiQuotes, TypeFamilies, GeneralizedNewtypeDeriving, TemplateHaskell, OverloadedStrings, GADTs #-}
  2. import Database.Persist
  3. import Database.Persist.TH
  4. import Database.Persist.Sqlite
  5. import Control.Monad.IO.Class (liftIO)
  6. mkPersist sqlSettings [persistLowerCase|
  7. Person
  8. name String
  9. age Int
  10. deriving Show
  11. |]

我们结合使用了Haskell模板与准引用(就像定义路由时那样):persistLowerCase是 一个准引用,它将空格敏感的语法转换为一列实体定义。“Lower case“指的是生成的表名 是小写的。在这个定义中,名为SomeTable的实体会变成表为some_table的SQL表 。你还可以用persistFileWith函数从外部文件定义实体。mkPersist接受一列实 体,并声明:

  • 给每个实体声明一个Haskell数据类型。

  • 将每个数据类型都声明成PersistEntity的实例。

上面的例子生成的代码会是这样的:

  1. {-# LANGUAGE TypeFamilies, GeneralizedNewtypeDeriving, OverloadedStrings, GADTs #-}
  2. import Database.Persist
  3. import Database.Persist.Sqlite
  4. import Control.Monad.IO.Class (liftIO)
  5. import Control.Applicative
  6. data Person = Person
  7. { personName :: !String
  8. , personAge :: !Int
  9. }
  10. deriving (Show, Read, Eq)
  11. type PersonId = Key Person
  12. instance PersistEntity Person where
  13. -- 一个广义代数数据类型(GADT: Generalized Algebraic Datatype)。
  14. -- 这提供给我们一种匹配字段和其数据类型的类型安全的方法。
  15. data EntityField Person typ where
  16. PersonId :: EntityField Person PersonId
  17. PersonName :: EntityField Person String
  18. PersonAge :: EntityField Person Int
  19. data Unique Person
  20. type PersistEntityBackend Person = SqlBackend
  21. toPersistFields (Person name age) =
  22. [ SomePersistField name
  23. , SomePersistField age
  24. ]
  25. fromPersistValues [nameValue, ageValue] = Person
  26. <$> fromPersistValue nameValue
  27. <*> fromPersistValue ageValue
  28. fromPersistValues _ = Left "Invalid fromPersistValues input"
  29. -- 每个字段的信息,在内部被用来生成SQL语句
  30. persistFieldDef PersonId = FieldDef
  31. (HaskellName "Id")
  32. (DBName "id")
  33. (FTTypeCon Nothing "PersonId")
  34. SqlInt64
  35. []
  36. True
  37. Nothing
  38. persistFieldDef PersonName = FieldDef
  39. (HaskellName "name")
  40. (DBName "name")
  41. (FTTypeCon Nothing "String")
  42. SqlString
  43. []
  44. True
  45. Nothing
  46. persistFieldDef PersonAge = FieldDef
  47. (HaskellName "age")
  48. (DBName "age")
  49. (FTTypeCon Nothing "Int")
  50. SqlInt64
  51. []
  52. True
  53. Nothing

你可能想到了,Person数据类型与Haskell模板中的定义高度一致。我们还通过一个 广义代数数据类型(GADT)给每个域一个单独的构造函数。这个GADT编码了实体类型和字段 的类型。我们在Persistent中会多次使用这些构造函数,比如当我们进行数据筛选时,要 保证筛选条件的类型与字段的类型一致。

我们可以像使用其它Haskell类型一样使用所生成的Person类型,可以将它传递给其 它Persistent函数。

  1. main = runSqlite ":memory:" $ do
  2. michaelId <- insert $ Person "Michael" 26
  3. michael <- get michaelId
  4. liftIO $ print michael

我们从标准的数据库连接代码开始讲。这个例子中,我们用的是单次连接函数。 Persistent也自带了连接池(connection pool)函数,是我们通常在生产环境要用的。

这个例子中,我们能看到这两个函数:insert在数据库中创建一条新的记录,并返回 它的ID。和Persistent中的所有要素一样,ID是类型安全的。我们会在后文详述ID是怎么 工作的。因此当你运行insert $ Person "Michael" 26时,它的返回值类型是 PersonId。

第二个函数是get,它尝试通过Id从数据库加载一个值。在Persistent中,你永 远不用担心你把键值用到错误的表上:试图使用PersonId从另一个实体(比如 House)加载数据,是无法编译通过的。

PersistStore

上例中最后一个没解释的细节是:runSqlite函数究竟做了什么操作,还有我们数据 库操作是运行在哪个monad里?

所有数据库操作都需要在PersistStore实例中。就像它的名字所说的一样,每一种数 据存储(PostgreSQL、SQLite、MongoDB)都有PersistStore的实例。就是在这里进行 所有PersistValue到数据库相关值的转换、SQL查询、等等。

注意 你可以想象,虽然PersistStore给外部世界提供了安全、类型完善的接口,还 是有很多数据库操作可能会出错。然而,通过在一个地方自动、彻底的测试代码,我们可 以将容易出错的代码集中化,并尽可能的保证没有bug。

runSqlite用提供的连接语句创建到数据库的单次连接。作为例子,我们使用了 :memory:,它是一个内存中的数据库。所有SQL后端都共用一个PersistSotre实 例:即SqlPersist。runSqlite通过所生成的连接值,来运行SqlPersist操 作。

注意 其实还有一些型类:PersistUpdate和PersistQuery。不同的型类提供了 不同的功能,这让我们可以给更简单的数据库(如Redis)写绑定库,即使这些数据库不提 供Persistent中所有的高级功能。

需要重点注意的一件事是在一条runSqlite语句中执行的所有操作都是在一个事务 (transaction)中运行。它说明两件重要的事:

  • 对很多数据库,提交一个事务是很耗费资源的。通过把多个操作放到一个事务中,你可 以大大加速代码运行。

  • 如果在runSqlite中抛出了异常,所有操作都会回滚(假设你的后端支持回滚的话)。

注意 这实际上比看起来有更深远的影响。很多Yesod中的短路函数,比如重定向 (redirect),是用异常来实现的。如果你在Persistent代码块中使用了这些函数,整个事 务都会回滚。

迁移

很抱歉告诉你,我对你撒了个小谎:上一节的例子实际上不能工作。如果你尝试运行它, 会得到错误消息:缺失表。

对于SQL数据库,一个主要的痛苦是管理数据定义的变更。Persistent可以帮忙,而不是 让用户去处理,但你需要要求它来帮忙。让我们看看代码:

  1. {-# LANGUAGE QuasiQuotes, TypeFamilies, GeneralizedNewtypeDeriving, TemplateHaskell,
  2. OverloadedStrings, GADTs, FlexibleContexts #-}
  3. import Database.Persist
  4. import Database.Persist.TH
  5. import Database.Persist.Sqlite
  6. import Control.Monad.IO.Class (liftIO)
  7. share [mkPersist sqlSettings, mkSave "entityDefs"] [persistLowerCase|
  8. Person
  9. name String
  10. age Int
  11. deriving Show
  12. |]
  13. main = runSqlite ":memory:" $ do
  14. -- 增加的就是这一行!
  15. runMigration $ migrate entityDefs $ entityDef (Nothing :: Maybe Person)
  16. michaelId <- insert $ Person "Michael" 26
  17. michael <- get michaelId
  18. liftIO $ print michael

仅仅是这一个小变化,Persistent就能自动为你创建Person表。runMigration和 migrate作为两个函数是为了让你能同时迁移多个表。

当只处理几个实体时,这样能行,但如果需要处理几十个实体就会很烦。Persistent有一 个辅助函数,mkMigrate,这样就就不用重复自己。

  1. {-# LANGUAGE QuasiQuotes, TypeFamilies, GeneralizedNewtypeDeriving, TemplateHaskell,
  2. OverloadedStrings, GADTs, FlexibleContexts #-}
  3. import Database.Persist
  4. import Database.Persist.Sqlite
  5. import Database.Persist.TH
  6. share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
  7. Person
  8. name String
  9. age Int
  10. deriving Show
  11. Car
  12. color String
  13. make String
  14. model String
  15. deriving Show
  16. |]
  17. main = runSqlite ":memory:" $ do runMigration migrateAll

mkMigrate是一个Haskell模板函数,它会创建一个新函数,新函数会自动对所有 persist块中定义的实体调用migratte。share函数只是一个小辅助函数,它 将persist块中的信息传递到每个Haskell模板函数,并拼接结果。

Persistent对于迁移期间可以执行的操作相当保守。它先从数据库加载表信息,完全以定 义好的SQL数据类型表示。然后将其与代码中的实体定义做比较。对于以下情况,它会自 动修改数据定义:

  • 字段的数据类型变更。然而,数据库可能会阻止修改,如果数据无法转义。

  • 新增了字段。然而,如果是非空(not 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语句,或将 迁移内容写入日志等。

唯一性

除了可以声明实体中的字段,你还可以声明唯一性约束。一个典型的例子是要求用户名唯 一。

  1. User
  2. username Text
  3. UniqueUsername username

每个字段的名字必须以小写字母开始,而唯一性约束必须以大写字母开始,因为在 Haskell中它是一个数据构造函数。

  1. {-# LANGUAGE QuasiQuotes, TypeFamilies, GeneralizedNewtypeDeriving, TemplateHaskell,
  2. OverloadedStrings, GADTs, FlexibleContexts #-}
  3. import Database.Persist
  4. import Database.Persist.Sqlite
  5. import Database.Persist.TH
  6. import Data.Time
  7. import Control.Monad.IO.Class (liftIO)
  8. share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
  9. Person
  10. firstName String
  11. lastName String
  12. age Int
  13. PersonName firstName lastName
  14. deriving Show
  15. |]
  16. main = runSqlite ":memory:" $ do
  17. runMigration migrateAll
  18. insert $ Person "Michael" "Snoyman" 26
  19. michael <- getBy $ PersonName "Michael" "Snoyman"
  20. liftIO $ print michael

为了声明字段组合的唯一性,我们在声明中增加一行。Persistent知道你是在定义一个唯 一性构造函数,因为那一行以大写字母开头。(构造函数后的)每个词都必须是实体中的字 段。

唯一性的主要限制是它只能被应用于非空字段。原因是SQL标准对于如何表达NULL的 唯一性很模糊(比如,NULL=NULL是真还是假?)。除了这个模糊性,大部分SQL引擎实 际上的规则与Haskell数据类型所想的相反(比如,PostgreSQL认为NULL=NULL为 假,而Haskell认为Nothing == Nothing为真)。

除了在数据库层面对数据一致性进行保证,唯一性限制还可以用来在你的Haskell代码中 执行特殊的查询,就像上面例子中的getBy函数。它借助Unique关联类型工作。 在上面的例子中,我们会得到一个新的构造函数:

  1. PersonName :: String -> String -> Unique Person

查询

基于你的目标是什么,可以有不同的方法来查询数据库。有些查询命令用数字ID,其它可 能用筛选。查询在返回结果的数量上也有差异:有些查询不会返回超过一个结果(如果查 询用的关键字是唯一的),而其它查询能返回很多结果。

Persistent因此提供了一些不同的查询函数。与往常一样,我们试图通过类型编码尽可能 多的不变量(invariants)。比如,一条查询如果只能返回0或1个结果,则用Maybe封 装,而能返回多个结果的查询,返回值的类型是列表。

用ID查询

在Persistent中最简单的查询是基于ID的。因为这个值有可能不存在,所以它的返回值封 装在Maybe中。

  1. personId <- insert $ Person "Michael" "Snoyman" 26
  2. maybePerson <- get personId
  3. case maybePerson of
  4. Nothing -> liftIO $ putStrLn "Just kidding, not really there"
  5. Just person -> liftIO $ print person

这对于提供像/person/5这样的URL的站点非常有用。然而,这样的话,我们通常不需 要考虑Maybe封装,只想要值,如果查询失败则返回404。幸运的是,get404(由 yesod-persistent包提供)函数能帮助我们。我们会在讲Persistent与Yesod集成时讲更多 细节。

通过唯一性约束查询

getBy和get几乎上一样,除了:

  • 它的参数是唯一性约束;也就是说,它接收Unique值,而不是ID。

  • 它返回一个Entity而不是一个值。Entity是ID和值的组合。

  1. personId <- insert $ Person "Michael" "Snoyman" 26
  2. maybePerson <- getBy $ UniqueName "Michael" "Snoyman"
  3. case maybePerson of
  4. Nothing -> liftIO $ putStrLn "Just kidding, not really there"
  5. Just (Entity personId person) -> liftIO $ print person

像get404一样,也有getBy404函数。

选择函数

极有可能,你会需要更强大的查询。你可能想查询年龄在一定岁数以上的所有人;所有蓝 色的汽车;没有用邮箱地址注册的用户。这些情况,你需要用选择函数。

所有的选择函数都有相似的接口,但输出略有不同:

函数名 返回值
selectSource 一个包含所有查询结果的ID和值的Source。这让你可以写流式代码 (streaming code)。 [caption="注意"] NOTE: Source是一个数据流,是conduit包的一部分。推荐阅读 School of Haskell conduit教程来开始。
selectList 一个包含所有查询结果的ID和值的列表。所有记录都会被载入到内存中。
selectFirst 如果查询成功,只取查询结果的第一个ID和值。
selectKeys 只返回键,而不返回值, 返回结果的类型是Source。

selectList是最常用的,因此我们专门讲解它。之后理解其它几个函数也很容易。

selectList有两个参数:一列Filter和一列SelectOpt。前者限制了结果所 需具有的特征;它允许等于、小于、在范围内等(限制条件)。SelectOpt提供了三种 功能:排序、限制返回结果的数量、结果偏移(offset)一定行数。

注意 结合使用返回数量限制(limits)和偏移量(offsets)非常重要;它允许在你的web应 用中有效的分页(pagination)。

让我们看一个筛选的例子,再来分析它。

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

这个例子很简单,我们只需讲三点:

  • PersonAge是一个关联影子类型(associated phantom type)的构造函数。这听起来 很可怕,但重点在于它唯一标识了“person”表的“age”列,而且它知道age列的类型是 Int。(这就是影子部分。)

  • 我们有很多Persistent筛选运算符。它们都很直接:只要在普通的关系运算符后加个点 。有三个需要注意的地方,我下面会讲。

  • 筛选条件是用逻辑与给合在一起,所以我们的限制条件意思是“年龄在25岁以上、在30 岁(含)以下”。我们稍后会介绍用逻辑或连接筛选条件。

有一个运算符的命名有点特别:“不等于”。我们用!=.,因为/=.被用作更新运算 符(表示“分离然后设置(divide-and-set)”,稍会后讲)。不用担心:如果你用错了,编译 器会报错。另外两个特殊的运算符是“在范围内”和“不在范围内”。他们分别是←.和 /←.(都以点结束)。

对于逻辑或连接筛选条件的情况,我们使用||.运算符。比如:

  1. people <- selectList
  2. ( [PersonAge >. 25, PersonAge <=. 30]
  3. ||. [PersonFirstName /<-. ["Adam", "Bonny"]]
  4. ||. ([PersonAge ==. 50] ||. [PersonAge ==. 60])
  5. )
  6. []
  7. liftIO $ print people

这个(完全胡谄)的例子说的是:查询年龄在26-30(含)间,或者名字既不是Adam也不是 Bonny,或者年龄是50或60岁的人。

选择选项(SelectOpt)

前面例子中selectList的第二个参数都是空列表。就是没有指明选项,意思是:按数 据库默认的方式排序、返回所有结果、不要跳过任何结果。一个SelectOpt有四个构 造函数,可以用来改变选择选项。

  • Asc
    在指定列以升序排序。它使用与筛选一样的影子类型,比如PersonAge。

  • Desc
    与Asc一样,不过是降序。

  • LimitTo
    接受一个整型参数。只返回不超过指定数量的结果。

  • OffsetBy
    接受一个整型参数。跳过指定数量的结果。

下面的代码定义了一个函数,它会将结果分页。它返回所有年龄在18岁及以上的人,然后 按年龄排序(年长的在前)。对于年龄相同的人,再按姓排序,最后按名排序。

  1. resultsForPage pageNumber = do
  2. let resultsPerPage = 10
  3. selectList
  4. [ PersonAge >=. 18
  5. ]
  6. [ Desc PersonAge
  7. , Asc PersonLastName
  8. , Asc PersonFirstName
  9. , LimitTo resultsPerPage
  10. , OffsetBy $ (pageNumber - 1) * resultsPerPage
  11. ]

操作(Manipulation)

查询只是任务的一半。我们还需要能够给数据库增加数据,或修改现有数据。

插入

能够查询、筛选数据库中的数据很好,但首先数据是怎么进到数据库的呢?答案是 insert函数。你给它一个值,它返回一个ID。

在这里,有必要解释一下Persistent背后的哲学。在很多其它的对象关系映射(ORM: Object-Relational Mapping)方案中,用来存放数据的数据类型是不透明的:你需要通过 他们定义好的接口来存取数据。而Persistent不是这样的,Persistent的做法是:我们完 全用的是普通的代数数据类型。这意味着你能得到所有(Haskell)的优点:模式匹配、 currying和所有你习惯的。

尽管如此,有一些事我们无法做到。举个例子,当Haskell中的记录值变更时,没有 办法自动更新数据库中对应的值。当然,Haskell自身的纯计算(purity)和不可变性 (immutability),意味着这种想法本身就没有多少意义,所以我也不会为此伤心落泪。

然而,有一个问题是初学者经常感到困扰的:为什么ID和值是完全分离的?将ID嵌入值似 乎非常合逻辑。换句话说,不写成这样:

  1. data Person = Person { name :: String }

而是写成

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

但是,这样做立即会有个问题:我们怎么执行insert?如果构造一个Person值需要ID ,而ID要通过插入才能得到,而插入又需要一个Person值,我们就陷入了无限循环。我们 可以用undefined来解决它,但那只是招来问题。

好,你说,让我们试试更安全的方法:

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

比起insert $ Person undefined "Michael",我当然更喜欢insert $ Person Nothing "Michael"。我们的类型还能更简单,对吧?比如selectList函数的返回 值会变成简单的[Person],而不是丑陋的[Entity SqlPersist Person]。

问题是“丑陋的”返回值却相当有用。Entity Person在类型层面清楚的说明我们在 处理一个数据库中的值。比如说我们想创建到另一个页面的链接,但需要用到PersonId( 我们稍后会看到这很常见)。Entity Person的形式明白无误的告诉我们这一信息;将 PersonId作为Person的记录,并用Maybe封装,意味着运行时要额外检查 Just,而不是能更好预防错误的编译时检查。

最后,将ID嵌入值会导致语义不匹配。Person是值。两个人(在数据库语境中)是一样 的如果它们的所有字段值都一样。如果把ID嵌入值,我们讨论的不再是一个人,而是数据 库的一行。相等不再是相等,而是一致:这是同一个人,而不是相同的人。

换句话说,将ID分离会有些恼人的地方,但总体上,它是正确的做法,它能在大的框 架上保证更好、更少bug的代码。

更新

现在,在以上讨论的基础上,让我们来想想数据更新。最简单的更新方法是:

  1. let michael = Person "Michael" 26
  2. michaelAfterBirthday = michael { personAge = 27 }

但这实际上没有更新任何值,它只是基于旧的创建了一个新的Person值。当我们说更 新,我们说的不是修改Haskell代码中的值。(我们最好不要,因为Haskell数据类型 是不可修改的。)

相反,我们要考虑修改数据表中行数据的方法。最简单的方法是用update函数。

  1. personId <- insert $ Person "Michael" "Snoyman" 26
  2. update personId [PersonAge =. 27]

update函数有两个参数:ID和一列Update操作。最简单的更新操作是赋值,但它 不总是最佳选择。如果你想把某些人的年龄加1,但你不知道他们当前的年龄呢? Persistent可以帮你:

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

你可能想到了,我们可以用所有基础的数学运算符:+=.、-=.、*=.和 /=.(句号)。这些对于更新一条记录的情况很方便,但它们对于保证ACID(Atomicity 、Consistency、Isolation、Durability)也非常重要。想象另一种情况:取出一个 Person值,增加他/她的年龄,把新的值更新到数据库。如果你有两个线程/进程同时 在读写数据库,你可能有危险(提示:资源竞态(race conditions))。

有时候你会想一次更新多个域(比如,给所有员工加薪5%)。updateWhere接受两个参 数:一列筛选条件和一列要应用的更新。

  1. updateWhere [PersonFirstName ==. "Michael"] [PersonAge *=. 2] -- 漫长的一天(章)

有时候,你只想将数据库中的一个值完全替换为另一个值。这种情况,你要用(惊喜 )replace函数。

  1. personId <- insert $ Person "Michael" "Snoyman" 26
  2. replace personId $ Person "John" "Doe" 20

删除

虽然数据库操作让我们头疼,但有时我们还是要和数据它们说再见。要删除它们,有三个 函数:

  • delete
    基于ID删除

  • deleteBy
    基于唯一约束删除

  • deleteWhere
    基于一列筛选条件删除

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

我们甚至可以用deleteWhere删除表中全部记录,我们只要给一些提示,让GHC知道我们感 兴趣的是哪个表就可以:

  1. deleteWhere ([] :: [Filter Person])

属性

目前为止,我们已经看到persistLowerCase块的基本语法:第一行指明实体的名字, 然后每个字段对应缩进的一行,每行两个词:字段名和类型。Persistent实际上可以做更 多:你可以在这两个词后指定任意的属性。

假设我们想让Person实体有一个(可选的)年龄字段和表示他/她何时加入系统的时间 戳字段。对于已经在数据库中的实体,则用当前时刻作为时间戳。

  1. {-# LANGUAGE QuasiQuotes, TypeFamilies, GeneralizedNewtypeDeriving, TemplateHaskell,
  2. OverloadedStrings, GADTs, FlexibleContexts #-}
  3. import Database.Persist
  4. import Database.Persist.Sqlite
  5. import Database.Persist.TH
  6. import Data.Time
  7. import Control.Monad.IO.Class
  8. share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
  9. Person
  10. name String
  11. age Int Maybe
  12. created UTCTime default=CURRENT_TIME
  13. deriving Show
  14. |]
  15. main = runSqlite ":memory:" $ do
  16. time <- liftIO getCurrentTime
  17. runMigration migrateAll
  18. insert $ Person "Michael" (Just 26) time
  19. insert $ Person "Greg" Nothing time

Maybe是自带的、单词(single word)属性。它让该字段可选。在Haskell中,这意味 着它用Maybe封装。在SQL中,它让列可空。

default属性与数据库后端有关,它使用任何能被数据库理解的语法。在这里,它用 了数据库自带的CURRENT_TIME函数。假设我们想加一个字段,用来表示这个人最喜欢 的编程语言:

  1. {-# LANGUAGE QuasiQuotes, TypeFamilies, GeneralizedNewtypeDeriving, TemplateHaskell,
  2. OverloadedStrings, GADTs, FlexibleContexts #-}
  3. import Database.Persist
  4. import Database.Persist.Sqlite
  5. import Database.Persist.TH
  6. import Data.Time
  7. share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
  8. Person
  9. name String
  10. age Int Maybe
  11. created UTCTime default=CURRENT_TIME
  12. language String default='Haskell'
  13. deriving Show
  14. |]
  15. main = runSqlite ":memory:" $ do
  16. runMigration migrateAll
注意 default属性对Haskell代码本身没有任何影响;你还是需要填充所有值。它只 会影响到数据库的数据定义及自动迁移。

我们需要将默认值用单引号包起来,这样数据库才能正确的解读它。最后,Persistent使 用双引号来包含有空格的值,因此,如果我们要将某人的默认家乡设置为“El Salvador” :

  1. {-# LANGUAGE QuasiQuotes, TypeFamilies, GeneralizedNewtypeDeriving, TemplateHaskell,
  2. OverloadedStrings, GADTs, FlexibleContexts #-}
  3. import Database.Persist
  4. import Database.Persist.Sqlite
  5. import Database.Persist.TH
  6. import Data.Time
  7. share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
  8. Person
  9. name String
  10. age Int Maybe
  11. created UTCTime default=now()
  12. language String default='Haskell'
  13. country String "default='El Salvador'"
  14. deriving Show
  15. |]
  16. main = runSqlite ":memory:" $ do
  17. runMigration migrateAll

最后一条关于属性的技巧是,你可以指定SQL中的表名和列名。对于与现有数据库交互的 情况很有用。

  1. share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
  2. Person sql=the-person-table id=numeric_id
  3. firstName String sql=first_name
  4. lastName String sql=fldLastName
  5. age Int Gt Desc "sql=The Age of the Person"
  6. UniqueName firstName lastName
  7. deriving Show
  8. |]

关于实体定义的语法还有一些其它特性。一个最新的特性列表在 Yesod维基 上。

关系

Persistent允许用与非关系型(non-SQL)数据库一致的方式在数据类型间做引用。我们通 过在相关实体中嵌入ID来实现。因此如果一个人有很多辆车:

  1. {-# LANGUAGE QuasiQuotes, TypeFamilies, GeneralizedNewtypeDeriving, TemplateHaskell,
  2. OverloadedStrings, GADTs, FlexibleContexts #-}
  3. import Database.Persist
  4. import Database.Persist.Sqlite
  5. import Database.Persist.TH
  6. import Control.Monad.IO.Class (liftIO)
  7. import Data.Time
  8. share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
  9. Person
  10. name String
  11. deriving Show
  12. Car
  13. ownerId PersonId Eq
  14. name String
  15. deriving Show
  16. |]
  17. main = runSqlite ":memory:" $ do
  18. runMigration migrateAll
  19. bruce <- insert $ Person "Bruce Wayne"
  20. insert $ Car bruce "Bat Mobile"
  21. insert $ Car bruce "Porsche"
  22. -- 还可以插入更多汽车
  23. cars <- selectList [CarOwnerId ==. bruce] []
  24. liftIO $ print cars

使用这项技术,你可以定义一对多的关系。要定义多对多的关系,我们需要连接(join)实 体,它会对每个表都使用一对多的联系。在这里使用唯一性约束也是好主意。比如,如果 我们要对一个人在哪个商店买了哪些东西建模:

  1. {-# LANGUAGE QuasiQuotes, TypeFamilies, GeneralizedNewtypeDeriving, TemplateHaskell,
  2. OverloadedStrings, GADTs, FlexibleContexts #-}
  3. import Database.Persist
  4. import Database.Persist.Sqlite
  5. import Database.Persist.TH
  6. import Data.Time
  7. share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
  8. Person
  9. name String
  10. Store
  11. name String
  12. PersonStore
  13. personId PersonId
  14. storeId StoreId
  15. UniquePersonStore personId storeId
  16. |]
  17. main = runSqlite ":memory:" $ do
  18. runMigration migrateAll
  19. bruce <- insert $ Person "Bruce Wayne"
  20. michael <- insert $ Person "Michael"
  21. target <- insert $ Store "Target"
  22. gucci <- insert $ Store "Gucci"
  23. sevenEleven <- insert $ Store "7-11"
  24. insert $ PersonStore bruce gucci
  25. insert $ PersonStore bruce sevenEleven
  26. insert $ PersonStore michael target
  27. insert $ PersonStore michael sevenEleven

深入理解类型

目前为止,我们提到了Person和PersonId,但并没真正解释它们是什么。在最简 单的情况下,对于一个SQL数据库,PersonId可以是type PersonId = Int64。然 而,这意味着无法在类型层面将PersonId与Person实体进行绑定。因此,你可能 不小心用PersonId去查询Car。为了建模这种关系,我们要使用影子类型。所以 ,我们幼稚的下一步是:

  1. newtype Key entity = Key Int64
  2. type PersonId = Key Person

这很好,直到我们使用的数据库后端不使用Int64来表示ID。这不只是理论上的问题; MongoDB用的就是ByteString。所以我们需要键值能包含Int或ByteString 。看上去应该用一个汇总类型:

  1. data Key entity = KeyInt Int64 | KeyByteString ByteString

但那只是自找麻烦。下一次我们会遇到一个后端使用时间戳作为ID,所以我们又会需要给 Key增加构造函数。这可以持续好一会。幸运的是,我们已经有一个用来表示任意数 据的汇总类型:PersistValue:

  1. newtype Key entity = Key PersistValue

但这样有另一个问题。假设我们有个web应用从用户那得到ID作为参数。它需要以 Text类型接收参数,然后尝试将其转为Key。好,这很简单:写一个将Text 转为PersistValue的函数,然后将结果用Key构造函数封装,对吗?

不对。我们试过这种方法,它有很大的问题。我们最后得到不可能有的Key。比如, 如果我们要用SQL,键必须是整数。但上面描述的方法可以允许任意的文本数据。结果是 服务器返回一堆500错误,因为数据库用整型列去和文本值做比较而抽风了。

所以我们需要一种将文本值转为Key的方法,但它要遵循数据库后端的规则。而且一旦定型 ,答案就很简单:增加另一个影子类型。Persistent中Key的真正定义是:

  1. newtype KeyBackend backend entity = Key { unKey :: PersistValue }
  2. type Key val = KeyBackend (PersistEntityBackend val) val

这个略微有点吓人的构造说的是:我们有一个KeyBackend类型,它有两个参数:数据 库后端和实体。然而,我们还有一个简化的Key类型,它假设实体和键的后端一 样,这通常也是正确的假设。

在实践中,它能很好工作:我们可以有一个Text → KeyBackend MongoDB entity函 数和一个Text → KeyBackend SqlPersist entity函数,然后所有事情都能流畅运行 。

更复杂、更通用

默认情况下,Persistent会根据使用的数据库后端硬编码你的数据类型。当使用 sqlSettings时,它是SqlBackend类型。但如果你希望你的Persistent代码可以 工作在多个后端上,你可以启用更加通用的类型,将sqlSettings替换为 sqlSettings { mpsGeneric = True }。

要理解为什么需要这么做,考虑关系。假设我们想表示博客和博客文章。我们可以这样定 义实体:

  1. Blog
  2. title Text
  3. Post
  4. title Text
  5. blogId BlogId

但用Key数据类型来表达会是怎样的呢?

  1. data Blog = Blog { blogTitle :: Text }
  2. data Post = Post { postTitle :: Text, postBlogId :: KeyBackend <这里放什么?> Blog }

我们需要填入后端类型。理论上,我们可以将其硬编码为SqlPersist或Mongo, 但那样我们的数据类型就只能工作在一种后端上。对于一个单独的应用,这样做是可以的 ,但如果是类库呢?它需要被多个应用使用,需要使用多种后端。

因此问题会更复杂一些。我们的类型实际上是:

  1. data BlogGeneric backend = Blog { blogTitle :: Text }
  2. data PostGeneric backend = Post { postTitle :: Text, postBlogId :: KeyBackend backend (BlogGeneric backend) }

注意,我们还是保留了构造函数和记录的短名。最后,为了给普通代码一个简单的接口, 我们定义一些类型别名:

  1. type Blog = BlogGeneric SqlPersist
  2. type BlogId = Key SqlPersist Blog
  3. type Post = PostGeneric SqlPersist
  4. type PostId = Key SqlPersist Post

不,SqlPersist没有硬编码进Persistent。在调用mkPersist时你已经传入了 sqlSettings,它告诉我们要用SqlPersist。Mongo代码会用mongoSettings 。

这可能有点复杂,但用户代码基本上不会碰到它们。回顾本章:我们没有一次需要直接处 理Key或Generic类型。它们最有可能会出现的地方是在编译器的错误消息中。因 此重点是知道它存在,但它不会影响你的日常使用。

自定义字段

有些时候,你会想要在数据库中自定义字段。最常见的情况是枚举,比如雇员状态。为此 ,Persistent提供了一个Haskell模板辅助函数:

  1. -- @Employment.hs
  2. {-# LANGUAGE TemplateHaskell #-}
  3. module Employment where
  4. import Database.Persist.TH
  5. data Employment = Employed | Unemployed | Retired
  6. deriving (Show, Read, Eq)
  7. derivePersistField "Employment"
  8. -- @Main.hs
  9. {-# LANGUAGE QuasiQuotes, TypeFamilies, GeneralizedNewtypeDeriving, TemplateHaskell,
  10. OverloadedStrings, GADTs, FlexibleContexts #-}
  11. import Database.Persist.Sqlite
  12. import Database.Persist.TH
  13. import Employment
  14. share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
  15. Person
  16. name String
  17. employment Employment
  18. |]
  19. main = runSqlite ":memory:" $ do
  20. runMigration migrateAll
  21. insert $ Person "Bruce Wayne" Retired
  22. insert $ Person "Peter Parker" Unemployed
  23. insert $ Person "Michael" Employed

derivePersistField用字符串字段将数据存入数据库,并用该类型的Show和 Read实例进行数据编组。这可能没有通过整型存储高效,但也更灵活:即使你以后增 加新的构造函数,你当前的数据仍然有效。

注意 在这个例子中,我们将定义分成了两个模块。需要这样做是由于GHC的编译步骤约 束,它本质上是说,在很多情况下,Haskell模板生成的代码不能在它所在的模块中使用 。

Persistent: 原始(raw)SQL

Persistent包提供了与数据库间的类型安全的接口。它试图与后端无关,比如不依赖于 SQL的关系型特性。我的经验是你可以用这个高层接口轻松执行95%的数据库操作。(实际 上,我写的大部分web应用都完全使用高层接口。)

但有时候你会想用某个后端专有的特性。我以前使用过的一个特性是全文搜索。这种情况 下,我们要用到SQL的“LIKE”运算符,Persistent没有建模它。假设我们要查询所有姓氏 为“Snoyman”的人,然后打印出结果。

注意 实际上,你可以用Persisten 0.6新增的特性直接用普通语法表示LIKE运算符 ,它会使用后端对应的运算符。但这仍然是一个(使用原始SQL的)好例子,所以让我们看 看。
  1. {-# LANGUAGE OverloadedStrings, TemplateHaskell, QuasiQuotes, TypeFamilies #-}
  2. {-# LANGUAGE GeneralizedNewtypeDeriving, GADTs, FlexibleContexts #-}
  3. import Database.Persist.TH
  4. import Data.Text (Text)
  5. import Database.Persist.Sqlite
  6. import Control.Monad.IO.Class (liftIO)
  7. import Data.Conduit
  8. import qualified Data.Conduit.List as CL
  9. share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
  10. Person
  11. name Text
  12. |]
  13. main :: IO ()
  14. main = runSqlite ":memory:" $ do
  15. runMigration migrateAll
  16. insert $ Person "Michael Snoyman"
  17. insert $ Person "Miriam Snoyman"
  18. insert $ Person "Eliezer Snoyman"
  19. insert $ Person "Gavriella Snoyman"
  20. insert $ Person "Greg Weber"
  21. insert $ Person "Rick Richardson"
  22. -- Persistent没有提供LIKE运算符,但我们希望查询整个Snoyman家族...
  23. let sql = "SELECT name FROM Person WHERE name LIKE '%Snoyman'"
  24. rawQuery sql [] $$ CL.mapM_ (liftIO . print)

此外还有支持自动数据编组的高层接口。详情请参阅Haddock API文档。

与Yesod集成

希望你已经信服Persistent的威力。如何将它与你的Yesod应用集成呢?如果你使用了脚 手架(scaffolding),大部分工作都已为你做好。但像本书通常所做的那样,我们要手动 来集成,以说明它到底是怎么工作的。

yesod-persistent包提供了Persistent和Yesod间的交汇点。它提供了YesodPersist 型类,它通过runDB方法标准化了存取数据库的操作。让我们看看代码。

  1. {-# LANGUAGE QuasiQuotes, TypeFamilies, GeneralizedNewtypeDeriving, FlexibleContexts #-}
  2. {-# LANGUAGE TemplateHaskell, OverloadedStrings, GADTs, MultiParamTypeClasses #-}
  3. import Yesod
  4. import Database.Persist.Sqlite
  5. import Control.Monad.Trans.Resource (runResourceT)
  6. import Control.Monad.Logger (runStderrLoggingT)
  7. -- 和之前一样定义我们的实体
  8. share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
  9. Person
  10. firstName String
  11. lastName String
  12. age Int Gt Desc
  13. deriving Show
  14. |]
  15. -- 我们将连接池放在基础数据类型中。在程序初始化时,我们就创建连接池,
  16. -- 每当要执行数据库操作时,就从连接池取出一个连接。
  17. data PersistTest = PersistTest ConnectionPool
  18. -- 我们只创建一条路由用于访问人员。在路由中使用Id类型非常常见。
  19. mkYesod "PersistTest" [parseRoutes|
  20. / HomeR GET
  21. /person/#PersonId PersonR GET
  22. |]
  23. -- 没什么特别的
  24. instance Yesod PersistTest
  25. -- 现在我们需要定义一个YesodPersist实例,它会记录我们使用的是哪个数据库后端,
  26. -- 以及怎么执行数据库操作
  27. instance YesodPersist PersistTest where
  28. type YesodPersistBackend PersistTest = SqlPersistT
  29. runDB action = do
  30. PersistTest pool <- getYesod
  31. runSqlPool action pool
  32. -- List all people in the database
  33. getHomeR :: Handler Html
  34. getHomeR = do
  35. people <- runDB $ selectList [] [Asc PersonAge]
  36. defaultLayout
  37. [whamlet|
  38. <ul>
  39. $forall Entity personid person <- people
  40. <li>
  41. <a href=@{PersonR personid}>#{personFirstName person}
  42. |]
  43. -- 我们返回字符串格式的人员信息,或者当人员在数据库中不存在时返回404
  44. getPersonR :: PersonId -> Handler String
  45. getPersonR personId = do
  46. person <- runDB $ get404 personId
  47. return $ show person
  48. openConnectionCount :: Int
  49. openConnectionCount = 10
  50. main :: IO ()
  51. main = withSqlitePool "test.db3" openConnectionCount $ \pool -> do
  52. runResourceT $ runStderrLoggingT $ flip runSqlPool pool $ do
  53. runMigration migrateAll
  54. insert $ Person "Michael" "Snoyman" 26
  55. warp 3000 $ PersistTest pool

这里有两个常用的信息。runDB用来在Handler中执行数据库操作。在runDB 中,你可以使用本章提到的任何操作函数,比如insert和selectList。

注意 runDB的类型是YesodDB site a → HandlerT site IO a。YesodDB的定义是 :
  1. type YesodDB site = YesodPersistBackend site (HandlerT site IO)
因为它构建于YesodPersistBackend的关联类型上,它使用了与当前站点一样的数据 库后端。

另一个新特性是get404。它与get一样,但当查询无结果时不是返回Nothing ,而是返回404错误页。getPersonR函数是真实世界Yesod应用中非常常用的方法: 用get404查询一个值,然后基于查询结果做出响应。

更复杂的SQL

Persistent努力做到与后端无关。这种方法的好处是代码可以很容易切换后端。不足是你 无法用一些后端专用的特性。可能受影响最大的是SQL的join操作。

幸运的是,得益于Felip Lessa,你可以吃一块蛋糕。 Esqueleto库提供了类型安全的 SQL查询,它使用现有的Persistent框架。这个包的Haddocks文档很好的介绍了它的用法 。而且因为它用了很多Persistent的概念,你掌握的大部分Persistent知识都能用上。

SQLite以外的数据库

为了让本章例子的简单,我们都用的SQLite后端。为了让事情圆满,下面是概要中例子的 PostgreSQL版本:

  1. {-# LANGUAGE FlexibleContexts #-}
  2. {-# LANGUAGE GADTs #-}
  3. {-# LANGUAGE OverloadedStrings #-}
  4. {-# LANGUAGE QuasiQuotes #-}
  5. {-# LANGUAGE TemplateHaskell #-}
  6. {-# LANGUAGE TypeFamilies #-}
  7. import Control.Monad.IO.Class (liftIO)
  8. import Database.Persist
  9. import Database.Persist.Postgresql
  10. import Database.Persist.TH
  11. share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
  12. Person
  13. name String
  14. age Int Maybe
  15. deriving Show
  16. BlogPost
  17. title String
  18. authorId PersonId
  19. deriving Show
  20. |]
  21. connStr = "host=localhost dbname=test user=test password=test port=5432"
  22. main :: IO ()
  23. main = withPostgresqlPool connStr 10 $ \pool -> do
  24. flip runSqlPersistMPool pool $ do
  25. runMigration migrateAll
  26. johnId <- insert $ Person "John Doe" $ Just 35
  27. janeId <- insert $ Person "Jane Doe" Nothing
  28. insert $ BlogPost "My fr1st p0st" johnId
  29. insert $ BlogPost "One more for good measure" johnId
  30. oneJohnPost <- selectList [BlogPostAuthorId ==. johnId] [LimitTo 1]
  31. liftIO $ print (oneJohnPost :: [Entity BlogPost])
  32. john <- get johnId
  33. liftIO $ print (john :: Maybe Person)
  34. delete janeId
  35. deleteWhere [BlogPostAuthorId ==. johnId]

小结

Persistent将Haskell的类型安全引入数据存储层。与其写一些容易出错、无类型的数据 访问或手写数据编组代码,你可以依靠Persistent帮你自动完成这些过程。

Persistent的目标是提供你所需要的一切功能,在大多数时候。当你需要一些更强大 的功能时,Persistent允许你直接访问底层的数据库,所以如果你想的话,可以写一个5 路(5-way)join运算。

Persistent可以直接集成到Yesod的工作流中。不仅yesod-persistent包提供了很多 辅助函数,yesod-form和yesod-auth包也使用了一些Persistent的功能。