Latest news about Bitcoin and all cryptocurrencies. Your daily crypto news habit.
As part of our Haskell Web Series, we examined the Persistent and Esqueleto libraries. The first of these allows you to create a database schema in a special syntax. You can then use Template Haskell to generate all the necessary Haskell data types and instances for your types. Even better, you can write Haskell code to query on these that resembles SQL. These queries are type-safe, which is awesome. However, the need to specify our schema with template Haskell presented some drawbacks. For instance, the code takes longer to compile and is less approachable for beginners.
This week on the blog, weâll be exploring another database library called Beam. This library allows us to specify our database schema without using Template Haskell. Thereâs some boilerplate involved, but itâs not bad at all! Like Persistent, Beam has support for many backends, such as SQLite and PostgresQL. Unlike Persistent, Beam also supports join queries as a built-in part of its system.
For some more ideas on advanced libraries, be sure to check out our Production Checklist! It includes a couple more different database options to look at.
Specifying Our Types
As a first note, while Beam doesnât require Template Haskell, it does need a lot of other compiler extensions. You can look at those in the appendix below, or else take a look at the example code on Github. Now letâs think back to how we specified our schema when using 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
With Beam, we wonât use Template Haskell, so weâll actually be creating normal Haskell data types. There will still be some oddities though. First, by convention, weâll specify our types with the extra character T at the end. This is unnecessary, but the convention helps us remember what types relate to tables. We'll also have to provide an extra type parameter f, that we'll get into a bit more later:
data UserT f = âŠ
data ArticleT f = ...
Our next convention will be to use an underscore in front of our field names. We will also, unlike Persistent, specify the type name in the field names. With these conventions, Iâm following the advice of the libraryâs creator, Travis.
data UserT f = { _userId :: ... , _userName :: ⊠, _userEmail :: ⊠, _userAge :: ⊠, _userOccupation :: ⊠}
data ArticleT f = { _articleId :: ⊠, _articleTitle :: ⊠, _articleBody :: ⊠, _articlePublishedTime :: ⊠}
So when we specify the actual types of each field, weâll just put the relevant data type, like Int, Text or whatever, right? Well, not quite. To complete our types, we're going to fill in each field with the type we want, except specified via Columnar f. Also, we'll derive Generic on both of these types, which will allow Beam to work its magic:
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)
Now there are a couple small differences between this and our previous schema. First, we have the primary key as an explicit field of our type. With Persistent, we separated it using the Entityabstraction. We'll see below how we can deal with situations where that key isn't known. The second difference is that (for now), we've left out the userId field on the article. We'll add this when we deal with primary keys.
Columnar
So what exactly is this Columnar business about? Well under most circumstances, we'd like to specify a User with the raw field types. But there are some situations where we'll have to use a more complicated type for an SQL expression. Let's start with the simple case first.
Luckily, Columnar works in such a way that if we useIdentity for f, we can use raw types to fill in the field values. We'll make a type synonym specifically for this identity case. We can then make some examples:
type User = UserT Identitytype Article = ArticleT Identity
user1 :: Useruser1 = User 1 "James" "james@example.com" 25 "programmer"
user2 :: Useruser2 = User 2 "Katie" "katie@example.com " 25 "engineer"
users :: [User]users = [ user1, user2 ]
As a note, if you find it cumbersome to repeat the Columnar keyword, you can shorten it to 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)
Now, our initial examples will assign all our fields with raw values. So we wonât initially need to use anything for the f parameter besides Identity. Further down though, we'll deal with the case of auto-incrementing primary keys. In this case, we'll use the default_ function, whose type is actually a Beam form of an SQL expression. In this case, we'll be using a different type for f, but the flexibility will allow us to keep using our User constructor!
Instances for our Types
Now that weâve specified our types, we can use the Beamable and Table type classes to tell Beam more about our types. Before we can make any of these types a Table, we'll want to assign its primary key type. So let's make a couple more type synonyms to represent these:
type UserId = PrimaryKey UserT Identitytype ArticleId = PrimaryKey ArticleT Identity
While weâre at it, letâs add that foreign key to our Article type:
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)
We can now generate instances for Beamable both on our main types and on the primary key types. We'll also derive instances for Show and Eq:
data UserT f = âŠ
deriving instance Show Userderiving instance Eq User
instance Beamable UserTinstance Beamable (PrimaryKey UserT)
data ArticleT f = âŠ
deriving instance Show Articlederiving instance Eq Article
instance Beamable ArticleTinstance Beamable (PrimaryKey ArticleT)
Now weâll create an instance for the Table class. This will involve some type family syntax. We'll specify UserId and ArticleId as our primary key data types. Then we can fill in the primaryKey function to match up the right field.
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
Accessor Lenses
Weâll do one more thing to mimic Persistent. The Template Haskell automatically generated lenses for us. We could use those when making database queries. Below, weâll use something similar. But weâll use a special function, tableLenses, to make these rather than Template Haskell. If you remember back to how we used the Servant Client library, we could create client functions by using client and matching it against a pattern. We'll do something similar with tableLenses. We'll use LensFor on each field of our tables, and create a pattern constructing an item.
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
Note we have to wrap the foreign key lens in UserId.
Creating Our Database
Now unlike Persistent, weâll create an extra type that will represent our database. Each of our two tables will have a field within this database:
data BlogDB f = BlogDB { _blogUsers :: f (TableEntity UserT) , _blogArticles :: f (TableEntity ArticleT) } deriving (Generic)
Weâll need to make our database type an instance of the Database class. We'll also specify a set of default settings we can use on our database. Both of these items will involve a parameter be, which stands for a backend, (e.g. SQLite, Postgres). We leave this parameter generic for now.
instance Database be BlogDB
blogDb :: DatabaseSettings be BlogDBblogDb = defaultDbSettings
Inserting Into Our Database
Now, migrating our database with Beam is a little more complicated than it is with Persistent. We might cover that in a later article. For now, weâll keep things simple, and use an SQLite database and migrate it ourselves. So letâs first create our tables. We have to follow Beamâs conventions here, particularly on the user_id__id field for our foreign key:
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 \ );
Now we want to write a couple queries that can interact with the database. Letâs start by inserting our raw users. We begin by opening up an SQLite connection, and weâll write a function that uses this connection:
import Database.SQLite.Simple (open, Connection)
main :: IO ()main = do conn <- open "blogdb1.db" insertUsers conn
insertUsers :: Connection -> IO ()insertUsers = ...
We start our expression by using runBeamSqlite and passing the connection. Then we use runInsert to specify to Beam that we wish to make an insert statement.
import Database.Beamimport Database.Beam.SQLite
insertUsers :: Connection -> IO ()insertUsers conn = runBeamSqlite conn $ runInsert $ ...
Now weâll use the insert function and signal which one of our tables we want out of our database:
insertUsers :: Connection -> IO ()insertUsers conn = runBeamSqlite conn $ runInsert $ insert (_blogUsers blogDb) $ ...
Last, since we are inserting raw values (UserT Identity), we use the insertValues function to complete this call:
insertUsers :: Connection -> IO ()insertUsers conn = runBeamSqlite conn $ runInsert $ insert (_blogUsers blogDb) $ insertValues users
And now we can check and verify that our users exist!
SELECT * FROM users;1|James|james@example.com|25|programmer2|Katie|katie@example.com|25|engineer
Letâs do the same for articles. Weâll use the pk function to access the primary key of a particular User:
article1 :: Articlearticle1 = Article 1 "First article" "A great article" 1531193221 (pk user1)
article2 :: Articlearticle2 = Article 2 "Second article" "A better article" 1531199221 (pk user2)
article3 :: Articlearticle3 = 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 Queries
Now that weâve inserted a couple elements, letâs run some basic select statements. In general for select, weâll want the runSelectReturningList function. We could also query for a single element with a different function if we wanted:
findUsers :: Connection -> IO ()findUsers conn = runBeamSqlite conn $ do users <- runSelectReturningList $ ...
Now weâll use select instead of insert from the last query. We'll also use the function all_ on our users field in the database to signify that we want them all. And that's all we need!:
findUsers :: Connection -> IO ()findUsers conn = runBeamSqlite conn $ do users <- runSelectReturningList $ select (all_ (_blogUsers blogDb)) mapM_ (liftIO . putStrLn . show) users
To do a filtered query, weâll start with the same framework. But now we need to enhance our select statement into a monadic expression. We'll start by selecting user from all our users:
findUsers :: Connection -> IO ()findUsers conn = runBeamSqlite conn $ do users <- runSelectReturningList $ select $ do user <- (all_ (_blogUsers blogDb)) ... mapM_ (liftIO . putStrLn . show) users
And weâll now filter on that by using guard_ and applying one of our lenses. We use a ==.operator for equality like in Persistent. We also have to wrap our raw comparison value with 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
And thatâs all we need! Beam will generate the SQL for us! Now letâs try to do a join. This is actually much simpler in Beam than with Persistent/Esqueleto. All we need is to add a couple more statements to our âselectâ on the articles. Weâll just filter them by the user 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
Thatâs all there is to it!
Auto-Incrementing Primary Keys
In the examples above, we hard-coded all our IDs. But this isnât typically what you want. We should let the database assign the ID via some rule, in our case auto-incrementing. In this case, instead of creating a User "value", we'll make an "expression". This is possible through the polymorphic fparameter in our type. We'll leave off the type signature since it's a bit confusing. But here's the expression we'll create:
user1' = User default_ (val_ "James") (val_ "james@example.com") (val_ 25) (val_ "programmer")
We use default_ to represent an expression that will tell SQL to use a default value. Then we lift all our other values with val_. Finally, we'll use insertExpressions instead of insertValues in our Haskell expression.
insertUsers :: Connection -> IO ()insertUsers conn = runBeamSqlite conn $ runInsert $ insert (_blogUsers blogDb) $ insertExpressions [ user1' ]
Then weâll have our auto-incrementing key!
Conclusion
That concludes our introduction to the Beam library. As we saw, Beam is a great library that lets you specify a database schema without using any Template Haskell. For more details, make sure to check out the documentation!
For a more in depth look at using Haskell libraries to make a web app, be sure to read our Haskell Web Series. It goes over some database mechanics as well as creating APIs and testing. As an added challenge, trying re-writing the code in that series to use Beam instead of Persistent. See how much of the Servant code needs to change to accommodate that.
And for more examples of cool libraries, download our Production Checklist! There are some more database and API libraries you can check out!
APPENDIX: COMPILER EXTENSIONS
{-# LANGUAGE DeriveGeneric #-}{-# LANGUAGE GADTs #-}{-# LANGUAGE OverloadedStrings #-}{-# LANGUAGE FlexibleContexts #-}{-# LANGUAGE FlexibleInstances #-}{-# LANGUAGE TypeFamilies #-}{-# LANGUAGE TypeApplications #-}{-# LANGUAGE StandaloneDeriving #-}{-# LANGUAGE TypeSynonymInstances #-}{-# LANGUAGE NoMonoMorphismRestriction #-}
Beam: Database Power without Template Haskell! was originally published in Hacker Noon on Medium, where people are continuing the conversation by highlighting and responding to this story.
Disclaimer
The views and opinions expressed in this article are solely those of the authors and do not reflect the views of Bitcoin Insider. Every investment and trading move involves risk - this is especially true for cryptocurrencies given their volatility. We strongly advise our readers to conduct their own research when making a decision.