data-basic-0.2.0.0: A database library with a focus on ease of use, type safety and useful error messages

Safe HaskellNone
LanguageHaskell2010

Internal.Data.Basic.Tutorial

Description

This tutorial describes how to use the basic library. Usually you would use the functions provided in the Data.Basic.TH module Internal.to generate all of the declarations in this tutorial from your database schema.

Basic is a database-first library meaning the schema comes from the database instead of your code. The library provides mechanisms for "explaining" your schema to the compiler. It can then use this information to provide a typesafe and convenient access and control of your data.

We start by defining a data type.

 data User = User { _serId   :: Key
                  , _serName :: Text } deriving (Eq, Ord, Read, Show)
 

Most of the functionality is implemented through lenses so we need to generate them for our datatype.

 makeLenses ''User
 

Next we provide a set of instances for our type. These describe how our type maps to a database table. We use type level strings to represent database names of the fields. Instances are needed for each field and for each constraint on the table. We also need a FromRow instance so the type can actually be deserialized from the query result.

 instance Table User where
     -- the database name for this table
     type TableName User = "blog_user"

     -- a type level list of all the fields in this table
     type TableFields User = ["id", "name"]

     -- a type level list of constraints on this table; each of these will need a corresponding
     -- instance that provides additional info
     type TableConstraints User = '[ 'Unique "blog_user_pkey"]

     -- the table can optionally have a primary key; for this we use a type level Maybe value
     type TablePrimaryKey User = 'Just "blog_user_pkey"

     -- a type level list of fields that are either Required or DynamicDefault
     type TableRequiredFields User = ['Required "id", 'Required "name"]

     -- a default user
     -- don't worry about undefined values, the types will make sure you can't accidentally evaluate
     -- them
     newEntity = Entity (User undefined undefined)

 instance UniqueConstraint "blog_user_pkey" where
     -- the table which this constraint targets
     type UniqueTable "blog_user_pkey" = User

     -- you can have multiple fields that make up one unique constraint
     type UniqueFields "blog_user_pkey" = '["id"]

 -- PrimaryKeyConstraint is really just a synonym for a unique constraint + the condition that
 -- all the values must not be null
 instance PrimaryKeyConstraint "blog_user_pkey"

 -- each field gets an instance saying what Haskell type it maps to and providing a lens
 instance TableField User "id" where
     type TableFieldType User "id" = Key
     tableFieldLens = serId

 instance TableField User "name" where
     type TableFieldType User "name" = Text
     tableFieldLens = serName

 instance FromRow User where
     fromRow = User <$> field <*> field
 

Now we do the same for a "blog_post" table.

@ data Post = Post { _ostId :: Key , _ostName :: Text , _ostUserId :: Key } deriving (Eq, Ord, Read, Show)

instance Table Post where type TableName Post = "blog_post" type TableFields Post = ["id", "name", "author"] type TableConstraints Post = '[ 'ForeignKey "blog_post_author_fkey"] type TablePrimaryKey Post = 'Just "blog_post_pkey" type TableRequiredFields Post = ['Required "id", 'Required "name", 'Required "author"] newEntity = Entity (Post undefined undefined)