Hi and thanks for your time. I'm trying to create a website that features a button that increments a counter. I want the current counter to be persistent and if somebody goes to my page, the current counter should be displayed. A request should be send every time I click the button to increment the counter. The request does not contain any information about the counter value. The server - in my case a warp web server - should update the counter value in the database, read the value after the update and then send it to the frontend if successful, of an error message if not.
So far, only the updating works, since I did not manage to figure out how to get the data from the database to the frontend. Here is the code from my Repository module that should do the updating:
{-# LANGUAGE EmptyDataDecls, FlexibleContexts, GADTs, GeneralizedNewtypeDeriving#-}
{-# LANGUAGE MultiParamTypeClasses, OverloadedStrings, QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell, TypeFamilies, DataKinds, FlexibleInstances#-}
{-# LANGUAGE DerivingStrategies, StandaloneDeriving, UndecidableInstances #-}
module Repository (increaseCounter) where
import Control.Monad.IO.Class (liftIO)
import Control.Monad.Logger (runStderrLoggingT)
import Database.Persist
import Database.Persist.Sqlite
import Database.Persist.TH
import Control.Monad.Reader
import Data.Text
import Data.Maybe
-- setting up the Counter entity with a unique key so I can use the getBy function
share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
Counter
counterName String
counterCount Int Maybe
UniqueCounterName counterName
deriving Show
|]
increaseCounter :: IO ()
increaseCounter =
runStderrLoggingT $ withSqliteConn "//absolute/path/database.db" $ runSqlConn $ do
runMigration migrateAll -- only for developing
updateWhere [CounterCounterName ==. "unique name"] [CounterCounterCount +=. Just 1]
counterEntity <- getBy $ UniqueCounterName name
liftIO $ print counterEntity
This compiles and actually persists the counter and updates the value every time its called. But as you can tell from the types, after update it only prints the counter value to the console. I seem to have problems understanding how to use the data that is returned from the getBy function. The docs say:
getBy :: (PersistUniqueRead backend, MonadIO m, PersistRecordBackend record backend) =>
Unique record -> ReaderT backend m (Maybe (Entity record))
Is the 'backend m' basically a nested monad?
Assuming I simply want to send the value of the counter if it is Just Int
and I want to return -1 if it is Nothing
.
I assume I can not modify the increaseCounter function so that its type is Maybe Int
. But how do I pass functions into the monad / access the value inside to send a response to the frontend?
If this question is to superficial and/or I lack too much knowledge to proceed at this point, can you recommend good sources for information? Something like a good tutorial or youtube channel or something?
Thanks!
You can ignore all the monadic parts of getBy
's type signature. Provided you get your code to type check, counterEntity
has type Maybe (Entity Counter)
, and that's all that's important here.
The counterEntity
is Nothing
if the query fails (i.e., no record in the table for that counter). Otherwise, it's Just
an Entity Counter
containing the retrieved record:
case counterEntity of
Just e -> ...
This e :: Entity Counter
can be turned into a Counter
via entityVal
. The desired field of that Counter
can be extracted with counterCounterCount
. The result will be a Maybe Int
because you've tagged that field as Maybe
, so you'll have another layer of Maybe
to unpack:
case counterEntity of
Nothing -> -1 -- no record for this counter
Just e -> case counterCounterCount (entityVal e) of
Nothing -> -1 -- record, but counter value missing
Just v -> v
You'll want to return this value from increaseCounter
, so the final version will look like this:
increaseCounter :: IO Int
increaseCounter =
runStderrLoggingT $ withSqliteConn "//absolute/path/database.db" $ runSqlConn $ do
runMigration migrateAll -- only for developing
updateWhere [CounterCounterName ==. "unique name"] [CounterCounterCount +=. Just 1]
counterEntity <- getBy $ UniqueCounterName "unique name"
return $ case counterEntity of
Nothing -> -1
Just e -> case counterCounterCount . entityVal $ e of
Nothing -> -1
Just v -> v
Wherever you previously successfully used increaseCounter
to increase the counter, you'll now want to write:
updatedCounterValue <- increaseCounter
and you can pass the plain old updatedCounterValue :: Int
to the front end.
You might find it more sensible to use upsertBy
, which can insert the counter record if it's missing and update it otherwise. It also returns the inserted/updated entity, saving you a separate getBy
call. I also don't understand why you've tagged counterCount
with Maybe
. Why would you insert a counter into your table with no value? Wouldn't "0" be a better starting value if you wanted to indicate "no count"?
So, I'd rewrite the schema as:
Counter
counterName String
counterCount Int -- no Maybe
UniqueCounterName counterName
deriving Show
and the increaseCounter
function as:
increaseCounter :: IO Int
increaseCounter =
runStderrLoggingT $ withSqliteConn "//absolute/path/database.db" $ runSqlConn $ do
runMigration migrateAll -- only for developing
let name = "unique name"
counterEntity <- upsertBy (UniqueCounterName name)
(Counter name 1)
[CounterCounterCount +=. 1]
return $ counterCounterCount (entityVal counterEntity)
To either insert a 1-count or increase an existing count.
Finally, as a general design approach, it's probably better to move the database migration and connection setup into the main
function, and maybe use a pool of connections, something like:
#!/usr/bin/env stack
-- stack --resolver lts-18.0 script
-- --package warp
-- --package persistent
-- --package persisent-sqlite
{-# LANGUAGE EmptyDataDecls, FlexibleContexts, GADTs, GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses, OverloadedStrings, QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell, TypeFamilies, DataKinds, FlexibleInstances#-}
{-# LANGUAGE DerivingStrategies, StandaloneDeriving, UndecidableInstances #-}
import Control.Monad.Logger (runStderrLoggingT)
import Database.Persist
import Database.Persist.TH
import Database.Persist.Sqlite
import Control.Monad.Reader
import Network.HTTP.Types
import Network.Wai
import Network.Wai.Handler.Warp
import qualified Data.ByteString.Lazy.Char8 as C8
share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
Counter
counterName String
counterCount Int
UniqueCounterName counterName
deriving Show
|]
increaseCounter :: ReaderT SqlBackend IO Int
increaseCounter = do
let name = "unique name"
counterEntity <- upsertBy (UniqueCounterName name)
(Counter name 1)
[CounterCounterCount +=. 1]
return $ counterCounterCount (entityVal counterEntity)
main :: IO ()
main = runStderrLoggingT $ withSqlitePool "some_database.db" 5 $ \pool -> do
runSqlPool (runMigration migrateAll) pool
let runDB act = runSqlPool act pool
liftIO $ run 3000 $ \req res -> do
count <- runDB $ increaseCounter
res $ responseLBS
status200
[("Content-Type", "text/plain")]
(C8.pack $ show count ++ "\n")