Part 1
In this tutorial sequence, we'll walk through creating a schema for a simple shopping cart database. We'll start by defining a user table. Then, we'll show how beam makes it easy to manipulate data in our database. Finally, we'll demonstrate how beam lets us declare type-safe and composable queries.
Beam Module Structure
Beam makes extensive use of GHC's Generics mechanism. This extension means beam does not need to rely on template haskell.
To start defining beam schemas and queries, you only need to import the
Database.Beam
module. To interface with an actual database, you'll need to
import one of the database backends. We'll see how to use the Sqlite backend
here (found in the beam-sqlite
package). Now, open up a GHCi prompt for us to
use. Make sure to get the beam-core
, beam-sqlite
and text
packages.
$ stack repl --package beam-core --package beam-sqlite --package sqlite-simple --package beam-migrate --package text
This will put you into a GHCi prompt with the beam-core
and beam-sqlite
packages available. We also include the sqlite-simple
package. Beam mainly
manages querying and data marshalling. Connections to the backends are done via
backend specific packages. In this case, beam-sqlite
uses the sqlite-simple
backend.
Before starting, we'll need to enable some extensions.
> :set -XDeriveGeneric -XGADTs -XOverloadedStrings -XFlexibleContexts -XFlexibleInstances -XTypeFamilies -XTypeApplications -XDeriveAnyClass
And import some modules...
import Database.Beam
import Database.Beam.Sqlite
import Data.Text (Text)
Defining our first table
Beam tables are regular Haskell data types with a bit of scaffolding. Thankfully, the magic of the modern Haskell type system allows us to remove the overhead and the syntactic fuzz of the scaffolding in most situations.
We start by declaring a data structure named UserT
. As a matter of convention, Beam table types
are suffixed with 'T'. Table types have only one constructor. Again, as a matter of convention, the
constructor has the same name as the table, but without the 'T' suffix. We'll soon see the reason
for this convention.
In this tutorial, I'll prefix all record selectors with an underscore. This is a matter of personal
preference. One reason for the prefix is that it plays nicely with the lens
library. Beam does not
necessitate the use of lens
(in fact Beam includes its own mechanism to generically derive van
Laarhoven lenses), but I recognize that some programmers use lens
quite a lot.
data UserT f
= User
{ _userEmail :: Columnar f Text
, _userFirstName :: Columnar f Text
, _userLastName :: Columnar f Text
, _userPassword :: Columnar f Text }
deriving Generic
This data type might look very complicated, so I'd like to show you that it's not that scary. Let's see if we can use GHCi to help us.
Prelude Database.Beam.Sqlite Database.Beam Data.Text> :t User
User
:: Columnar f Text
-> Columnar f Text -> Columnar f Text -> Columnar f Text -> UserT f
Hmm... That did not help much. Let's see what happens if we bind f
to something concrete, like Identity
. Using the TypeApplications
extension:
Prelude Database.Beam Database.Beam.Sqlite Data.Text> :t (User @Identity)
(User @Identity) :: Text -> Text -> Text -> Text -> UserT Identity
Woah! That looks a lot like what we'd expect if we had declared the type in the "regular" Haskell way:
data User = User
{ _userEmail :: Text
, _userFirstName :: Text
, _userLastName :: Text
, _userPassword :: Text }
This functionality is due to the fact that Columnar
is a type family defined
such that for any x
, Columnar Identity x = x
. This strategy is known as
defunctionalization 1 or higher-kinded data types 2.
Knowing this, let's define a type synonym to make our life easier.
type User = UserT Identity
type UserId = PrimaryKey UserT Identity
Now you can see why we named the type of the table UserT
and its constructor
User
. This allows us to use the "regular" User
constructor to construct
values of type User
. We can use the StandaloneDeriving
and
TypeSynonymInstances
extensions to derive instances of Show
and Eq
for the
'regular' datatype.
> :set -XStandaloneDeriving -XTypeSynonymInstances -XMultiParamTypeClasses
Now we can derive Show
and Eq
instances.
deriving instance Show User
deriving instance Eq User
Note that this does require us to use an explicit type signature where we otherwise wouldn't. For example,
Prelude Database.Beam.Sqlite Database.Beam Data.Text> User "john@example.com" "John" "Smith" "password!"
<interactive>:46:2: error:
* No instance for (Show (UserT f0)) arising from a use of ‘print’
* In a stmt of an interactive GHCi command: print it
Here, GHC is complaining that it cannot infer the type of the f
parameter
based on the values we've supplied. This is because the Columnar
type family
is non-injective. However, an explicit type annotation fixes it all up.
Prelude Database.Beam.Sqlite Database.Beam Data.Text> User "john@example.com" "John" "Smith" "password!" :: User
User {_userEmail = "john@example.com", _userFirstName = "John", _userLastName = "Smith", _userPassword = "password!"}
You can also use type applications, if you like that style better:
Prelude Database.Beam Database.Beam.Sqlite Data.Text> User @Identity "john@example.com" "John" "Smith" "password!"
User {_userEmail = "john@example.com", _userFirstName = "John", _userLastName = "Smith", _userPassword = "password!"}
Usually, you won't need to deal with this, as you'll explicitly annotate your
top-level functions to use the User
type.
Teaching Beam about our table
We've defined a type that can represent the data in our table. Now, let's
inform beam that we'd like to use UserT
as a table.
All beam tables need to implement the Beamable
type class. Due to GHC's
DeriveGeneric and DefaultSignatures extensions, all these methods can be written
for us by the compiler at compile-time!
instance Beamable UserT
Tip
If you turn on the DeriveAnyClass
feature, you can simply derive
the Beamable
type class. For example, the type
data UserT f
= User
{ _userEmail :: Columnar f Text
, _userFirstName :: Columnar f Text
, _userLastName :: Columnar f Text
, _userPassword :: Columnar f Text }
deriving Generic
could be written
data UserT f
= User
{ _userEmail :: Columnar f Text
, _userFirstName :: Columnar f Text
, _userLastName :: Columnar f Text
, _userPassword :: Columnar f Text }
deriving (Generic, Beamable)
Additionally, all beam tables must implement the Table
type class, which we
can use to declare a primary key.
The only thing we need to provide is the type of the primary keys for users, and
a function that can extract the primary key from any UserT f
object. To do
this, add the following lines to the instance declaration.
instance Table UserT where
data PrimaryKey UserT f = UserId (Columnar f Text) deriving (Generic, Beamable)
primaryKey = UserId . _userEmail
The data
declaration is similar to a toplevel data definition, construct a key
for UserT
with the UserId
constructor like a regular table.
userKey = UserId "john@doe.org"
Defining our database
Now that we have our table, we're going to define a type to hold information about our
database. Defining our database is going to follow the same pattern as defining a table. We'll
define a higher-kinded datatype and then declare an instance of Database
, and let the compiler
figure most of it out.
Tables are a collection of Columnar
values. Databases are a collection of
entities, such as tables. Many database systems can also hold other entities
(such as views, domain types, etc). Beam allows you to declare these as well 3.
Our database consists of only one table.
data ShoppingCartDb f = ShoppingCartDb
{ _shoppingCartUsers :: f (TableEntity UserT) }
deriving (Generic, Database be)
Note
By deriving Database be
we actually allowed our database to be used with any Beam
backend that supports it. We could also have explicitly listed the database backends we
liked. For example, specifying deriving (Generic, Database Sqlite, Database Postgres)
would
derive instances only for SQLite or Postgres.
The next step is to create a description of the particular database we'd like to create. This involves giving each of the tables in our database a name. If you've named all your database selectors using camel case, beam can automatically figure out what all the table names should be. If you haven't, or you have multiple tables holding the same type in your database, you might have to manually name your tables. For now, we'll let beam do the hard work 4.
shoppingCartDb :: DatabaseSettings be ShoppingCartDb
shoppingCartDb = defaultDbSettings
Adding users to our database
Let's add some users to our database. As we said above, beam is
backend-agnostic. However, backend integration libraries are maintained in the
official beam repository. The beam-sqlite
package offers straightforwards
integration with the sqlite-simple
library.
First, let's create a sqlite3 database with the right schema. Open up terminal, and do
$ sqlite3 shoppingcart1.db
SQLite version 3.14.0 2016-07-26 15:17:14
Enter ".help" for usage hints.
sqlite> CREATE TABLE cart_users (email VARCHAR NOT NULL, first_name VARCHAR NOT NULL, last_name VARCHAR NOT NULL, password VARCHAR NOT NULL, PRIMARY KEY( email ));
sqlite>
Now, let's open the database in Haskell.
import Database.SQLite.Simple
conn <- open "shoppingcart1.db"
Now let's add a few users. We'll give each user an MD5 encoded password too.
We'll use the runBeamSqliteDebug
function (supplied by beam-sqlite
) to
output the statements that beam would normally run. In production, you'd use the
runBeamSqlite
function, or use the backend integration packages to directly
use the underlying backend library.
runBeamSqliteDebug putStrLn {- for debug output -} conn $ runInsert $
insert (_shoppingCartUsers shoppingCartDb) $
insertValues [ User "james@example.com" "James" "Smith" "b4cc344d25a2efe540adbf2678e2304c" {- james -}
, User "betty@example.com" "Betty" "Jones" "82b054bd83ffad9b6cf8bdb98ce3cc2f" {- betty -}
, User "sam@example.com" "Sam" "Taylor" "332532dcfaa1cbf61e2a266bd723612c" {- sam -} ]
The runInsert
function runs an insert statement, which we construct using the
insert
function. Since we're inserting concrete values, we use the
insertValues
function to supply the values. We can also use the
insertExpressions
function to insert arbitrary SQL expressions, or the
insertFrom
to insert the results of an arbitrary select (the INSERT INTO ..
SELECT ..
syntax).
Because we're in debug mode, we'll see the SQL that beam is running:
INSERT INTO "cart_users"("email", "first_name", "last_name", "password") VALUES (?, ?, ?, ?), (?, ?, ?, ?), (?, ?, ?, ?)
-- With values: [SQLText "james@example.com",SQLText "James",SQLText "Smith",SQLText "b4cc344d25a2efe540adbf2678e2304c",SQLText "betty@example.com",SQLText "Betty",SQLText "Jones",SQLText "82b054bd83ffad9b6cf8bdb98ce3cc2f",SQLText "sam@example.com",SQLText "Sam",SQLText "Taylor",SQLText "332532dcfaa1cbf61e2a266bd723612c"]
The ?
represent the values passed to the database (beam uses the backend's
value interpolation to avoid SQL injection attacks).
Querying the database
Now let's write some queries for the database. Let's get all the users we just added. Click between the tabs to see the SQL and console output generated
let allUsers = all_ (_shoppingCartUsers shoppingCartDb)
runBeamSqliteDebug putStrLn conn $ do
users <- runSelectReturningList $ select allUsers
mapM_ (liftIO . putStrLn . show) users
SELECT "t0"."email" AS "res0",
"t0"."first_name" AS "res1",
"t0"."last_name" AS "res2",
"t0"."password" AS "res3"
FROM "cart_users" AS "t0";
-- With values: []
User {_userEmail = "james@example.com", _userFirstName = "James", _userLastName = "Smith", _userPassword = "b4cc344d25a2efe540adbf2678e2304c"}
User {_userEmail = "betty@example.com", _userFirstName = "Betty", _userLastName = "Jones", _userPassword = "82b054bd83ffad9b6cf8bdb98ce3cc2f"}
User {_userEmail = "sam@example.com", _userFirstName = "Sam", _userLastName = "Taylor", _userPassword = "332532dcfaa1cbf61e2a266bd723612c"}
Note
The --
at the ends of the console output lines are an artifact of the
documentation build process. They won't appear in your console.
Next let's suppose you wanted to sort the users into order by their first name,
and then descending by their last name. We can use the orderBy_
function to
order the query results. This is similar to the sortBy
function for lists.
let sortUsersByFirstName = orderBy_ (\u -> (asc_ (_userFirstName u), desc_ (_userLastName u))) (all_ (_shoppingCartUsers shoppingCartDb))
runBeamSqliteDebug putStrLn conn $ do
users <- runSelectReturningList $ select sortUsersByFirstName
mapM_ (liftIO . putStrLn . show) users
SELECT "t0"."email" AS "res0",
"t0"."first_name" AS "res1",
"t0"."last_name" AS "res2",
"t0"."password" AS "res3"
FROM "cart_users" AS "t0"
ORDER BY "t0"."first_name" ASC,
"t0"."last_name" DESC;
-- With values: []
User {_userEmail = "betty@example.com", _userFirstName = "Betty", _userLastName = "Jones", _userPassword = "82b054bd83ffad9b6cf8bdb98ce3cc2f"}
User {_userEmail = "james@example.com", _userFirstName = "James", _userLastName = "Smith", _userPassword = "b4cc344d25a2efe540adbf2678e2304c"}
User {_userEmail = "sam@example.com", _userFirstName = "Sam", _userLastName = "Taylor", _userPassword = "332532dcfaa1cbf61e2a266bd723612c"}
We can use limit_
and offset_
in a similar manner to take
and drop
respectively.
let boundedQuery = limit_ 1 $ offset_ 1 $
orderBy_ (asc_ . _userFirstName) $
all_ (_shoppingCartUsers shoppingCartDb)
runBeamSqliteDebug putStrLn conn $ do
users <- runSelectReturningList (select boundedQuery)
mapM_ (liftIO . putStrLn . show) users
SELECT "t0"."email" AS "res0",
"t0"."first_name" AS "res1",
"t0"."last_name" AS "res2",
"t0"."password" AS "res3"
FROM "cart_users" AS "t0"
ORDER BY "t0"."first_name" ASC
LIMIT 1
OFFSET 1;
-- With values: []
User {_userEmail = "james@example.com", _userFirstName = "James", _userLastName = "Smith", _userPassword = "b4cc344d25a2efe540adbf2678e2304c"}
Aggregations
Sometimes we also want to group our data together and perform calculations over the groups of data. SQL calls these aggregations.
The simplest aggregation is counting. We use the aggregate_
function to create
aggregations. For example, to count all users, we can use the countAll_
aggregation. We also use the runSelectReturningOne
function to get at most one
record from the database.
let userCount = aggregate_ (\u -> as_ @Int32 countAll_) (all_ (_shoppingCartUsers shoppingCartDb))
runBeamSqliteDebug putStrLn conn $ do
Just c <- runSelectReturningOne $ select userCount
liftIO $ putStrLn ("We have " ++ show c ++ " users in the database")
SELECT COUNT(*) AS "res0"
FROM "cart_users" AS "t0";
-- With values: []
We have 3 users in the database
Note
countAll_
is happy to unmarshal into any Integral
type, so we use as_
to constrain the type to Int32
.
Maybe we'd like something a little more interesting, such as the number of users
for each unique first name. We can also express these aggregations using the
aggregate_
function. In order to get interesting results, we'll need to add
more users to our database.
runBeamSqliteDebug putStrLn conn $
runInsert $
insert (_shoppingCartUsers shoppingCartDb) $
insertValues [ User "james@pallo.com" "James" "Pallo" "b4cc344d25a2efe540adbf2678e2304c" {- james -}
, User "betty@sims.com" "Betty" "Sims" "82b054bd83ffad9b6cf8bdb98ce3cc2f" {- betty -}
, User "james@oreily.com" "James" "O'Reily" "b4cc344d25a2efe540adbf2678e2304c" {- james -}
, User "sam@sophitz.com" "Sam" "Sophitz" "332532dcfaa1cbf61e2a266bd723612c" {- sam -}
, User "sam@jely.com" "Sam" "Jely" "332532dcfaa1cbf61e2a266bd723612c" {- sam -} ]
INSERT INTO "cart_users"("email",
"first_name",
"last_name",
"password")
VALUES (?, ?, ?, ?),
(?, ?, ?, ?),
(?, ?, ?, ?),
(?, ?, ?, ?),
(?, ?, ?, ?);
-- With values: [SQLText "james@pallo.com",SQLText "James",SQLText "Pallo",SQLText "b4cc344d25a2efe540adbf2678e2304c",SQLText "betty@sims.com",SQLText "Betty",SQLText "Sims",SQLText "82b054bd83ffad9b6cf8bdb98ce3cc2f",SQLText "james@oreily.com",SQLText "James",SQLText "O'Reily",SQLText "b4cc344d25a2efe540adbf2678e2304c",SQLText "sam@sophitz.com",SQLText "Sam",SQLText "Sophitz",SQLText "332532dcfaa1cbf61e2a266bd723612c",SQLText "sam@jely.com",SQLText "Sam",SQLText "Jely",SQLText "332532dcfaa1cbf61e2a266bd723612c"]
Now we can use aggregate_
to both group by a user's first name, and then count
the number of users.
let numberOfUsersByName = aggregate_ (\u -> (group_ (_userFirstName u), as_ @Int32 countAll_)) $
all_ (_shoppingCartUsers shoppingCartDb)
runBeamSqliteDebug putStrLn conn $ do
countedByName <- runSelectReturningList $ select numberOfUsersByName
mapM_ (liftIO . putStrLn . show) countedByName
SELECT "t0"."first_name" AS "res0",
COUNT(*) AS "res1"
FROM "cart_users" AS "t0"
GROUP BY "t0"."first_name";
-- With values: []
("Betty",2)
("James",3)
("Sam",3)
Conclusion
In this tutorial, we've covered creating a database schema, opening up a beam database, inserting values into the database, and querying values from them. We used the knowledge we learned to create a partial shopping cart database that contains information about users. In the next tutorial, we'll delve deeper into the some of the query types and show how we can create relations between tables. We'll also use the monadic query interface to create SQL joins.
Until next time!
If you have any questions about beam, feel free to send them to travis@athougies.net . Pull requests and bug reports are welcome on GitHub.
-
Thanks to various bloggers for pointing this out. You can read more about this technique here. ↩
-
https://reasonablypolymorphic.com/blog/higher-kinded-data/ ↩
-
Adding entities other than tables is covered in more depth in the user guide. ↩
-
More on the default naming conventions can be found in the models section of the user guide. We'll talk about how to override defaults in the next sections. ↩