haskell
作为Haskell Web系列的一部分,我们检查了Persistent和Esqueleto库。 其中的第一个允许您使用特殊语法创建数据库模式。 然后,您可以使用Template Haskell生成所有必需的Haskell数据类型和类型的实例。 更好的是,您可以编写Haskell代码以查询类似于SQL的代码。 这些查询是类型安全的,这非常好。 但是,需要使用模板Haskell指定我们的架构存在一些缺点。 例如,代码需要花费更长的时间来编译,而对于初学者来说则较难获得。
本周在博客上,我们将探索另一个名为Beam的数据库库。 该库使我们无需使用Template Haskell即可指定数据库架构。 涉及一些样板,但这一点都还不错! 与Persistent一样,Beam也支持许多后端,例如SQLite和PostgresQL。 与Persistent不同,Beam还支持将联接查询作为其系统的内置部分。
有关高级库的更多想法,请务必查看我们的生产清单 ! 它包含几个其他数据库选项供您查看。
指定我们的类型
首先,尽管Beam不需要Template 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
语句增强为monadic表达式。 我们将从所有用户中选择一个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 #-}
翻译自: https://hackernoon.com/beam-database-power-without-template-haskell-77a2df12fa24
haskell