Yesod - 数据库 (9)
2018-09-02 17:54
369 查看
表单处理用户和应用程序之间的关系。我们需要处理的另一个是应用程序和存储层之间的关系。无论是SQL数据库,YAML文件还是二进制blob,您的存储层无法理解您的应用程序的数据类型,并且您需要执行一些转换操作。
Haskell有许多数据库绑定。但是,其中大多数对模式知之甚少,因此不提供有用的静态保证。它们还强制程序员使用依赖于数据库的API和数据类型。
一些Haskellers尝试了一条更具革命性的路线:创建Haskell特定的数据存储,允许用户轻松存储任何强类型的Haskell数据。这些选项对于某些用例非常有用,但它们将一个选项限制在库提供的存储技术中,并且与其他语言不能很好地连接。
相比之下,
数据库无关。支持PostgreSQL,SQLite,MySQL和MongoDB,支持实验性质的Redis。
方便的数据建模。Persistent允许您建模并以类型安全的方式使用它们。默认类型安全的
在非生产环境中自动迁移数据库以加快开发速度。
上面代码段中的类型注释不需要让您的代码进行编译,而是用于向读者阐明每个值的类型。
如果您使用的是像PostgreSQL这样的数据库,则可以保证数据库永远不会在您的年龄字段中存储一些任意文本。(同样SQLite则不太可能保证,但让我们暂时忘掉它。) 要映射此数据库表,您可能会创建一个类似于以下内容的Haskell数据类型:
看起来一切都是类型安全的:数据库模式匹配我们的Haskell数据类型,数据库确保无效数据永远不会进入我们的数据存储,一切都很棒。好吧,直到:
你希望从数据库中提取数据,数据库层以非类型格式提供数据。
* 您希望找到32岁以上的所有人,并且您不小心在SQL语句中写了“threetwo”。猜猜看:编译没问题,直到运行时才会发现有问题。
* 您决定要按字母顺序查找前10个人。没问题......直到你在SQL中输入错字。再一次,直到运行时才发现。
在动态语言中,这些问题的答案是单元测试。对于任何可能出错的一切,请确保编写测试用例。但是,我相信你现在已经知道了,这与Yesod的方法并不相符。我们喜欢利用Haskell强大的类型系统来尽可能地保护我们,数据存储也不例外。
所以问题仍然存在:我们如何使用Haskell的类型系统来挽救这一天?
每个
为了绑定客户端代码,我们的最后一个类型类是PersistEntity。
我们使用Template Haskell和Quasi-Quotation的组合(就像定义路由时一样):
* 每个实体对应一个Haskell数据类型。
* 为每个数据类型实现PersistEntity类型类。
上面的示例生成的代码如下所示:
正如您所见,我们的Person数据类型与我们在原始Template Haskell版本中给出的定义非常匹配。我们还有一个广义代数数据类型(GADT),它为每个字段提供单独的构造函数。该GADT编码实体的类型和字段的类型。我们在Persistent中使用它的构造函数,例如确保在应用
我们可以像使用任何其他Haskell类型一样使用生成的Person类型,然后将其传递给其他Persistent函数。
此代码编译,但会生成有关丢失表的运行时异常。我们将在下面解释并解决这个问题。
我们从一个标准的数据库连接代码开始。在这种情况下,我们使用了单连接功能。Persistent还内置了连接池功能,我们通常希望在生产中使用它们。
在这个例子中,我们看到了两个函数:
我们看到的下一个函数是get,它尝试使用Id从数据库加载值。在Persistent中,您永远不必担心您使用的是错误表中的密钥:尝试使用PersonId加载不同的实体(如House)永远不会编译。
所有数据库操作都需要一个参数,该参数是PersistStore的一个实例。顾名思义,每个数据存储(PostgreSQL,SQLite,MongoDB)都有一个PersistStore实例。这是从PersistValue到特定于数据库的值的所有转换,SQL查询生成发生的地方,等等。
可以想象,尽管PersistStore为外部世界提供了安全,良好类型的接口,但仍有许多数据库交互可能出错。但是,通过在一个位置自动彻底地测试此代码,我们可以集中容易出错的代码,并确保它尽可能没有错误。
实际上还有一些其他类型类:PersistUpdate和PersistQuery。不同的类型类提供不同的功能,这允许我们编写使用更简单的数据存储(例如,Redis)的后端,即使它们无法为我们提供Persistent中可用的所有高级功能。
需要注意的一件重要事情是,在一次调用runSqlite中发生的所有事情都在一个事务中运行。这有两个重要的含义:
对于许多数据库,提交事务可能是一项代价高昂的活动。通过将多个步骤放入单个事务中,您可以显着加快代码速度。
如果在对runSqlite的单个调用中的任何地方抛出异常,则将回滚所有操作(假设您的后端具有回滚支持)。
这实际上比最初看起来有更深远的影响。 Yesod中的许多短路功能(例如重定向)是使用异常实现的。如果您在Persistent块内使用此类调用,它将回滚整个事务。
##迁移
很抱歉告诉你,刚才我撒了一个谎:上一节中的示例实际上不起作用。如果您尝试运行它,您将收到有关丢失表的错误消息。
对于SQL数据库,主要的难点之一是管理模式更改。Persistent不是将其留给用户,而是提供帮助,但您必须要求它提供帮助。让我们看看这是什么样的:
通过这一小段代码更改,Persistent将自动为您创建Person表。
仅在开发环境中建议使用自动数据库迁移。不鼓励您允许应用程序在生产环境中修改数据库模式。自动迁移可用于帮助加快开发速度,但不能替代在生产部署之前进行的人工审查和测试。
这只适用于处理几个实体,但一旦我们处理了十几个实体,就会很快变得烦人。Persistent提供了一个辅助函数,
mkMigrate是一个Template Haskell函数,它创建一个新函数,它将自动调用在persist块中定义的所有实体上的迁移。
Persistent对于迁移过程中会做什么有非常保守的规则。
它首先从数据库加载表信息,完成所有已定义的SQL数据类型。然后将其与代码中给出的实体定义进行比较。对于以下情况,它将自动更改架构:
字段的数据类型已更改。但是,如果无法翻译数据,则数据库可能会反对此修改。
添加了一个字段。但是,如果该字段不为null,则不提供默认值(我们稍后将讨论默认值)并且数据库中已存在数据,数据库将不允许这种情况发生。
字段从非null转换为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或仅记录发生的迁移可能很有用。
虽然每个字段名称必须以小写字母开头,但唯一性约束必须以大写字母开头,因为它将在Haskell中表示为数据构造函数。
要声明一个唯一的字段组合,我们在声明中添加一个额外的行。Persistent知道它正在定义一个唯一的构造函数,因为该行以大写字母开头。每个后续单词必须是此实体中的一个字段。
唯一性的主要限制是它只能应用于非空字段。这样做的原因是SQL标准在如何将唯一性应用于NULL方面不明确(例如,NULL = NULL true或false?)。除了这种歧义之外,大多数SQL引擎实际上实现了与Haskell数据类型预期相反的规则(例如,PostgreSQL说NULL = NULL是假的,而Haskell说Nothing == Nothing是True)。
除了在数据库级别提供有关数据一致性的良好保证之外,还可以使用唯一性约束在Haskell代码中执行某些特定查询,如上面演示的getBy。这通过Unique关联类型发生。在上面的例子中,我们最终得到了一个新的构造函数:
使用MongoDB后端,无法创建唯一性约束:您必须在该字段上放置唯一索引。
因此,Persistent提供了一些不同的查询功能。像往常一样,我们尝试在类型中编码尽可能多的安全。例如,只返回0或1结果的查询将使用Maybe包装器,而返回许多结果的查询将返回列表。
这对于
它需要一个唯一性约束;也就是说,它通过唯一值替换ID值。
它返回一个
与get404一样,还有一个getBy404函数。
所有select函数都使用类似的接口,只是输出略有不同:
selectList有两个参数:一个
让我们直接跳到一个过滤的例子,然后分析它。
2.我们有一堆Persistent过滤运算符。
过滤器列表是AND,因此我们的约束意味着“年龄大于25且年龄小于或等于30”。我们稍后会描述ORing。
我们使用
关于OR,我们使用
这个例子的意思是找到大于25岁小于30岁的人,或者找到名称是
###Desc
与Asc相同,按降序排列。
以下代码定义了一个将结果分页的函数。它返回所有18岁及以上的人,然后按年龄(最老的人)排序。对于年龄相同的人,他们按姓氏按字母顺序排序,然后按名字排序。
在这一点上,解释一下Persistent背后的哲学是有道理的。在许多其他ORM解决方案中,用于保存数据的数据类型是不透明的:你需要通过他们定义的接口来获取和修改数据。Persistent的情况并非如此:我们使用普通的代数数据类型来处理整个问题。这意味着你仍然可以使用模式匹配,currying以及你习惯的其他一切的功能。
但是,也有一些我们做不到的事情。例如,每次在Haskell中更新记录时,都无法自动更新数据库中的值。当然,凭借Haskell的纯正和不变性的正常立场,无论如何这都没有多大意义,所以我不会为此伤心。
然而,有一个问题是新手经常被困扰:为什么ID和值完全分开?似乎将ID嵌入到值中是非常合乎逻辑的。换句话说,而不是:
而是这样
嗯,这有一个问题就是:我们如何进行插入?如果一个Person需要一个ID,但是我们是通过插入获取的ID,但是插入又需要一个Person,那么我们就碰到了死循环。我们可以用undefined来解决这个问题,但这只是在自找麻烦。
好吧,你说,让我们尝试一些更安全的东西:
我绝对更喜欢
问题是“丑陋”是有用的。
最后,将ID嵌入值中会出现语义不匹配。
换句话说,将ID分离出来会有一些烦恼,但总的来说,这是正确的方法,在宏观方案中会导致更好,更少错误的代码。
但实际上并没有更新任何东西,它只是根据旧的值创建一个新的Person值。当我们说更新时,我们不是在谈论对Haskell中的值的修改。 (我们最好不要这样,因为Haskell中的数据是不可变的。)
相反,我们正在研究的是修改表中行的方法。最简单的方法是使用更新功能。
update有两个参数:一个ID和一个
正如您所料,我们拥有所有基本的数学运算符:
有时您会想要一次更新多行(例如,让所有员工加薪5%)。
有时,您只想用不同的值完全替换数据库中的值。为此,您使用替换功能。
我们甚至可以使用deleteWhere来消除表中的所有记录,我们只需要向GHC提供一些关于我们感兴趣的表的类型签名:
假设我们想要一个具有可选年龄的Person实体,以及记录添加到系统时的时间戳。对于已存在于数据库中的实体,我们希望使用当前时间。
默认属性对Haskell代码本身完全没有影响;你仍然需要填写所有值。这只会影响数据库架构和自动迁移。
我们需要用单引号括起字符串,以便数据库可以正确解释它。最后,Persistent可以使用双引号来包含空格,因此如果我们想将某人的默认本国设置为萨尔瓦多:
可以对属性执行的最后一个技巧是指定要用于SQL表和列的名称。在与现有数据库交互时,这很方便。
实体定义语法还有许多其他功能。Persistent文档 中维护了最新列表。
使用此技术,您可以定义一对多关系。要定义多对多关系,我们需要一个连接实体,它与每个原始表具有一对多的关系。在这些上使用唯一性约束也是一个好主意。例如,要模拟我们想要跟踪哪些人在哪些商店购物的情况:
这很好,直到你得到一个不使用Int64作为其ID的后端。这不仅仅是一个理论问题;MongoDB使用ByteStrings代替。所以我们需要的是一个可以包含Int和ByteString的键值。对于和类型来说似乎是一个美好的时光:
但这只是在寻找麻烦。接下来我们将有一个使用时间戳的后端,因此我们需要向Key添加另一个构造函数。这可能会持续一段时间。幸运的是,我们已经有一个用于表示任意数据的和类型:PersistValue:
这是(或多或少)Persistent在2.0版之前所做的事情。但是,这有一个不同的问题:它会抛弃数据。例如,在处理SQL数据库时,我们知道密钥类型将是Int64(假设正在使用默认值)。但是,您无法在具有此构造的类型级别断言。因此,从Persistent 2.0开始,我们现在在PersistEntity类中使用关联的数据类型:
当您使用SQL后端并且未使用自定义键类型时,这将成为Int64的新类型包装器,并且toSqlKey / fromSqlKey函数可以为您执行类型安全的转换。另一方面,使用MongoDB,它是ByteString的包装器。
要了解为什么这是必要的,请考虑关系。假设我们想要代表博客和博客文章。我们将使用实体定义:
我们知道BlogId只是Key Blog的一个类型同义词,但是如何定义Key Blog?我们不能使用Int64,因为它不适用于MongoDB。我们不能使用ByteString,因为这对SQL数据库不起作用。
为了实现这一点,一旦将mpsGeneric设置为True,输出的数据类型就会有一个类型参数来指示它们使用的数据库后端,以便可以正确编码密钥。这看起来像:
请注意,我们仍然保留构造函数和记录的短名称。最后,为了给普通代码提供一个简单的接口,我们定义了一些类型的同义词:
不,SqlBackend在任何地方都没有硬编码到Persistent中。您传递给mkPersist的sqlSettings参数告诉我们使用SqlBackend。 Mongo代码将使用mongoSettings。
这可能在表面上非常复杂,但用户代码几乎没有涉及到这一点。回顾整个章节:我们不是一次需要直接处理Key或Generic的东西。它弹出的最常见位置是编译器错误消息。所以重要的是要意识到这存在,但它不应该影响你的日常生活。
在这种情况下,我们将定义分为两个单独的模块。由于GHC阶段限制,这是必要的,这实际上意味着,在许多情况下,模板Haskell生成的代码不能在其创建的同一模块中使用。
但偶尔你会想要使用特定于后端的功能。我过去使用的一个功能是全文搜索。在这种情况下,我们将使用SQL“LIKE”运算符,该运算符未在Persistent中建模。我们将为所有人命名为“Snoyman”并打印出来的记录。
实际上,由于Persistent 0.6中添加了一个允许特定于后端的运算符的功能,因此可以直接以正常语法表示LIKE运算符。但这仍然是一个很好的例子,所以让我们继续吧。
还有更高级别的支持,允许自动数据封送。有关更多详细信息,请参阅Haddock API文档。
这里有两个重要的部分供一般使用。 runDB用于从Handler中运行DB操作。在runDB中,您可以使用我们目前所说的任何函数,例如insert和selectList。
runDB的类型是
由于它构建在YesodPersistBackend关联类型之上,因此它使用基于当前站点的相应数据库后端。
另一个新功能是get404。它就像get一样工作,但是当找不到结果时,它不会返回Nothing,而是返回404消息页面。 getPersonR函数是在现实世界的Yesod应用程序中使用的一种非常常见的方法:get404一个值,然后根据它返回响应。
幸运的是,感谢Felipe Lessa。Esqueleto库使用现有的Persistent基础结构为编写类型安全的SQL查询提供支持。该软件包的Haddocks为其使用提供了很好的介绍。由于它使用了许多持久性概念,因此大多数现有的持久性知识都应该轻松转移。
有关使用Esqueleto的简单示例,请参阅SQL联接章节。
目标是在大多数情况下提供您需要的一切。对于需要更强大功能的时候,Persistent允许您直接访问底层数据存储,因此您可以编写所需的任何5向连接。
Persistent直接集成到Yesod工作流程中。像yesod-persistent这样的帮助程序包不仅提供了一个很好的层,而像yesod-form和yesod-auth这样的包也可以利用Persistent的功能。
有关实体声明,数据库连接等语法的更多信息,请访问https://github.com/yesodweb
Persistent是Yesod对数据存储的解决方案, 一种用于Haskell的类型安全的通用数据存储接口。
Haskell有许多数据库绑定。但是,其中大多数对模式知之甚少,因此不提供有用的静态保证。它们还强制程序员使用依赖于数据库的API和数据类型。
一些Haskellers尝试了一条更具革命性的路线:创建Haskell特定的数据存储,允许用户轻松存储任何强类型的Haskell数据。这些选项对于某些用例非常有用,但它们将一个选项限制在库提供的存储技术中,并且与其他语言不能很好地连接。
相比之下,
Persistent允许我们在现有数据库中进行选择,这些数据库针对不同的数据存储用例进行了高度调整,可与其他编程语言进行互操作,并使用安全且高效的查询接口,同时仍保持Haskell数据类型的类型安全性。
Persistent遵循类型安全和简洁,声明性语法的指导原则。一些不错的功能是:
数据库无关。支持PostgreSQL,SQLite,MySQL和MongoDB,支持实验性质的Redis。
方便的数据建模。Persistent允许您建模并以类型安全的方式使用它们。默认类型安全的
persistent不支持
joins,但是允许更多的存储层。
Joins和其他SQL特殊的功能可以通过原生的SQL层实现(类型安全性很小)。另一个库Esqueleto构建在Persistent数据模型之上,添加了类型安全的joins和SQL功能。
在非生产环境中自动迁移数据库以加快开发速度。
persistent与Yesod一起结合的很好,但它本身也可以作为一个独立的库使用。本章的大部分内容将单独讨论Persistent。
概要
以下所需的依赖项是:persistent,
persistent-sqlite和
persistent-template。
{-# LANGUAGE EmptyDataDecls #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} import Control.Monad.IO.Class (liftIO) import Database.Persist import Database.Persist.Sqlite import Database.Persist.TH share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase| Person name String age Int Maybe deriving Show BlogPost title String authorId PersonId deriving Show |] main :: IO () main = runSqlite ":memory:" $ do runMigration migrateAll johnId <- insert $ Person "John Doe" $ Just 35 janeId <- insert $ Person "Jane Doe" Nothing insert $ BlogPost "My fr1st p0st" johnId insert $ BlogPost "One more for good measure" johnId oneJohnPost <- selectList [BlogPostAuthorId ==. johnId] [LimitTo 1] liftIO $ print (oneJohnPost :: [Entity BlogPost]) john <- get johnId liftIO $ print (john :: Maybe Person) delete janeId deleteWhere [BlogPostAuthorId ==. johnId]
上面代码段中的类型注释不需要让您的代码进行编译,而是用于向读者阐明每个值的类型。
解决边界问题
假设您正在SQL数据库中存储人员数据。你的table可能看起来像:
CREATE TABLE person(id SERIAL PRIMARY KEY, name VARCHAR NOT NULL, age INTEGER)
如果您使用的是像PostgreSQL这样的数据库,则可以保证数据库永远不会在您的年龄字段中存储一些任意文本。(同样SQLite则不太可能保证,但让我们暂时忘掉它。) 要映射此数据库表,您可能会创建一个类似于以下内容的Haskell数据类型:
data Person = Person { personName :: Text , personAge :: Int }
看起来一切都是类型安全的:数据库模式匹配我们的Haskell数据类型,数据库确保无效数据永远不会进入我们的数据存储,一切都很棒。好吧,直到:
你希望从数据库中提取数据,数据库层以非类型格式提供数据。
* 您希望找到32岁以上的所有人,并且您不小心在SQL语句中写了“threetwo”。猜猜看:编译没问题,直到运行时才会发现有问题。
* 您决定要按字母顺序查找前10个人。没问题......直到你在SQL中输入错字。再一次,直到运行时才发现。
在动态语言中,这些问题的答案是单元测试。对于任何可能出错的一切,请确保编写测试用例。但是,我相信你现在已经知道了,这与Yesod的方法并不相符。我们喜欢利用Haskell强大的类型系统来尽可能地保护我们,数据存储也不例外。
所以问题仍然存在:我们如何使用Haskell的类型系统来挽救这一天?
Types
与路由一样,类型安全的数据访问本身并不困难。他只是需要你写一些单调易错的代码。像往常一样,这意味着我们可以使用类型系统来保持良好。为了避免这些苦差事,我们将使用Tmplate Haskell。
PersistValue是
Persistent的基本单元。它是一种sum类型,可以表示发送到数据库和从数据库发送的数据。它的定义是:
data PersistValue = PersistText Text | PersistByteString ByteString | PersistInt64 Int64 | PersistDouble Double | PersistRational Rational | PersistBool Bool | PersistDay Day | PersistTimeOfDay TimeOfDay | PersistUTCTime UTCTime | PersistNull | PersistList [PersistValue] | PersistMap [(Text, PersistValue)] | PersistObjectId ByteString -- ^ Intended especially for MongoDB backend | PersistDbSpecific ByteString -- ^ Using 'PersistDbSpecific' allows you to use types -- specific to a particular backend
每个
Persistent backend都需要知道如何将相关值转换为数据库可以理解的内容。但是,仅仅根据这些基本类型来表达我们的所有数据是很尴尬的。下一个是
PersistField类型类,它定义了如何将任意Haskell数据类型和
PersistValue互相关联。
PersistField与SQL数据库中的列相关联。在我们上面的示例中,名称和年龄将是我们的
PersistFields。
为了绑定客户端代码,我们的最后一个类型类是PersistEntity。
PersistEntity的实例与SQL数据库中的表相关联。这个类型类定义了许多函数和一些相关的类型。我们在Persistent和SQL之间有以下对应关系:
SQL | Persistent |
---|---|
Datatypes (VARCHAR, INTEGER, etc) | PersistValue |
Column | PersistField |
Table | PersistEntity |
代码生成
为了确保PersistEntity实例与Haskell数据类型正确匹配,Persistent负责这两者。从DRY(不要重复自己)的角度来看,这也很好:您只需要定义一次实体。我们来看一个简单的例子:{-# LANGUAGE GADTs #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} import Database.Persist import Database.Persist.TH import Database.Persist.Sqlite import Control.Monad.IO.Class (liftIO) mkPersist sqlSettings [persistLowerCase| Person name String age Int deriving Show |]
我们使用Template Haskell和Quasi-Quotation的组合(就像定义路由时一样):
persistLowerCase是一个
quasi-quoter,它将空白敏感语法转换为实体定义列表。"Lower case" 是指生成的表名的格式。在这个方案中,像
SomeTable这样的实体将成为SQL表
some_table。您还可以使用
persistFileWith在单独的文件中声明实体。
mkPersist获取实体列表并声明:
* 每个实体对应一个Haskell数据类型。
* 为每个数据类型实现PersistEntity类型类。
上面的示例生成的代码如下所示:
{-# LANGUAGE TypeFamilies, GeneralizedNewtypeDeriving, OverloadedStrings, GADTs #-} import Database.Persist import Database.Persist.Sqlite import Control.Monad.IO.Class (liftIO) import Control.Applicative data Person = Person { personName :: !String , personAge :: !Int } deriving Show type PersonId = Key Person instance PersistEntity Person where newtype Key Person = PersonKey (BackendKey SqlBackend) deriving (PersistField, Show, Eq, Read, Ord) -- A Generalized Algebraic Datatype (GADT). -- This gives us a type-safe approach to matching fields with -- their datatypes. data EntityField Person typ where PersonId :: EntityField Person PersonId PersonName :: EntityField Person String PersonAge :: EntityField Person Int data Unique Person type PersistEntityBackend Person = SqlBackend toPersistFields (Person name age) = [ SomePersistField name , SomePersistField age ] fromPersistValues [nameValue, ageValue] = Person <$> fromPersistValue nameValue <*> fromPersistValue ageValue fromPersistValues _ = Left "Invalid fromPersistValues input" -- Information on each field, used internally to generate SQL statements persistFieldDef PersonId = FieldDef (HaskellName "Id") (DBName "id") (FTTypeCon Nothing "PersonId") SqlInt64 [] True NoReference persistFieldDef PersonName = FieldDef (HaskellName "name") (DBName "name") (FTTypeCon Nothing "String") SqlString [] True NoReference persistFieldDef PersonAge = FieldDef (HaskellName "age") (DBName "age") (FTTypeCon Nothing "Int") SqlInt64 [] True NoReference
正如您所见,我们的Person数据类型与我们在原始Template Haskell版本中给出的定义非常匹配。我们还有一个广义代数数据类型(GADT),它为每个字段提供单独的构造函数。该GADT编码实体的类型和字段的类型。我们在Persistent中使用它的构造函数,例如确保在应用
filter时,过滤值的类型与字段匹配。此实体的数据库主键还有另一个关联的新类型。
我们可以像使用任何其他Haskell类型一样使用生成的Person类型,然后将其传递给其他Persistent函数。
{-# LANGUAGE EmptyDataDecls #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} import Control.Monad.IO.Class (liftIO) import Database.Persist import Database.Persist.Sqlite import Database.Persist.TH import Control.Monad.IO.Unlift import Data.Text import Control.Monad.Reader import Control.Monad.Logger import Conduit share [mkPersist sqlSettings, mkSave "entityDefs"] [persistLowerCase| Person name String age Int Maybe deriving Show |] runSqlite' :: (MonadUnliftIO m) => Text -> ReaderT SqlBackend (NoLoggingT (ResourceT m)) a -> m a runSqlite' = runSqlite main :: IO () main = runSqlite' ":memory:" $ do michaelId <- insert $ Person "Michael" $ Just 26 michael <- get michaelId liftIO $ print michael
此代码编译,但会生成有关丢失表的运行时异常。我们将在下面解释并解决这个问题。
我们从一个标准的数据库连接代码开始。在这种情况下,我们使用了单连接功能。Persistent还内置了连接池功能,我们通常希望在生产中使用它们。
在这个例子中,我们看到了两个函数:
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为外部世界提供了安全,良好类型的接口,但仍有许多数据库交互可能出错。但是,通过在一个位置自动彻底地测试此代码,我们可以集中容易出错的代码,并确保它尽可能没有错误。
runSqlite使用其提供的连接字符串创建与数据库的单个连接。对于我们的测试用例,我们将使用
:memory:,它使用内存数据库。所有SQL后端共享相同的PersistStore实例:SqlBackend。
runSqlite通过
runReaderT将
SqlBackend值作为环境参数提供给操作。
实际上还有一些其他类型类:PersistUpdate和PersistQuery。不同的类型类提供不同的功能,这允许我们编写使用更简单的数据存储(例如,Redis)的后端,即使它们无法为我们提供Persistent中可用的所有高级功能。
需要注意的一件重要事情是,在一次调用runSqlite中发生的所有事情都在一个事务中运行。这有两个重要的含义:
对于许多数据库,提交事务可能是一项代价高昂的活动。通过将多个步骤放入单个事务中,您可以显着加快代码速度。
如果在对runSqlite的单个调用中的任何地方抛出异常,则将回滚所有操作(假设您的后端具有回滚支持)。
这实际上比最初看起来有更深远的影响。 Yesod中的许多短路功能(例如重定向)是使用异常实现的。如果您在Persistent块内使用此类调用,它将回滚整个事务。
##迁移
很抱歉告诉你,刚才我撒了一个谎:上一节中的示例实际上不起作用。如果您尝试运行它,您将收到有关丢失表的错误消息。
对于SQL数据库,主要的难点之一是管理模式更改。Persistent不是将其留给用户,而是提供帮助,但您必须要求它提供帮助。让我们看看这是什么样的:
{-# LANGUAGE EmptyDataDecls #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} import Control.Monad.IO.Class (liftIO) import Database.Persist import Database.Persist.Sqlite import Database.Persist.TH import Control.Monad.IO.Unlift import Data.Text import Control.Monad.Reader import Control.Monad.Logger import Conduit share [mkPersist sqlSettings, mkSave "entityDefs"] [persistLowerCase| Person name String age Int Maybe deriving Show |] main :: IO () main = runSqlite ":memory:" $ do runMigration $ migrate entityDefs $ entityDef (Nothing :: Maybe Person) michaelId <- insert $ Person "Michael" $ Just 26 michael <- get michaelId liftIO $ print michael
通过这一小段代码更改,Persistent将自动为您创建Person表。
runMigration和
migrate之间的这种拆分允许您同时迁移多个表。
仅在开发环境中建议使用自动数据库迁移。不鼓励您允许应用程序在生产环境中修改数据库模式。自动迁移可用于帮助加快开发速度,但不能替代在生产部署之前进行的人工审查和测试。
这只适用于处理几个实体,但一旦我们处理了十几个实体,就会很快变得烦人。Persistent提供了一个辅助函数,
mkMigrate:
{-# LANGUAGE EmptyDataDecls #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} import Database.Persist import Database.Persist.Sqlite import Database.Persist.TH share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase| Person name String age Int deriving Show Car color String make String model String deriving Show |] main :: IO () main = runSqlite ":memory:" $ do runMigration migrateAll
mkMigrate是一个Template Haskell函数,它创建一个新函数,它将自动调用在persist块中定义的所有实体上的迁移。
share函数只是一个辅助函数,它将信息从persist块传递给每个Template Haskell函数并连接结果。
Persistent对于迁移过程中会做什么有非常保守的规则。
它首先从数据库加载表信息,完成所有已定义的SQL数据类型。然后将其与代码中给出的实体定义进行比较。对于以下情况,它将自动更改架构:
字段的数据类型已更改。但是,如果无法翻译数据,则数据库可能会反对此修改。
添加了一个字段。但是,如果该字段不为null,则不提供默认值(我们稍后将讨论默认值)并且数据库中已存在数据,数据库将不允许这种情况发生。
字段从非null转换为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 UniqueUsername username
虽然每个字段名称必须以小写字母开头,但唯一性约束必须以大写字母开头,因为它将在Haskell中表示为数据构造函数。
{-# LANGUAGE EmptyDataDecls #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} import Database.Persist import Database.Persist.Sqlite import Database.Persist.TH import Data.Time import Control.Monad.IO.Class (liftIO) share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase| Person firstName String lastName String age Int PersonName firstName lastName deriving Show |] main :: IO () main = runSqlite ":memory:" $ do runMigration migrateAll insert $ Person "Michael" "Snoyman" 26 michael <- getBy $ PersonName "Michael" "Snoyman" liftIO $ print michael
要声明一个唯一的字段组合,我们在声明中添加一个额外的行。Persistent知道它正在定义一个唯一的构造函数,因为该行以大写字母开头。每个后续单词必须是此实体中的一个字段。
唯一性的主要限制是它只能应用于非空字段。这样做的原因是SQL标准在如何将唯一性应用于NULL方面不明确(例如,NULL = NULL true或false?)。除了这种歧义之外,大多数SQL引擎实际上实现了与Haskell数据类型预期相反的规则(例如,PostgreSQL说NULL = NULL是假的,而Haskell说Nothing == Nothing是True)。
除了在数据库级别提供有关数据一致性的良好保证之外,还可以使用唯一性约束在Haskell代码中执行某些特定查询,如上面演示的getBy。这通过Unique关联类型发生。在上面的例子中,我们最终得到了一个新的构造函数:
PersonName :: String -> String -> Unique Person
使用MongoDB后端,无法创建唯一性约束:您必须在该字段上放置唯一索引。
查询
根据您的目标,查询数据库有不同的方法。一些命令基于数字ID进行查询,而其他命令将进行过滤。查询返回的结果数也不同:某些查找应返回不超过一个结果(如果查找键是唯一的),而其他查找可返回许多结果。因此,Persistent提供了一些不同的查询功能。像往常一样,我们尝试在类型中编码尽可能多的安全。例如,只返回0或1结果的查询将使用Maybe包装器,而返回许多结果的查询将返回列表。
通过ID获取
在Persistent中执行的最简单的查询是基于ID获取的。由于此值可能存在,也可能不存在,因此其返回类型包含在Maybe中。
personId <- insert $ Person "Michael" "Snoyman" 26 maybePerson <- get personId case maybePerson of Nothing -> liftIO $ putStrLn "Just kidding, not really there" Just person -> liftIO $ print person
这对于
/person/ 5等网址的网站非常有用。在这种情况下,我们通常不关心Maybe包装器,只想要值,如果找不到则返回404消息。幸运的是,get404(由yesod-persistent包提供)功能可以帮助我们解决这个问题。当我们看到与Yesod的集成时,我们会详细介绍。
通过唯一约束获取
getBy几乎与get相同,除了:它需要一个唯一性约束;也就是说,它通过唯一值替换ID值。
它返回一个
Entity而不是一个值。
Entity是数据库ID和值的组合。
personId <- insert $ Person "Michael" "Snoyman" 26 maybePerson <- getBy $ PersonName "Michael" "Snoyman" case maybePerson of Nothing -> liftIO $ putStrLn "Just kidding, not really there" Just (Entity personId person) -> liftIO $ print person
与get404一样,还有一个getBy404函数。
Select 函数
有些时候是,我们需要更强大的查询。例如查找一些超过某个年龄的人,查找所有蓝色的汽车。所有没有填写邮件地址的用户。为此你需要一些select 函数。所有select函数都使用类似的接口,只是输出略有不同:
Function | Returns |
---|---|
selectSource | 包含数据库中所有ID和值的Source。这允许您编写流代码。 注意:Source是数据流,是 conduit包的一部分。我建议您阅读以开始使用。School of Haskell conduit tutorial |
selectList | 包含数据库中所有ID和值的列表。所有记录都将加载到内存中。 |
selectFirst | 仅获取数据库中的第一个ID和值(如果可用) |
selectKeys | 仅返回键值,作为Source |
selectList是最用的,我们将专门介绍它,之后理解其他的就更简单了。
selectList有两个参数:一个
Filters列表,和一个
SelectOpts列表。前者是通过一些条件过滤你的结果,它支持 等于,大于小于等条件。
SelectOpts提供三种不同的功能:排序,分割,偏移。
limits和
offsets的组合非常重要;它允许在您的webapps中进行有效的分页。
让我们直接跳到一个过滤的例子,然后分析它。
people <- selectList [PersonAge >. 25, PersonAge <=. 30] [] liftIO $ print people
PersonAgePersonAge是相关幻像类型的构造函数。这可能听起来很可怕,但重要的是它唯一地标识“person”表的“age”列,并且它知道年龄字段是Int。
2.我们有一堆Persistent过滤运算符。
过滤器列表是AND,因此我们的约束意味着“年龄大于25且年龄小于或等于30”。我们稍后会描述ORing。
我们使用
!=.代表不等于,
/=.用于更新(用于“分割和设置”,稍后描述),别担心:如果使用错误,编译器会告诉你。另外我们使用
<-.和
/<-.是XX的元素,和不是XX的元素。
关于OR,我们使用
||.运算符。例如:
people <- selectList ( [PersonAge >. 25, PersonAge <=. 30] ||. [PersonFirstName /<-. ["Adam", "Bonny"]] ||. ([PersonAge ==. 50] ||. [PersonAge ==. 60]) ) [] liftIO $ print people
这个例子的意思是找到大于25岁小于30岁的人,或者找到名称是
Adam和
Bonny的人,或者找到50和60的人。
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 ]
操作
查询只是成功的一半。我们还需要能够在数据库中添加数据并修改现有数据。Insert
数据库里的数据是如何插入的呢,答案是insert函数,他需要一个值之后会返回给你一个ID。
在这一点上,解释一下Persistent背后的哲学是有道理的。在许多其他ORM解决方案中,用于保存数据的数据类型是不透明的:你需要通过他们定义的接口来获取和修改数据。Persistent的情况并非如此:我们使用普通的代数数据类型来处理整个问题。这意味着你仍然可以使用模式匹配,currying以及你习惯的其他一切的功能。
但是,也有一些我们做不到的事情。例如,每次在Haskell中更新记录时,都无法自动更新数据库中的值。当然,凭借Haskell的纯正和不变性的正常立场,无论如何这都没有多大意义,所以我不会为此伤心。
然而,有一个问题是新手经常被困扰:为什么ID和值完全分开?似乎将ID嵌入到值中是非常合乎逻辑的。换句话说,而不是:
data Person = Person { name :: String }
而是这样
data Person = Person { personId :: PersonId, name :: String }
嗯,这有一个问题就是:我们如何进行插入?如果一个Person需要一个ID,但是我们是通过插入获取的ID,但是插入又需要一个Person,那么我们就碰到了死循环。我们可以用undefined来解决这个问题,但这只是在自找麻烦。
好吧,你说,让我们尝试一些更安全的东西:
data Person = Person { personId :: Maybe PersonId, name :: String }
我绝对更喜欢
insert $ Person Nothing "Michael"而不是
nsert $ Person undefined "Michael"。现在我们的类型会简单得多,对吧?例如,selectList可以返回一个简单的
[Person]而不是那个丑陋的
[Entity SqlPersist Person]。
问题是“丑陋”是有用的。
Entity Person在类型上明确表示我们正在处理数据库中存在的值。假设我们想要创建一个指向需要PersonId的另一个页面的链接(这不是一个不常见的事件,我们稍后会讨论)。
Entity Person为我们提供了对该信息的明确访问;使用Maybe包装器在Person中嵌入PersonId意味着Just的额外运行时检查,而不是更加防错的编译时间检查。
最后,将ID嵌入值中会出现语义不匹配。
Person是值。如果所有字段都相同,则两个人是相同的(在Haskell的上下文中)。通过在值中嵌入ID,我们不再谈论一个人,而是谈论数据库中的一行。相等不再是真正的相等,而是:这是同一个人,而不是相等的人。
换句话说,将ID分离出来会有一些烦恼,但总的来说,这是正确的方法,在宏观方案中会导致更好,更少错误的代码。
Update
现在,让我们讨论一下更新。最简单的更新方法是:let michael = Person "Michael" 26 michaelAfterBirthday = michael { personAge = 27 }
但实际上并没有更新任何东西,它只是根据旧的值创建一个新的Person值。当我们说更新时,我们不是在谈论对Haskell中的值的修改。 (我们最好不要这样,因为Haskell中的数据是不可变的。)
相反,我们正在研究的是修改表中行的方法。最简单的方法是使用更新功能。
personId <- insert $ Person "Michael" "Snoyman" 26 update personId [PersonAge =. 27]
update有两个参数:一个ID和一个
Updates列表。最简单的更新是赋值,但它并不总是最好的。如果您想将某人的年龄提高1,但您没有现在的年龄,该怎么办?
Persistent可以这样:
haveBirthday personId = update personId [PersonAge +=. 1]
正如您所料,我们拥有所有基本的数学运算符:
+=.``-=.``/=.。这些可以方便地更新单个记录,但它们对于正确的ACID保证也很重要。想象一下另一种选择:获取出一个Person,增加年龄,并更新新值。如果你有两个线程/进程同时在这个数据库上工作,你就会陷入一个受伤的世界(提示:竞争条件)。
有时您会想要一次更新多行(例如,让所有员工加薪5%)。
updateWhere有两个参数:过滤器列表和要应用的更新列表。
updateWhere [PersonFirstName ==. "Michael"] [PersonAge *=. 2] -- it's been a long day
有时,您只想用不同的值完全替换数据库中的值。为此,您使用替换功能。
personId <- insert $ Person "Michael" "Snoyman" 26 replace personId $ Person "John" "Doe" 20
Delete
但有时我们想删除我们。为此,我们有三个功能:delete
根据ID删除deleteBy
基于唯一约束删除deleteWhere
基于一组过滤器删除personId <- insert $ Person "Michael" "Snoyman" 26 delete personId deleteBy $ PersonName "Michael" "Snoyman" deleteWhere [PersonFirstName ==. "Michael"]
我们甚至可以使用deleteWhere来消除表中的所有记录,我们只需要向GHC提供一些关于我们感兴趣的表的类型签名:
deleteWhere ([] :: [Filter Person])
Attributes
到目前为止,我们已经看到了persistLowerCase块的基本语法:一个代表实体名称的行。之后是一个缩进的行,这行有个单词,一个是字段的名称,一个是数据的类型。
Persistent为此提供了更多的功能:您可以在两个单词之后分配任意的属性列表。
假设我们想要一个具有可选年龄的Person实体,以及记录添加到系统时的时间戳。对于已存在于数据库中的实体,我们希望使用当前时间。
-# LANGUAGE EmptyDataDecls #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} import Database.Persist import Database.Persist.Sqlite import Database.Persist.TH import Data.Time import Control.Monad.IO.Class share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase| Person name String age Int Maybe created UTCTime default=CURRENT_TIME deriving Show |] main :: IO () main = runSqlite ":memory:" $ do time <- liftIO getCurrentTime runMigration migrateAll insert $ Person "Michael" (Just 26) time insert $ Person "Greg" Nothing time return ()
Maybe是内置的。它使该字段可选。在Haskell中,这意味着它包含在一个Maybe中。在SQL中,它使列可以为空。
default属性是特定于后端的,可以使用数据库理解的任何语法。在这里,它使用数据库的内置
CURRENT_TIME函数。假设我们现在想为一个人最喜欢的编程语言添加一个字段:
{-# LANGUAGE EmptyDataDecls #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} import Database.Persist import Database.Persist.Sqlite import Database.Persist.TH import Data.Time share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase| Person name String age Int Maybe created UTCTime default=CURRENT_TIME language String default='Haskell' deriving Show |] main :: IO () main = runSqlite ":memory:" $ do runMigration migrateAll
默认属性对Haskell代码本身完全没有影响;你仍然需要填写所有值。这只会影响数据库架构和自动迁移。
我们需要用单引号括起字符串,以便数据库可以正确解释它。最后,Persistent可以使用双引号来包含空格,因此如果我们想将某人的默认本国设置为萨尔瓦多:
{-# LANGUAGE EmptyDataDecls #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} import Database.Persist import Database.Persist.Sqlite import Database.Persist.TH import Data.Time share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase| Person name String age Int Maybe created UTCTime default=CURRENT_TIME language String default='Haskell' country String "default='El Salvador'" deriving Show |] main :: IO () main = runSqlite ":memory:" $ do runMigration migrateAll
可以对属性执行的最后一个技巧是指定要用于SQL表和列的名称。在与现有数据库交互时,这很方便。
share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase| Person sql=the-person-table id=numeric_id firstName String sql=first_name lastName String sql=fldLastName age Int "sql=The Age of the Person" PersonName firstName lastName deriving Show |]
实体定义语法还有许多其他功能。Persistent文档 中维护了最新列表。
关系
Persistent允许以与支持非SQL数据库一致的方式引用数据类型。我们通过在相关实体中嵌入ID来实现此目的。所以,如果一个人有很多车:{-# LANGUAGE EmptyDataDecls #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} 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"] [persistLowerCase| Person name String deriving Show Car ownerId PersonId name String deriving Show |] main :: IO () main = runSqlite ":memory:" $ do runMigration migrateAll bruce <- insert $ Person "Bruce Wayne" insert $ Car bruce "Bat Mobile" insert $ Car bruce "Porsche" -- this could go on a while cars <- selectList [CarOwnerId ==. bruce] [] liftIO $ print cars
使用此技术,您可以定义一对多关系。要定义多对多关系,我们需要一个连接实体,它与每个原始表具有一对多的关系。在这些上使用唯一性约束也是一个好主意。例如,要模拟我们想要跟踪哪些人在哪些商店购物的情况:
{-# LANGUAGE EmptyDataDecls #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} import Database.Persist import Database.Persist.Sqlite import Database.Persist.TH import Data.Time share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase| Person name String Store name String PersonStore personId PersonId storeId StoreId UniquePersonStore personId storeId |] main :: IO () main = runSqlite ":memory:" $ 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 return ()
仔细观察类型
到目前为止,我们已经谈到过Person和PersonId而没有真正解释它们是什么。在最简单的意义上,对于仅限SQL的系统,PersonId可以只是类型PersonId = Int64。但是,这意味着类型级别的PersonId与Person实体之间没有任何约束。因此,您可能会意外地使用PersonId并获得一辆汽车。为了模拟这种关系,我们可以使用幻像类型。那么,我们下一个步骤将是:newtype Key entity = Key Int64 type PersonId = Key Person
这很好,直到你得到一个不使用Int64作为其ID的后端。这不仅仅是一个理论问题;MongoDB使用ByteStrings代替。所以我们需要的是一个可以包含Int和ByteString的键值。对于和类型来说似乎是一个美好的时光:
data Key entity = KeyInt Int64 | KeyByteString ByteString
但这只是在寻找麻烦。接下来我们将有一个使用时间戳的后端,因此我们需要向Key添加另一个构造函数。这可能会持续一段时间。幸运的是,我们已经有一个用于表示任意数据的和类型:PersistValue:
newtype Key entity = Key PersistValue
这是(或多或少)Persistent在2.0版之前所做的事情。但是,这有一个不同的问题:它会抛弃数据。例如,在处理SQL数据库时,我们知道密钥类型将是Int64(假设正在使用默认值)。但是,您无法在具有此构造的类型级别断言。因此,从Persistent 2.0开始,我们现在在PersistEntity类中使用关联的数据类型:
class PersistEntity record where data Key record ...
当您使用SQL后端并且未使用自定义键类型时,这将成为Int64的新类型包装器,并且toSqlKey / fromSqlKey函数可以为您执行类型安全的转换。另一方面,使用MongoDB,它是ByteString的包装器。
更复杂,更通用
默认情况下,Persistent将对您的数据类型进行硬编码以使用特定的数据库后端。使用sqlSettings时,这是SqlBackend类型。但是,如果要编写可在多个后端使用的持久代码,可以通过将sqlSettings替换为sqlSettings {mpsGeneric = True}来启用更多泛型类型。要了解为什么这是必要的,请考虑关系。假设我们想要代表博客和博客文章。我们将使用实体定义:
Blog title Text Post title Text blogId BlogId
我们知道BlogId只是Key Blog的一个类型同义词,但是如何定义Key Blog?我们不能使用Int64,因为它不适用于MongoDB。我们不能使用ByteString,因为这对SQL数据库不起作用。
为了实现这一点,一旦将mpsGeneric设置为True,输出的数据类型就会有一个类型参数来指示它们使用的数据库后端,以便可以正确编码密钥。这看起来像:
data BlogGeneric backend = Blog { blogTitle :: Text } data PostGeneric backend = Post { postTitle :: Text , postBlogId :: Key (BlogGeneric backend) }
请注意,我们仍然保留构造函数和记录的短名称。最后,为了给普通代码提供一个简单的接口,我们定义了一些类型的同义词:
type Blog = BlogGeneric SqlBackend type BlogId = Key Blog type Post = PostGeneric SqlBackend type PostId = Key Post
不,SqlBackend在任何地方都没有硬编码到Persistent中。您传递给mkPersist的sqlSettings参数告诉我们使用SqlBackend。 Mongo代码将使用mongoSettings。
这可能在表面上非常复杂,但用户代码几乎没有涉及到这一点。回顾整个章节:我们不是一次需要直接处理Key或Generic的东西。它弹出的最常见位置是编译器错误消息。所以重要的是要意识到这存在,但它不应该影响你的日常生活。
自定义字段
有时,您需要定义要在数据存储中使用的自定义字段。最常见的情况是枚举,例如就业状况。为此,Persistent提供了一个帮助模板Haskell函数:-- @Employment.hs {-# LANGUAGE TemplateHaskell #-} module Employment where import Database.Persist.TH data Employment = Employed | Unemployed | Retired deriving (Show, Read, Eq) derivePersistField "Employment"
{-# LANGUAGE EmptyDataDecls #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} import Database.Persist.Sqlite import Database.Persist.TH import Employment share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase| Person name String employment Employment |] main :: IO () main = runSqlite ":memory:" $ do runMigration migrateAll insert $ Person "Bruce Wayne" Retired insert $ Person "Peter Parker" Unemployed insert $ Person "Michael" Employed return ()
derivePersistField使用字符串字段将数据存储在数据库中,并使用数据类型的Show和Read实例执行封送处理。这可能不如通过整数存储那么有效,但它更具有未来性:即使您将来添加额外的构造函数,您的数据仍然有效。
在这种情况下,我们将定义分为两个单独的模块。由于GHC阶段限制,这是必要的,这实际上意味着,在许多情况下,模板Haskell生成的代码不能在其创建的同一模块中使用。
Persistent: 原始 SQL
Persistent包为数据存储提供了类型安全的接口。它试图与后端无关,例如不依赖于SQL的关系特性。我的经验是,您可以轻松执行高级接口所需的95%。(事实上,我的大多数网络应用都只使用高级接口。)但偶尔你会想要使用特定于后端的功能。我过去使用的一个功能是全文搜索。在这种情况下,我们将使用SQL“LIKE”运算符,该运算符未在Persistent中建模。我们将为所有人命名为“Snoyman”并打印出来的记录。
实际上,由于Persistent 0.6中添加了一个允许特定于后端的运算符的功能,因此可以直接以正常语法表示LIKE运算符。但这仍然是一个很好的例子,所以让我们继续吧。
{-# LANGUAGE EmptyDataDecls #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} import Database.Persist.TH import Data.Text (Text) import Database.Persist.Sqlite import Control.Monad.IO.Class (liftIO) import Data.Conduit import qualified Data.Conduit.List as CL share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase| Person name Text |] main :: IO () main = runSqlite ":memory:" $ 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 does not provide the LIKE keyword, but we'd like to get the -- whole Snoyman family... let sql = "SELECT name FROM Person WHERE name LIKE '%Snoyman'" rawQuery sql [] $$ CL.mapM_ (liftIO . print)
还有更高级别的支持,允许自动数据封送。有关更多详细信息,请参阅Haddock API文档。
与Yesod集成
所以你认为Persistent很好用。但是如何将它与Yesod应用程序集成?如果您使用脚手架,大部分工作已经为您完成。但正如我们通常所做的那样,我们将手动构建所有内容,以指出它在表面下的工作原理。yesod-persistent包提供
Persistent和
Yesod之间的会合点。它提供了YesodPersist类型类,它通过runDB方法标准化对数据库的访问。让我们看看这个例子。
{-# LANGUAGE EmptyDataDecls #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE ViewPatterns #-} import Yesod import Database.Persist.Sqlite import Control.Monad.Trans.Resource (runResourceT) import Control.Monad.Logger (runStderrLoggingT) -- Define our entities as usual share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase| Person firstName String lastName String age Int deriving Show |] -- We keep our connection pool in the foundation. At program initialization, we -- create our initial pool, and each time we need to perform an action we check -- out a single connection from the pool. data PersistTest = PersistTest ConnectionPool -- We'll create a single route, to access a person. It's a very common -- occurrence to use an Id type in routes. mkYesod "PersistTest" [parseRoutes| / HomeR GET /person/#PersonId PersonR GET |] -- Nothing special here instance Yesod PersistTest -- Now we need to define a YesodPersist instance, which will keep track of -- which backend we're using and how to run an action. instance YesodPersist PersistTest where type YesodPersistBackend PersistTest = SqlBackend runDB action = do PersistTest pool <- getYesod runSqlPool action pool -- List all people in the database getHomeR :: Handler Html getHomeR = do people <- runDB $ selectList [] [Asc PersonAge] defaultLayout [whamlet| <ul> $forall Entity personid person <- people <li> <a href=@{PersonR personid}>#{personFirstName person} |] -- We'll just return the show value of a person, or a 404 if the Person doesn't -- exist. getPersonR :: PersonId -> Handler String getPersonR personId = do person <- runDB $ get404 personId return $ show person openConnectionCount :: Int openConnectionCount = 10 main :: IO () main = runStderrLoggingT $ withSqlitePool "test.db3" openConnectionCount $ \pool -> liftIO $ do runResourceT $ flip runSqlPool pool $ do runMigration migrateAll insert $ Person "Michael" "Snoyman" 26 warp 3000 $ PersistTest pool
这里有两个重要的部分供一般使用。 runDB用于从Handler中运行DB操作。在runDB中,您可以使用我们目前所说的任何函数,例如insert和selectList。
runDB的类型是
YesodDB site a→HandlerT site IO a。 YesodDB定义为:
type YesodDB site = ReaderT (YesodPersistBackend site) (HandlerT site IO)
由于它构建在YesodPersistBackend关联类型之上,因此它使用基于当前站点的相应数据库后端。
另一个新功能是get404。它就像get一样工作,但是当找不到结果时,它不会返回Nothing,而是返回404消息页面。 getPersonR函数是在现实世界的Yesod应用程序中使用的一种非常常见的方法:get404一个值,然后根据它返回响应。
更复杂的SQL
坚持不懈地追求与后端无关。这种方法的优点是可以从不同的后端类型轻松移动的代码。缺点是你会失去一些特定于后端的功能。可能最大的牺牲品是SQL join支持。幸运的是,感谢Felipe Lessa。Esqueleto库使用现有的Persistent基础结构为编写类型安全的SQL查询提供支持。该软件包的Haddocks为其使用提供了很好的介绍。由于它使用了许多持久性概念,因此大多数现有的持久性知识都应该轻松转移。
有关使用Esqueleto的简单示例,请参阅SQL联接章节。
除了SQLite之外的东西
为了使本章中的示例简单,我们使用了SQLite后端。只是为了解决问题,这里是我们用PostgreSQL重写的原始概要:{-# LANGUAGE EmptyDataDecls #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} import Control.Monad.IO.Class (liftIO) import Control.Monad.Logger (runStderrLoggingT) import Database.Persist import Database.Persist.Postgresql import Database.Persist.TH share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase| Person name String age Int Maybe deriving Show BlogPost title String authorId PersonId deriving Show |] connStr = "host=localhost dbname=test user=test password=test port=5432" main :: IO () main = runStderrLoggingT $ withPostgresqlPool connStr 10 $ \pool -> liftIO $ do flip runSqlPersistMPool pool $ do runMigration migrateAll johnId <- insert $ Person "John Doe" $ Just 35 janeId <- insert $ Person "Jane Doe" Nothing insert $ BlogPost "My fr1st p0st" johnId insert $ BlogPost "One more for good measure" johnId oneJohnPost <- selectList [BlogPostAuthorId ==. johnId] [LimitTo 1] liftIO $ print (oneJohnPost :: [Entity BlogPost]) john <- get johnId liftIO $ print (john :: Maybe Person) delete janeId deleteWhere [BlogPostAuthorId ==. johnId]
结语
Persistent将Haskell的类型安全性带到您的数据访问层。您可以依靠Persistent为您自动执行该过程,而不是编写容易出错,无类型的数据访问或手动编写样板编组代码。目标是在大多数情况下提供您需要的一切。对于需要更强大功能的时候,Persistent允许您直接访问底层数据存储,因此您可以编写所需的任何5向连接。
Persistent直接集成到Yesod工作流程中。像yesod-persistent这样的帮助程序包不仅提供了一个很好的层,而像yesod-form和yesod-auth这样的包也可以利用Persistent的功能。
有关实体声明,数据库连接等语法的更多信息,请访问https://github.com/yesodweb
相关文章推荐
- Haskell的yesod框架-1
- Oracle 免费的数据库--Database 快捷版 11g 安装使用与"SOD框架"对Oracle的CodeFirst支持
- Haskell的Yesod技术大全
- Yesod - 简介 (0)
- Oracle 免费的数据库--Database 快捷版 11g 安装使用与"SOD框架"对Oracle的CodeFirst支持
- Yesod - Haskell (1)
- Yesod - 类型类 (5)
- Yesod - 基础结构 (2)
- Yesod - 组件 (4)
- Yesod - 路由和响应 (6)
- yesod / yesod-auth-oauth
- yesod / yesod-auth / openid
- yesod / yesod-auth / browserid
- SODBASE CEP学习(十六):CEP与数据库交互
- yesod / yesod-auth / auth2
- SOD框架的Model、连接数据库及增删改查
- Yesod is a Haskell web framework
- yesod / yesod-auth / Yesod / Auth
- yesod / yesod-core / widget-benchmark
- node.js-ORM数据库框架sequelize使用总结 超时查询、缓存查询、多表关联查询