作为Haskell Web系列的 一部分,我们检查了Persistent 和Esqueleto 库。 这些中的第一个允许您使用特殊语法创建数据库模式。 然后,您可以使用Template Haskell生成所有必要的Haskell数据类型和类型的实例。 更好的是,您可以编写Haskell代码以查询类似于SQL的代码。 这些查询是类型安全的,这非常好。 但是,需要使用模板Haskell指定我们的架构存在一些缺点。 例如,代码花费更长的时间进行编译,而对于初学者而言则较难获得。
本周在博客上,我们将探索另一个名为Beam的 数据库库。 该库使我们无需 使用Template Haskell即可指定数据库架构。 涉及到一些样板,但这还不错! 与Persistent一样,Beam也支持许多后端,例如SQLite和PostgresQL。 与Persistent不同,Beam还支持将联接查询作为其系统的内置部分。
有关高级库的更多想法,请务必查看我们的生产清单 ! 它包括几个其他数据库选项供您查看。
指定我们的类型
首先,虽然Beam不需要模板Haskell,但确实需要很多其他编译器扩展。 您可以查看下面附录中的内容,也可以查看Github上的示例代码 。 现在让我们回想一下在使用Persistent时如何指定架构:
import qualified Database.Persist.TH as PTH
PTH.share [PTH.mkPersist PTH.sqlSettings, PTH.mkMigrate "migrateAll"] [PTH.persistLowerCase| User sql=users name Text email Text age Int occupation Text UniqueEmail email deriving Show Read Eq
Article sql=articles title Text body Text publishedTime UTCTime authorId UserId UniqueTitle title deriving Show Read Eq
使用Beam,我们不会使用Template Haskell,因此实际上将创建普通的Haskell数据类型。 但是仍然会有一些奇怪之处。 首先,按照惯例,我们将在类型的最后加上额外的字符T
这是不必要的,但是约定可以帮助我们记住与表相关的类型。 我们还必须提供一个额外的类型参数f
,稍后我们将进一步介绍它:
data UserT f = …
data ArticleT f = ...
我们的下一个约定是在字段名称前使用下划线。 与持久性不同,我们还将在字段名称中指定类型名称。 遵循这些约定,我遵循图书馆创建者Travis的建议。
data UserT f = { _userId :: ... , _userName :: … , _userEmail :: … , _userAge :: … , _userOccupation :: … }
data ArticleT f = { _articleId :: … , _articleTitle :: … , _articleBody :: … , _articlePublishedTime :: … }
因此,当我们指定每个字段的实际类型时,我们只需放入相关的数据类型,例如Int
, Text
或其他,对吗? 好吧,不完全是。 为了完成我们的类型,我们将用所需的类型填充每个字段,除非通过Columnar f
指定。 同样,我们将在这两种类型上派生Generic
,这将使Beam发挥其魔力:
data UserT f = { _userId :: Columnar f Int64 , _userName :: Columnar f Text , _userEmail :: Columnar f Text , _userAge :: Columnar f Int , _userOccupation :: Columnar f Text } deriving (Generic)
data ArticleT f = { _articleId :: Columnar f Int64 , _articleTitle :: Columnar f Text , _articleBody :: Columnar f Text , _articlePublishedTime :: Columnar f Int64 -- Unix Epoch } deriving (Generic)
现在,此模式与我们之前的模式之间存在一些小差异。 首先,我们将主键作为类型的显式字段。 对于持久性,我们使用Entity
抽象将其分离。 我们将在下面看到如何处理未知密钥的情况。 第二个区别是(目前),我们在文章上省略了userId
字段。 我们在处理主键时会添加它。
柱状
那么,这种Columnar
业务到底是什么呢? 在大多数情况下,我们希望使用原始字段类型指定一个User
。 但是在某些情况下,我们必须为SQL表达式使用更复杂的类型。 让我们先从简单的案例开始。
幸运的是, Columnar
工作方式是,如果我们将Identity
用作f
,则可以使用原始类型来填充字段值。 我们将专门为此身份案例创建类型同义词。 然后我们可以举一些例子:
type User = UserT Identity type Article = ArticleT Identity
user1 :: User user1 = User 1 "James" "james@example.com" 25 "programmer"
user2 :: User user2 = User 2 "Katie" "katie@example.com " 25 "engineer"
users :: [User] users = [ user1, user2 ]
请注意,如果您发现重复Columnar
关键字比较麻烦,可以将其缩短为C
:
data UserT f = { _userId :: C f Int64 , _userName :: C f Text , _userEmail :: C f Text , _userAge :: C f Int , _userOccupation :: C f Text } deriving (Generic)
现在,我们的初始示例将为我们的所有字段分配原始值。 因此,除了Identity
之外,我们最初不需要为f
参数使用任何东西。 不过,接下来,我们将讨论自动递增主键的情况。 在这种情况下,我们将使用default_
函数,该函数的类型实际上是SQL表达式的Beam形式。 在这种情况下,我们将为f
使用其他类型,但是灵活性将使我们能够继续使用User
构造函数!
我们的类型的实例
既然我们已经指定了类型,我们就可以使用Beamable
和Table
类型类来告诉Beam有关我们类型的更多信息。 在将任何这些类型设置为Table
,我们需要分配其主键类型。 因此,让我们再加上几个类型同义词来表示这些:
type UserId = PrimaryKey UserT Identity type ArticleId = PrimaryKey ArticleT Identity
在此过程中,让我们将外键添加到Article
类型中:
data ArticleT f = { _articleId :: Columnar f Int64 , _articleTitle :: Columnar f Text , _articleBody :: Columnar f Text , _articlePublishedTime :: Columnar f Int64 , _articleUserId :: PrimaryKey UserT f } deriving (Generic)
现在,我们可以在主要类型和主键类型上为Beamable
生成实例。 我们还将导出Show
和Eq
实例:
data UserT f = …
deriving instance Show User deriving instance Eq User
instance Beamable UserT instance Beamable (PrimaryKey UserT)
data ArticleT f = …
deriving instance Show Article deriving instance Eq Article
instance Beamable ArticleT instance Beamable (PrimaryKey ArticleT)
现在,我们将为Table
类创建一个实例。 这将涉及一些类型族语法。 我们将指定UserId
和ArticleId
作为我们的主键数据类型。 然后,我们可以填写primaryKey
函数以匹配右边的字段。
instance Table UserT where data PrimaryKey UserT f = UserId (Columnar f Int64) deriving Generic primaryKey = UserId . _userId
instance Table ArticleT where data PrimaryKey ArticleT f = ArticleId (Columnar f Int64) deriving Generic primaryKey = ArticleId . _articleId
隐形眼镜
我们将做另一件事来模仿持久性。 模板Haskell为我们自动生成了镜头。 我们可以在进行数据库查询时使用它们。 下面,我们将使用类似的方法。 但是,我们将使用特殊功能tableLenses
而不是Template Haskell来制作这些。 如果您还记得我们如何使用Servant Client 库,我们可以通过使用client
并将其与模式匹配来创建client函数。 我们将使用tableLenses
做类似的tableLenses
。 我们将在表的每个字段上使用LensFor
,并创建构造项目的模式。
User (LensFor userId) (LensFor userName) (LensFor userEmail) (LensFor userAge) (LensFor userOccupation) = tableLenses
Article (LensFor articleId) (LensFor articleTitle) (LensFor articleBody) (LensFor articlePublishedTime) (UserId (LensFor articuleUserId)) = tableLenses
注意,我们必须将外键镜头包装在UserId
。
创建我们的数据库
现在,与持久性不同,我们将创建一个额外的类型来表示数据库。 我们的两个表中的每个表在此数据库中都有一个字段:
data BlogDB f = BlogDB { _blogUsers :: f (TableEntity UserT) , _blogArticles :: f (TableEntity ArticleT) } deriving (Generic)
我们需要使数据库类型成为Database
类的实例。 我们还将指定一组可在数据库上使用的默认设置。 这两项都将包含参数be
,代表后端(例如,SQLite,Postgres)。 我们暂时保留此参数的通用性。
instance Database be BlogDB
blogDb :: DatabaseSettings be BlogDB blogDb = defaultDbSettings
插入我们的数据库
现在,使用Beam迁移我们的数据库要比使用Persistent迁移更为复杂。 我们可能会在以后的文章中介绍。 现在,我们将使事情变得简单,并使用SQLite数据库并自己进行迁移。 因此,让我们首先创建表。 我们必须在这里遵循Beam的约定,特别是在外键的user_id__id
字段上:
CREATE TABLE users \ ( id INTEGER PRIMARY KEY AUTOINCREMENT\ , name VARCHAR NOT NULL \ , email VARCHAR NOT NULL \ , age INTEGER NOT NULL \ , occupation VARCHAR NOT NULL \ ); CREATE TABLE articles \ ( id INTEGER PRIMARY KEY AUTOINCREMENT \ , title VARCHAR NOT NULL \ , body VARCHAR NOT NULL \ , published_time INTEGER NOT NULL \ , user_id__id INTEGER NOT NULL \ );
现在,我们要编写一些可以与数据库交互的查询。 让我们从插入原始用户开始。 我们首先打开一个SQLite连接,然后编写一个使用该连接的函数:
import Database.SQLite.Simple (open, Connection)
main :: IO () main = do conn <- open "blogdb1.db" insertUsers conn
insertUsers :: Connection -> IO () insertUsers = ...
我们通过使用runBeamSqlite
并传递连接来开始表达式。 然后,我们使用runInsert
将希望创建插入语句的内容指定给Beam。
import Database.Beam import Database.Beam.SQLite
insertUsers :: Connection -> IO () insertUsers conn = runBeamSqlite conn $ runInsert $ ...
现在,我们将使用insert
函数,并从数据库中发出想要从哪个表中发出信号的信号:
insertUsers :: Connection -> IO () insertUsers conn = runBeamSqlite conn $ runInsert $ insert (_blogUsers blogDb) $ ...
最后,由于我们要插入原始值( UserT Identity
),因此我们使用insertValues
函数来完成此调用:
insertUsers :: Connection -> IO () insertUsers conn = runBeamSqlite conn $ runInsert $ insert (_blogUsers blogDb) $ insertValues users
现在,我们可以检查并验证我们的用户是否存在!
SELECT * FROM users; 1|James|james@example.com|25|programmer 2|Katie|katie@example.com|25|engineer
让我们对文章做同样的事情。 我们将使用pk
函数来访问特定User
的主键:
article1 :: Article article1 = Article 1 "First article" "A great article" 1531193221 (pk user1)
article2 :: Article article2 = Article 2 "Second article" "A better article" 1531199221 (pk user2)
article3 :: Article article3 = Article 3 "Third article" "The best article" 1531200221 (pk user1)
articles :: [Article] articles = [ article1, article2, article3]
insertArticles :: Connection -> IO () insertArticles conn = runBeamSqlite conn $ runInsert $ insert (_blogArticles blogDb) $ insertValues articles
选择查询
现在我们已经插入了几个元素,让我们运行一些基本的select语句。 通常,对于select,我们需要runSelectReturningList
函数。 如果需要,我们还可以查询具有不同功能的单个元素:
findUsers :: Connection -> IO () findUsers conn = runBeamSqlite conn $ do users <- runSelectReturningList $ ...
现在,我们将使用select
而不是从上一个查询insert
。 我们还将在数据库的用户字段中使用all_
函数,以表示我们希望他们全部获得。 这就是我们所需要的!:
findUsers :: Connection -> IO () findUsers conn = runBeamSqlite conn $ do users <- runSelectReturningList $ select (all_ (_blogUsers blogDb)) mapM_ (liftIO . putStrLn . show) users
为了进行过滤查询,我们将从相同的框架开始。 但是现在我们需要将select
语句增强为单子表达式。 我们将从所有用户中选择一个user
开始:
findUsers :: Connection -> IO () findUsers conn = runBeamSqlite conn $ do users <- runSelectReturningList $ select $ do user <- (all_ (_blogUsers blogDb)) ... mapM_ (liftIO . putStrLn . show) users
现在,我们将使用guard_
并应用其中一个镜头来guard_
进行过滤。 我们使用==.
像“持久性”中的平等运算符。 我们还必须用val
包装我们的原始比较值:
findUsers :: Connection -> IO () findUsers conn = runBeamSqlite conn $ do users <- runSelectReturningList $ select $ do user <- (all_ (_blogUsers blogDb)) guard_ (user ^. userName ==. (val_ "James")) return user mapM_ (liftIO . putStrLn . show) users
这就是我们所需要的! Beam将为我们生成SQL! 现在让我们尝试加入。 在Beam中,这实际上比使用Persistent / Esqueleto简单得多。 我们需要的是在文章的“选择”部分添加更多声明。 我们将仅通过用户ID对其进行过滤!
findUsersAndArticles :: Connection -> IO () findUsersAndArticles conn = runBeamSqlite conn $ do users <- runSelectReturningList $ select $ do user <- (all_ (_blogUsers blogDb)) guard_ (user ^. userName ==. (val_ "James")) articles <- (all_ (_blogArticles blogDb)) guard_ (article ^. articleUserId ==. user ^. userId) return user mapM_ (liftIO . putStrLn . show) users
这里的所有都是它的!
自动递增主键
在上面的示例中,我们对所有ID进行了硬编码。 但这通常不是您想要的。 我们应该让数据库通过某些规则分配ID,在本例中为自动递增。 在这种情况下,我们将创建一个“表达式”,而不是创建一个User
“值”。 这可以通过我们类型中的多态f
参数来实现。 我们将取消类型签名,因为它有点混乱。 但是,我们将创建以下表达式:
user1' = User default_ (val_ "James") (val_ "james@example.com") (val_ 25) (val_ "programmer")
我们使用default_
表示将告诉SQL使用默认值的表达式。 然后,我们使用val_
提升所有其他值。 最后,我们将在Haskell表达式中使用insertExpressions
而不是insertValues
。
insertUsers :: Connection -> IO () insertUsers conn = runBeamSqlite conn $ runInsert $ insert (_blogUsers blogDb) $ insertExpressions [ user1' ]
然后,我们将获得我们的自动递增密钥!
结论
到此结束我们对Beam
库的介绍。 如我们所见,Beam是一个很棒的库,可让您无需使用任何模板Haskell即可指定数据库架构。 有关更多详细信息,请确保签出文档 !
要更深入地了解使用Haskell库制作Web应用程序,请务必阅读我们的Haskell Web系列 。 它介绍了一些数据库机制,以及创建API和测试。 另一个挑战是,尝试重新编写该系列中的代码以使用Beam而不是Persistent。 查看需要更改多少Servant
代码以适应这种情况。
有关酷库的更多示例,请下载我们的生产清单 ! 您还可以查看更多数据库和API库!
附录:编译器扩展
{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeApplications #-} {-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE TypeSynonymInstances #-} {-# LANGUAGE NoMonoMorphismRestriction #-}
From: https://hackernoon.com/beam-database-power-without-template-haskell-77a2df12fa24