Beam:没有模板Haskell的数据库功能!

作为Haskell Web系列的一部分,我们检查了PersistentEsqueleto库。 这些中的第一个允许您使用特殊语法创建数据库模式。 然后,您可以使用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 :: …
}

因此,当我们指定每个字段的实际类型时,我们只需放入相关的数据类型,例如IntText或其他,对吗? 好吧,不完全是。 为了完成我们的类型,我们将用所需的类型填充每个字段,除非通过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构造函数!

我们的类型的实例

既然我们已经指定了类型,我们就可以使用BeamableTable类型类来告诉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生成实例。 我们还将导出ShowEq实例:

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类创建一个实例。 这将涉及一些类型族语法。 我们将指定UserIdArticleId作为我们的主键数据类型。 然后,我们可以填写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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值