10

All the tutorials and references that I could find about Persistent describe in great detail how Persistent can automatically create a new data-type, schema, migration, etc. out of a single definition in its DSL. However, I couldn't find an explanation on how to get Persistent to handle already existing data-types.

An example: Suppose I have an already existing Haskell module for some game logic. It includes a record type for a player. (It's meant to be used through lenses, hence the underscores.)

data Player = Player { _name   :: String
                     , _points :: Int
                     -- more fields ...
                     }
$(makeLenses ''Player)

Question: What's the canonical way to store such a type in a data-base with Persistent? Is there some type-class that I can implement. Or should I best define a new type through Persistent, e.g.

share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
PlayerEntry
    name Text
    points Int
|]

and then manually map between these types?

playerToEntry :: Player -> PlayerEntry
playerToEntry pl = PlayerEntry (pl^.name) (pl^.points)

entryToPlayer :: PlayerEntry -> Player
entryToPlayer e = Player (name e) (points e)
Lemming
  • 4,085
  • 3
  • 23
  • 36
  • Do you think it would make sense just to manually define the fields like [this](http://stackoverflow.com/q/14097615/439699)? – ace Aug 17 '15 at 13:56
  • @ace Sure, you could manually generate all the code which `mkPersist` generates for you. I posted this question originally, because I didn't find a place where it was well documented how to do that. However, as it turned out I actually needed extra columns in the table anyway. So, marshalling between two types turned out to be a good solution in my case. – Lemming Aug 17 '15 at 21:55

2 Answers2

3

From: http://www.yesodweb.com/book/persistent

{-# LANGUAGE TemplateHaskell #-}
module Employment where

import Database.Persist.TH

data Employment = Employed | Unemployed | Retired
    deriving (Show, Read, Eq)
derivePersistField "Employment"

The derivePersistField function is the template Haskell magic that makes it work.

Note, you need to do the derivePersistField thing in a separate file to where you do the mkPersist to avoid a TH phase error.

  • Thanks for your answer. Unfortunately, that's not exactly what I'm looking for. `derivePersistField` will generate a type for a *column* of a table. However, I need a type for a *whole table*. The way I solved it in my case was to generate a new type like `PlayerEntry` above and then manually marshal between `Player` and `PlayerEntry`. For some of the fields within my type I used `derivePersistField` to generate the corresponding columns. I guess I should write this up in an answer here. – Lemming Feb 24 '15 at 12:42
  • I would like to see how you did it @Lemming. I'm also trying to find a good solution for this issue and strongly considering moving away from persistent because of this. – ace Aug 17 '15 at 13:50
  • @ace As requested, I added my solution. – Lemming Aug 17 '15 at 21:53
1

My solution to this problem was to add a new type through Yesod's mkPersist, and manually marshal between those.

config/models:

PlayerEntry
    name Text
    points Int
    created UTCTime default=CURRENT_TIMESTAMP

Marshalling.hs:

fromPlayerEntry :: PlayerEntry -> Player
fromPlayerEntry PlayerEntry {..} = Player { name = playerName
                                          , points = playerPoints
                                          }

createPlayerEntry :: Text -> YesodDB App (Entity PlayerEntry)
createPlayerEntry name = do
    currentTime <- liftIO getCurrentTime
    let player = PlayerEntry { playerName = name
                             , playerPoints = 0
                             , playerCreated = currentTime
                             }
    playerId <- insert player
    return $ Entity playerId player

updatePlayerEntry :: PlayerEntryId -> Player -> YesodDB App ()
updatePlayerEntry playerId Player {..} =
    update playerId [ PlayerName =. name
                    , PlayerPoints =. points
                    ]

One possible advantage is that you can have fields in your table, that are not required in the internal record. In my example, it was useful to attach a creation date to the player. However, this was only used in the web-interface layer, it was never used in the internal game logic, which defined the Player type. However, due to the manual marshalling I could add that field to the same database table nonetheless.

Lemming
  • 4,085
  • 3
  • 23
  • 36