You’ll need a Haskell project named using-json
for this tutorial.
The setup is identical to the setup in Getting Started
aside from the package name, so we’ll avoid explaining it again here.
SQL has a rigid schema. Using JSON inside SQL allows for pockets of schemaless data, that is still queryable using PostgreSQL’s built-in functionality.
This document explains how to use Orville with the JSONB data type that PostgreSQL natively supports.
Project initialization is similar to previous tutorials, but additional dependencies like Aeson have been added. Aeson is a JSON library for Haskell.
Here’s the beginning of our src/Main.hs
file.
module Main
( mainwhere
)
import qualified Orville.PostgreSQL as O
import qualified Orville.PostgreSQL.AutoMigration as AutoMigration
import qualified Orville.PostgreSQL.Marshall as Marshall
import qualified Orville.PostgreSQL.Raw.RawSql as RawSql
import Control.Monad.IO.Class (MonadIO(liftIO))
import Data.Aeson (Value, eitherDecodeStrict')
import Data.Aeson.Text (encodeToLazyText)
import qualified Data.Aeson as Aeson
import qualified Data.Int as Int
import qualified Data.Text as T
import qualified Data.Text.Encoding as Enc
import qualified Data.Text.Lazy as LazyText
import qualified Data.Vector as Vector
Let’s suppose we have an example entity with an ID, and some arbitrary JSON data in a column called ‘tags’.
Note how fooTagsField
below uses the Value
type from the Aeson library.
Remember that the Value
contains its own Null
constructor, which is
distinct from SQL’s NULL
. So we can have JSON nulls in this field, but no SQL
nulls.
We could also use a custom type with FromJSON
/ToJSON
instances, since
jsonb
allows for that too. Aeson is not the focus of this document though.
data Foo = Foo
fooId :: Int.Int32
{ fooTags :: Value
,
}deriving Show
fooIdField :: O.FieldDefinition O.NotNull Int.Int32
=
fooIdField "id"
O.integerField
fooTagsField :: O.FieldDefinition O.NotNull Value
=
fooTagsField "tags"
aesonValueField
Before we can define the corresponding SqlMarshaller
, we’ll need to define the
aesonValueField
helper function. This is done tryConvertSqlType
along with
jsonb
field to apply Aeson encoding and decode.
aesonValueField :: String -> O.FieldDefinition O.NotNull Value
=
aesonValueField name
O.convertField
(O.tryConvertSqlType encodeJSON decodeJSON)
(O.jsonbField name)
decodeJSON :: T.Text -> Either String Value
=
decodeJSON . Enc.encodeUtf8
eitherDecodeStrict'
encodeJSON :: Value -> T.Text
=
encodeJSON . encodeToLazyText
LazyText.toStrict
Let’s define the SqlMarshaller
and the table. This is standard stuff, no
surprises here.
fooMarshaller :: O.SqlMarshaller Foo Foo
=
fooMarshaller Foo
<$> O.marshallField fooId fooIdField
<*> O.marshallField fooTags fooTagsField
table :: O.TableDefinition (O.HasKey Int.Int32) Foo Foo
=
table "json_demo" (O.primaryKey fooIdField) fooMarshaller
O.mkTableDefinition
With all definitions done, we can write main
. Orville will also use the parts
of the SqlType
during migration.
main :: IO ()
= do
main <-
pool
O.createConnectionPoolO.ConnectionOptions
= "host=localhost user=postgres password=postgres"
{ O.connectionString = O.DisableNoticeReporting
, O.connectionNoticeReporting = O.OneStripePerCapability
, O.connectionPoolStripes = 10
, O.connectionPoolLingerTime = O.MaxConnectionsPerStripe 1
, O.connectionPoolMaxConnections
}
$ do
O.runOrville pool AutoMigration.SchemaTable table ]
AutoMigration.autoMigrateSchema AutoMigration.defaultOptions [ <- O.deleteEntity table 0
_
We’ll construct a JSON value using the Aeson library, which makes this look
fairly verbose. But imagine that the Array
value below was read from a file,
or received over HTTP from a web browser.
<- O.insertEntity table Foo { fooId = 0
_ = Aeson.Array $ Vector.fromList
, fooTags Aeson.Number 1
[ Aeson.Number 2
, Aeson.Number 3
,
]
}. print =<< O.findEntity table 0
liftIO
Using raw SQL, we can use PostgreSQL’s built-in JSONB functions. Let’s suppose
we want a row returned for each of the values in the Array
above.
ID | Tag |
---|---|
0 | 1 |
0 | 2 |
0 | 3 |
We can use an SqlMarshaller
to produce a result like this, even though there
is no table for the returned schema. The programmer must ensure correspondence
of the SQL and the SqlMarshaller
. If they don’t match, an exception will be
thrown.
We’ll have the SqlMarshaller
work with tuples and marshallReadOnlyField
s.
These allow for succintly defining a quick one-off SqlMarshaller
.
let
marshaller :: O.SqlMarshaller w (Int.Int32, Value)
=
marshaller <$> O.marshallReadOnlyField fooIdField
(,) <*> O.marshallReadOnlyField (aesonValueField "tag")
<-
readEntities
O.executeAndDecodeO.SelectQuery
"SELECT id, jsonb_array_elements(tags) AS tag FROM json_demo")
(RawSql.fromString
(Marshall.annotateSqlMarshallerEmptyAnnotation marshaller)$ print readEntities
liftIO
Program output and test
This concludes this tutorial. You can build an execute this as usual:
stack build
stack exec using-json
And you should see the following output:
Just (Foo {fooId = 0, fooTags = Array [Number 1.0,Number 2.0,Number 3.0]}) [(0,Number 1.0),(0,Number 2.0),(0,Number 3.0)]