Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

MongoDB Abstraction Layer

branch: master
README.md

mt-mongodb - MongoDB Helper Module

This package provides a number of helper functions and type classes for MongoDB that work with the MongoDB Haskell binding.

This library provides a mechanism for storing and retrieving objects to and from MongoDB. Any type that is an instance of the MongoEntity type class can be stored in and retrieved from a MongoDB collection.

Type Classes

There are a number of type classes in the mt-mongdb package. The most important of these are as follows:

  • MongoValue - Any type that is an instance of this type class can be stored as a value in the database. All field types in record type constructors must be instances of this type class.

  • MongoEntity - Any object that you want to store and retrieve from a collection needs to be an instance of this type class.

Collection Names

The name of the MongoDB collection to which a MongoEntity instance is stored is by default assumed to be the name of the type. For instance, if a type MyType was defined as an instance of the MongoEntity type class using the provided Template Haskell function asMongoEntity, the name of the collection will be MyType:

data MyType = MyType String Int
asMongoEntity ''MyType useDefaults

The name of the collection can be overridden using the setCollectionName function as the argument to asMongoEntity. If we wanted our MyType type from above to be stored in the Foo collection, we could change the collection name on the asMongoEntity line:

asMongoEntity ''MyType $ do
  setCollectionName "Foo"

Field Names

By default mt-mongodb will name fields in collections the same as their field names. For example, if we had a MyType data constructor as follows:

data MyType = MyType { fieldA :: String
                     , fieldB :: Int
                     }
asMongoEntity ''MyType useDefaults

The default behaviour is to store the value MyType "john" 13 as:

{ _type: "MyType", fieldA: "john", fieldB: 13 }

In case this is not suitable, the field naming can be overridden for each constructor to the type. For example, if we wanted fieldA to be called name and fieldB to be called age, we could provide the following mapping to the asMongoEntity function:

asMongoEntity ''MyType $ do
  forConstructor 'MyType $
    assocFieldNames [ ( 'fieldA, "name" )
                    , ( 'fieldB, "age"  ) ]

Now the value MyType "john" 13 will be stored as:

{ _type: "MyType", name: "john", age: 13 }

Note: If there is a field that has the form typeNameId (for example myTypeId or userId) that field will not be stored in the collection. However, when reading from a document in the collection it will correspond to MongoDB's automatic _id field. It is assumed that the type of such a field will be either an ObjectId (or a specialisation of Key) or a Maybe ObjectId to allow Nothing to be used as a nullary value.

Type Constructors

This package provides support for the storage and retrieval of data types, including sum types. For example, we could define an Avatar type, which is stored in a collection Avatar. This type has the following definition:

data Avatar = NoAvatar
            | Gravatar { gravatarId :: String }
            | Avatar   { avatarData :: ByteString }
asMongoEntity ''Avatar useDefaults

By default, in order for mt-mongodb to know which of the three constructors to use an additional field _type is added to the object. The _type field contains the name of the constructor to Avatar to use (one of either NoAvatar, Gravatar or Avatar).

Quasi-Quoted Querying

As of version 0.1 of mt-mongo there is a quasi-quoter for MongoDB queries. This quoter will parse any JSON-style object definition. This is useful for writing queries that are not supported by the more basic querying interface.

To demonstrate the quasi-quoter, we will use the User type from the next chapter. To find all users whose telephone number is Nothing, we can use the following query:

select [mongo| userTelephone: null |] []

This is equivalent to:

select (filters [UserTelephone `eq` Nothing]) []

As another example, we could rewrite our findLogin function from the next chapter to use the mongo quasi-quoter:

findLogin :: Text -> Text -> Action (Maybe (UserId, User))
findLogin email password =
  selectOne [mongo| { userName: #{email}, userPassword: #{password} } |] []

If we wanted to find all the Post documents up to a certain date, we can use the following code:

now <- liftIO $ getCurrentTime
select [mongo| postDate: { $lte: #{now} } |] []

As is probably obvious, the #{...} sequence (borrowed from Hamlet) is used to paste Haskell expressions into the JSON. All pasted expressions must be an instance of the MongoValue type class. The main difference between the mongo quasi-quoter and Hamlet is that any valid Haskell expression can be pasted, for example:

select [mongo| field: #{case expr of { Left x -> x; Right y -> T.pack y }} |] []

Example

In this example we will examine the example1.hs file in the examples folder. This is a simple example showing how to store and retrieve Users and Posts from the database. By way of a manual we will describe a number of simple operations that can be performed on these collections.

Firstly, we need to define our User type:

data User = User { userName        :: Text
                 , userPassword    :: Text
                 , userEmail       :: Text
                 , userTelephone   :: Maybe Text
                 , userPermissions :: [UserPermission]
                 }
          deriving (Show)

Following this, we define the Key for our User type. This is used to represent the unqiue ID that is assigned to each object when inserted into the MongoDB database:

type UserId = Key User

We will also need to define the UserPermission arithmetic data type:

data UserPermission = CanAddUser
                    | CanRemoveUser
                    | CanModifyUser
                    deriving (Eq, Read, Show)

In order to be able to marshal this type to and from a MongoDB object, we need it to be an instance of the MongoValue type class. We define our instance simply in terms of read and show to make life somewhat simpler:

asMongoValue ''UserPermissions
  encodedViaShow

This is equivalent to defining the instance:

instance MongoValue UserPermission where
    toValue     = toValue . show
    fromValue v = (return . read) =<< fromValue v

This means that our list of UserPermission will be stored in the database as an array of strings. For example, if our User type was given the value:

User { userName        = "John"
     , userPassword    = "pass123"
     , userEmail       = "john@mail.com"
     , userTelephone   = Nothing
     , userPermissions = [CanAddPost, CanRemovePost]
     }

It will be stored into a MongoDB collection called User as follows:

{ _id: ObjectId("00000...")
, _type: "User"
, userName: "John"
, userPassword: "pass123"
, userEmail: "john@mail.com"
, userTelephone: null
, userPermissions: [ "CanAddPost", "CanRemovePost" ]
}

Next up we will define a Post type which represents a post to the site, with the associated user and a list of comments:

data Post = Post { postUser     :: UserId
                 , postTitle    :: Text
                 , postDate     :: UTCTime
                 , postContent  :: Text
                 , postComments :: [(UserId, UTCTime, Text)]
                 }

type PostId = Key Post

The comments on the post are stored as a list of tuples for brevity, rather than a record data type. Currently, tuples are marshalled as arrays in MongoDB.

Now we need to define the MongoValue, MongoEntity and HasFields instances for our User and Post types. However, rather than fiddle around we will use the Template Haskell function asMongoEntity provided in the Massive.Database.MongoDB.Template module:

asMongoEntity ''User useDefaults
asMongoEntity ''Post useDefaults

Inserting Entities

We can insert our record for John from above:

insertJohn :: (MongoDB.MonadIO' m) => MongoDB.Action m UserId
insertJohn = do
  let john = User { userName        = "John Smith"
                  , userPassword    = "pass123"
                  , userEmail       = "john.smith@gmail.com"
                  , userTelephone   = Just "0113 987 6543"
                  , userPermissions = [ CanAddUser, CanRemoveUser ]
                  }
  insert john

The insert function takes a value that is an instance of the MongoEntity type class and yields an action which marshals the entity to a BSON representation, saves this record in the associated collection (in this case the User collection) and then yields the unique ID of the object:

insert :: (MongoDB.MonadIO' m, MongoEntity a) => a -> MongoDB.Action m (Key a)

Getting an Entity by Key

We can get an entity by it's unique Key using the get function:

get :: (Functor m, MongoDB.MonadIO' m, MongoEntity a) => Key a -> MongoDB.Action m (Maybe a)

The specialization of the Key to a means that we can use this function without excessive type signatures. This is quite useful when used in a handler for a resource pattern in yesod, for example:

getEditPostR :: PostId -> GHandler sub MySite RepHtml
getEditPostR postId = do
  mPost <- db $ get postId
  case mPost of
    Just post -> ...
    ....

If we have many IDs, we can use the getMany function:

getMany :: (Functor m, MongoDB.MonadIO' m, MongoEntity a) => [Key a] -> MongoDB.Action m [Maybe a]

This function is currently expressed simply in terms of get as:

getMany = mapM get

We do not flatten the Maybes into a list (i.e., by lifting catMaybes), as it is important that the mapping from a Key of some type a to a value of type a is preserved.

Finding Entities with Filters

This MongoDB abstraction provides a convenient type-checked method for filtering elements in a collection. This is provided by the functions eq, ne, lt, lte, gt, gte, isIn and notIn. These filters can be used with the select, selectOne, count, deleteWhere, and updateWhere functions.

Let's say that we wanted to find all the users in the system that did not have telephone numbers (that is, their userTelephone field is Nothing). We can do this using the select function:

select (filters [UserTelephone `eq` Nothing]) []

alternatively, we can use the (==?) operator, which is an alias for eq:

select (filters [UserTelephone ==? Nothing]) []

We can also use MongoDB's equivalent statement:

select [mongo| { userTelephone: null } |]

These examples use the select function, which takes a document and yields list of tuples containing the object ID and value for all matching records:

select :: (Functor m, MonadBaseControlIO m, MongoEntity a) => Document a -> [SelectorOption] -> MongoDB.Action m [(Key a, a)]

Remember that MongoDB filters are unions; so to select based on two field values we simply add extra filters. For example, to select the user for a particular user-name and password:

findLogin :: (Functor m, MonadBaseControlIO m) => Text -> Text -> Action m (Maybe (UserId, User))
findLogin email password =
  selectOne [mongo| { userEmail: #{email}, userPassword: #{password} } |] []

If we wanted to find all posts before a certain date, we can use the lte filter:

select (filters [PostDate `lte` now])

or

select [mongo| postDate: { $lte: #{now} } |] []

The above code will yield a list of all posts whose date is before or on the current system time.

What about support for $or? Well, we can use the or operator. This operator takes two filters and generates a $or for the values:

now <- liftIO $ getCurrentTime
select (filters [(PostDate `lte` now) `or` (PostUser `eq` currentUser)]) []

Alternatively, we can use the MongoDB syntax:

select [mongo| $or: { postDate: { $lte: #{now} }, postUser: #{currentUser} } |] []

The above will yield all the posts on or before the current system time, or which have been authored by the currentUser.

Limiting, Offseting and Sorting Selections

You can limit, offset and sort the selection of documents using the list of SelectorOption in the second argument to the select functions.

If we wanted to select all posts by a specific user, ordered by the time at which they were posted and limited to a view of ten such posts, we could use:

select [mongo| postUser: #{user}|] [orderAsc PostDate, limit 10]

Updating Records

There are two ways to update records in MongoDB: either by updating a single record in a collection or by applying the modification to an entire collection based on a filter. These two methods are provided by the update and updateWhere functions:

update :: (Functor m, MongoDB.MonadIO' m, MongoEntity a) => Key a -> Document a -> MongoDB.Action m ()

updateWhere :: (Functor m, MongoDB.MonadIO' m, MongoEntity a) => Document a -> Document a -> MongoDB.Action m ()

Going back to our user and post example above, given a PostId and the details of a comment, we could update the post with the new comment as follows:

addComment :: (MongoDB.MonadIO' m) => PostId -> UserId -> Text -> MongoDB.Action m ()
addComment postId who said =
  now <- liftIO $ getCurrentTime
  update postId [mongo| $push: { postComments: #{(who, now, said)} } |]

Notice that we are using the paste expression to write the tuple. We could also have used an array and written postComments: [#{who}, #{now}, #{said}].

Alternatively, we can use UpdateOps:

  update postId (updates [PostComments `push` (who, now, said)])
Something went wrong with that request. Please try again.