pg-entity-0.0.4.1: docs/book/print.html
<!DOCTYPE HTML>
<html lang="en" class="sidebar-visible no-js light">
<head>
<!-- Book generated using mdBook -->
<meta charset="UTF-8">
<title>PG-Entity Book</title>
<meta name="robots" content="noindex" />
<!-- Custom HTML head -->
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#ffffff" />
<link rel="icon" href="favicon.svg">
<link rel="shortcut icon" href="favicon.png">
<link rel="stylesheet" href="css/variables.css">
<link rel="stylesheet" href="css/general.css">
<link rel="stylesheet" href="css/chrome.css">
<link rel="stylesheet" href="css/print.css" media="print">
<!-- Fonts -->
<link rel="stylesheet" href="FontAwesome/css/font-awesome.css">
<link rel="stylesheet" href="fonts/fonts.css">
<!-- Highlight.js Stylesheets -->
<link rel="stylesheet" href="highlight.css">
<link rel="stylesheet" href="tomorrow-night.css">
<link rel="stylesheet" href="ayu-highlight.css">
<!-- Custom theme stylesheets -->
</head>
<body>
<!-- Provide site root to javascript -->
<script>
var path_to_root = "";
var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "navy" : "light";
</script>
<!-- Work around some values being stored in localStorage wrapped in quotes -->
<script>
try {
var theme = localStorage.getItem('mdbook-theme');
var sidebar = localStorage.getItem('mdbook-sidebar');
if (theme.startsWith('"') && theme.endsWith('"')) {
localStorage.setItem('mdbook-theme', theme.slice(1, theme.length - 1));
}
if (sidebar.startsWith('"') && sidebar.endsWith('"')) {
localStorage.setItem('mdbook-sidebar', sidebar.slice(1, sidebar.length - 1));
}
} catch (e) { }
</script>
<!-- Set the theme before any content is loaded, prevents flash -->
<script>
var theme;
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
if (theme === null || theme === undefined) { theme = default_theme; }
var html = document.querySelector('html');
html.classList.remove('no-js')
html.classList.remove('light')
html.classList.add(theme);
html.classList.add('js');
</script>
<!-- Hide / unhide sidebar before it is displayed -->
<script>
var html = document.querySelector('html');
var sidebar = 'hidden';
if (document.body.clientWidth >= 1080) {
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
sidebar = sidebar || 'visible';
}
html.classList.remove('sidebar-visible');
html.classList.add("sidebar-" + sidebar);
</script>
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
<div class="sidebar-scrollbox">
<ol class="chapter"><li class="chapter-item expanded affix "><a href="Introduction.html">Introduction</a></li><li class="chapter-item expanded affix "><a href="Tutorial.html">Tutorial</a></li><li class="chapter-item expanded affix "><li class="part-title">Guides</li><li class="chapter-item expanded "><a href="ErrorsGuide.html"><strong aria-hidden="true">1.</strong> Errors handling</a></li></ol>
</div>
<div id="sidebar-resize-handle" class="sidebar-resize-handle"></div>
</nav>
<div id="page-wrapper" class="page-wrapper">
<div class="page">
<div id="menu-bar-hover-placeholder"></div>
<div id="menu-bar" class="menu-bar sticky bordered">
<div class="left-buttons">
<button id="sidebar-toggle" class="icon-button" type="button" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
<i class="fa fa-bars"></i>
</button>
<button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
<i class="fa fa-paint-brush"></i>
</button>
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
<li role="none"><button role="menuitem" class="theme" id="light">Light</button></li>
<li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
<li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
<li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
<li role="none"><button role="menuitem" class="theme" id="ayu">Ayu</button></li>
</ul>
<button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
<i class="fa fa-search"></i>
</button>
</div>
<h1 class="menu-title">PG-Entity Book</h1>
<div class="right-buttons">
<a href="print.html" title="Print this book" aria-label="Print this book">
<i id="print-button" class="fa fa-print"></i>
</a>
<a href="https://github.com/tchoutri/pg-entity/tree/main/docs" title="Git repository" aria-label="Git repository">
<i id="git-repository-button" class="fa fa-github"></i>
</a>
</div>
</div>
<div id="search-wrapper" class="hidden">
<form id="searchbar-outer" class="searchbar-outer">
<input type="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
</form>
<div id="searchresults-outer" class="searchresults-outer hidden">
<div id="searchresults-header" class="searchresults-header"></div>
<ul id="searchresults">
</ul>
</div>
</div>
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
<script>
document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
link.setAttribute('tabIndex', sidebar === 'visible' ? 0 : -1);
});
</script>
<div id="content" class="content">
<main>
<p>Welcome to <a href="https://github.com/tchoutri/pg-entity"><code>pg-entity</code></a>'s documentation.</p>
<p>Read the Tutorial to learn how to use the library.</p>
<p>Read the Guides to learn more about specific usecases and patterns.</p>
<p>See also these resources to understand the library's underlying mechanisms:</p>
<ul>
<li>
<p><a href="https://hackage.haskell.org/package/postgresql-simple/docs/Database-PostgreSQL-Simple.html">postgresql-simple</a>: The library that provies the Query primitives and the database connection</p>
</li>
<li>
<p><a href="https://hackage.haskell.org/package/pg-transact/docs/Database-PostgreSQL-Transact.html">pg-transact</a>: The library that provides the transaction monad transformer in which our queries run</p>
</li>
<li>
<p>Oliver Charles' <em>24 Days of GHC Extensions</em> series if you want to understand the underlying derivation mechanisms that enable us to write less code:</p>
<ul>
<li><a href="https://ocharles.org.uk/guest-posts/2014-12-15-deriving.html">Deriving</a></li>
<li><a href="https://ocharles.org.uk/posts/2014-12-16-derive-generic.html">DeriveGeneric</a></li>
</ul>
</li>
<li>
<p>Richard Eisenberg's <a href="https://www.youtube.com/watch?v=UZaQuSIrO6s"><em>Avoid boilerplate instances with -XDerivingVia</em></a> is most helpful to understand how the <code>GenericEntity</code> mechanism</p>
<p>works, as well as a showcase for other deriving strategies.</p>
</li>
</ul>
<div style="break-before: page; page-break-before: always;"></div><h2 id="pg-entity-tutorial"><a class="header" href="#pg-entity-tutorial">pg-entity tutorial</a></h2>
<p>In this tutorial, you will learn how to implement the Entity typeclass for your business logic data-types, and run
queries against the database.</p>
<h3 id="setting-up"><a class="header" href="#setting-up">Setting up</a></h3>
<h4 id="language-extensions"><a class="header" href="#language-extensions">Language Extensions</a></h4>
<p>* <code>OverloadedLists</code> allow us to use the <code>[list]</code> syntax for datatypes other than List, like Vector.
* <code>QuasiQuotes</code> enable us to write plain SQL and field names in a <code>[|quasi-quoter block|]</code>.
* The Deriving extensions give us more powerful typeclass derivation.</p>
<pre><code class="language-haskell">{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
module Tutorial where
</code></pre>
<p>Then, let's import some data-types and modules:</p>
<pre><code class="language-haskell">import Data.Pool (Pool)
import Data.Text (Text)
import Data.Time (UTCTime)
import Data.UUID (UUID)
import Database.PostgreSQL.Simple
import Database.PostgreSQL.Simple.FromField
import Database.PostgreSQL.Simple.ToField
import Database.PostgreSQL.Transact (DBT)
import GHC.Generics
import Control.Monad.Error
import qualified Data.Time as Time
import qualified Data.UUID.V4 as UUID
import Database.PostgreSQL.Entity
import Database.PostgreSQL.Entity.DBT
import Database.PostgreSQL.Entity.Types
</code></pre>
<p>And let's write down our initial data models for a blog. <code>Author</code>, and <code>BlogPost</code>.</p>
<p>It is good practice to wrap your primary key in a newtype to gain more
type-safety, but without losing access to typeclass instances of the
underlying type, with <code>deriving newtype</code>.</p>
<pre><code class="language-haskell">newtype AuthorId = AuthorId {getAuthorId :: UUID}
deriving newtype (Eq, Show, FromField, ToField)
data Author = Author
{ authorId :: AuthorId
-- ^ Primary key
, name :: Text
, createdAt :: UTCTime
}
deriving stock (Eq, Generic, Show)
deriving anyclass (FromRow, ToRow)
</code></pre>
<p>and</p>
<pre><code class="language-haskell">newtype BlogPostId = BlogPostId {getBlogPostId :: UUID}
deriving newtype (Eq, FromField, Show, ToField)
data BlogPost = BlogPost
{ blogPostId :: BlogPostId
-- ^ Primary key
, authorId :: AuthorId
-- ^ Foreign key
, title :: Text
, content :: Text
, createdAt :: UTCTime
}
deriving stock (Eq, Generic, Show)
deriving anyclass (FromRow, ToRow)
</code></pre>
<p>Let us write the <code>Entity</code> instances now:</p>
<pre><code class="language-haskell">instance Entity Author where
tableName = "authors"
primaryKey = [field| author_id |]
fields =
[ [field| author_id |]
, [field| name |]
, [field| created_at |]
]
</code></pre>
<p>The above instance declaration reads as:</p>
<blockquote>
<p>My table's name is <em>authors</em>, its primary key is <em>author_id</em>, and the fields are <em>author_id</em>, <em>name</em>, and <em>created_at</em>.</p>
</blockquote>
<p>The order matters for the declarations, so make sure that each field is at the correct position.</p>
<p>Let's do the same for <code>BlogPost</code>:</p>
<pre><code class="language-haskell">instance Entity BlogPost where
tableName = "blogposts"
primaryKey = [field| blogpost_id |]
fields =
[ [field| blogpost_id |]
, [field| author_id |]
, [field| title |]
, [field| content |]
, [field| created_at |]
]
</code></pre>
<p>And these instances will give you access to the Entity functions to query your tables</p>
<h3 id="using-generics"><a class="header" href="#using-generics">Using Generics</a></h3>
<p>But all these manual instances are a tad tedious. Fortunately, there is a derivation mechanism available that allows you
to automate the generation of the <code>Entity</code> instance. This mechanism is called <code>DerivingVia</code>.
It allows you to use a wrapper, called <code>GenericEntity</code>, and a list of options at the type-level to generate the instance</p>
<pre><code class="language-haskell">deriving via
(GenericEntity
'[ TableName "authors"
, PrimaryKey "author_id"
] Author
) instance Entity Author
</code></pre>
<p>Those two options, <code>TableName</code> and <code>PrimaryKey</code> are optional, and the library will adopt the following defaults:</p>
<ul>
<li>
<p><code>tableName</code> will be the snake_case version of the record's name. No pluralisation will be done.</p>
</li>
<li>
<p><code>primaryKey</code> will be the snake_case version of the first field of the record.</p>
</li>
<li>
<p><code>field</code> names will be the snake_case version of the record fields.</p>
</li>
</ul>
<p>The advantages brought by this technique are that the error surface is reduced when implementing the <code>Entity</code> typeclass.</p>
<p>⚠️ While all these deriving parameters are optional, I advise you to write down the table name (with the <code>TableName</code> option).
In the PostgreSQL world, it is customary to pluralise the name of the table containing your data, and
<code>pg-entity</code> does not automatically do this.</p>
<p>⚠️ For performance reasons, you are advised to write the primary key in the options. If you do not, the generic deriving mechanism will have to do a linear
search in the fields list (of complexity 𝛰(n) whereas the lookup will take 𝛰(1) if the primary key is given in your code. Do this especially if you have
long tables.</p>
<p>Do it if you have long tables.</p>
<h3 id="writing-the-sql-migrations"><a class="header" href="#writing-the-sql-migrations">Writing the SQL migrations</a></h3>
<p>In a separate file, we will translate our Haskell data models into SQL migrations.</p>
<pre><code class="language-sql">create table authors (
author_id uuid primary key,
name text not null,
created_at timestamptz not null
);
create table blogposts (
blogpost_id uuid primary key,
author_id uuid not null,
title text not null,
content text not null,
created_at timestamptz not null,
constraint fk_author
foreign key(author_id)
references authors(author_id)
);
</code></pre>
<h3 id="making-queries"><a class="header" href="#making-queries">Making queries</a></h3>
<p>By implementing the <code>Entity</code> Typeclass, your data-type has access to a variety of functions, combinators and helpers that serve the one true purpose of
this library:</p>
<blockquote>
<p>Provide a safe mechanism to expand the fields of a table while writing a query.</p>
</blockquote>
<p>Let us define our insertion function for the Author model:</p>
<pre><code class="language-haskell">insertAuthor :: Author -> DBT IO ()
insertAuthor = insert @Author
</code></pre>
<p>The result of this function, which we call a “DBT action”, is then passed to <a href="https://hackage.haskell.org/package/pg-entity-0.0.1.0/docs/Database-PostgreSQL-Entity-DBT.html#v:withPool"><code>withPool</code></a>.</p>
<p>You can then build a higher-level API endpoint or route controller like that:</p>
<pre><code class="language-haskell">data AuthorInfo = AuthorInfo
{ name :: Text
}
deriving stock (Eq, Show)
mkAuthor :: MonadIO m => AuthorInfo -> m Author
mkAuthor AuthorInfo{name = authorName} = do
authorId <- liftIO $ AuthorId <$> UUID.nextRandom
createdAt <- liftIO Time.getCurrentTime
pure Author{name = authorName, authorId, createdAt}
addAuthor
:: (MonadIO m)
=> Pool Connection
-> AuthorInfo
-> m ()
addAuthor pool info = do
newAuthor <- mkAuthor info
withPool pool $ insertAuthor newAuthor
</code></pre>
<p>And if you want to later select an <code>Author</code> based on its <code>AuthorId</code>:</p>
<pre><code class="language-haskell">getAuthor
:: (MonadIO m)
=> Pool Connection
-> AuthorId
-> m (Maybe Author)
getAuthor pool authorId =
withPool pool $ selectOneByField [field| author_id |] (Only authorId)
</code></pre>
<p>This is the end of this tutorial. There are many more functions to discover that will help you write your queries.
While you shouldn't have to explore the source code to gain understanding of how to use the library, feel free to
navigate it to see how the underlying mechanisms work. :)</p>
<p>Next, you can consult our Guides section to see how to do error handling.</p>
<div style="break-before: page; page-break-before: always;"></div><pre><code class="language-haskell">module ErrorsGuide where
import Control.Monad.Except (MonadError (..))
import Control.Monad.Trans
import Data.Text (Text)
import Database.PostgreSQL.Entity (Entity, selectById)
import Database.PostgreSQL.Entity.Types (GenericEntity)
import Database.PostgreSQL.Simple
import Database.PostgreSQL.Transact (DBT)
import GHC.Generics (Generic)
</code></pre>
<h2 id="error-handling"><a class="header" href="#error-handling">Error Handling</a></h2>
<p>Error handling is a tricky subject, and most often you will have to provide translation layers between your different components to express how a request
has failed and what is the relevant information to be reported.</p>
<p>For example, inserting twice the same entity with the same primary key will raise an error in the database engine that you have violated the uniqueness
constraint of a primary key. This is of little use for consumers of the system, who simply need to be told that their chosen email address or username is already used.</p>
<p>Building a top-down error datatype can be a very good or very bad idea, and should sometimes be replaced with a more extensible mechanism like Haskell
Exceptions (whose datatypes can be used outside of this mechanism, fortunately).</p>
<p>Let us consider a simple usecase, where we wish to express the following error modes:</p>
<ul>
<li>
<p>Entity was not found</p>
</li>
<li>
<p>Entity is in a Bad State™</p>
</li>
<li>
<p>Entity processing is running</p>
</li>
</ul>
<pre><code class="language-haskell">data EntityError
= EntityNotFound
| EntityBadState
| EntityProcessingIsRunning
deriving (Eq, Show)
</code></pre>
<p>A <a href="https://hackage.haskell.org/package/mtl-2.2.2/docs/Control-Monad-Except.html#t:MonadError"><code>MonadError</code></a> stack can be used to handle errors with your data-type.</p>
<p>Here are the functions that we will be using:</p>
<p>This function allows great control over the way we report errors,
and allows us to plug a <code>MonadError</code> for reporting.</p>
<pre><code class="language-haskell">withPool :: (MonadBaseControl IO m)
=> Pool Connection
-> DBT m a
-> m a
</code></pre>
<p>Those two functions show that we do not have to put a <code>MonadError</code> everywhere, and if a lower-level error happens, we can let it bubble up to create a
higher-level error (like status code 500 in an http server).</p>
<pre><code class="language-haskell">data E = E
{ eId :: Text
}
deriving stock (Eq, Show, Generic)
deriving
(Entity)
via (GenericEntity '[]) E
deriving anyclass (FromRow)
</code></pre>
<pre><code class="language-haskell">insertEntity :: (MonadIO m)
=> E
-> DBT m ()
</code></pre>
<pre><code class="language-haskell">getEntity
:: (MonadError EntityError m, MonadIO m)
=> Int
-> DBT m E
getEntity key = do
result <- selectById (Only key)
case result of
Just e -> pure e
Nothing -> lift $ throwError EntityNotFound
</code></pre>
<p>↑ Here, we convert a valid database response into a more precise
business logic component.</p>
</main>
<nav class="nav-wrapper" aria-label="Page navigation">
<!-- Mobile navigation buttons -->
<div style="clear: both"></div>
</nav>
</div>
</div>
<nav class="nav-wide-wrapper" aria-label="Page navigation">
</nav>
</div>
<!-- Livereload script (if served using the cli tool) -->
<script>
const wsProtocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsAddress = wsProtocol + "//" + location.host + "/" + "__livereload";
const socket = new WebSocket(wsAddress);
socket.onmessage = function (event) {
if (event.data === "reload") {
socket.close();
location.reload();
}
};
window.onbeforeunload = function() {
socket.close();
}
</script>
<script>
window.playground_copyable = true;
</script>
<script src="elasticlunr.min.js"></script>
<script src="mark.min.js"></script>
<script src="searcher.js"></script>
<script src="clipboard.min.js"></script>
<script src="highlight.js"></script>
<script src="book.js"></script>
<!-- Custom JS scripts -->
<script>
window.addEventListener('load', function() {
window.setTimeout(window.print, 100);
});
</script>
</body>
</html>