Typer des requêtes SQL (Haskell)
Voir aussi : video youtube - code source
Cet article présente deux bibliothèques Haskell pour manipuler des bases de données, l’une via des requêtes SQL classiques, l’autre via des requêtes typées. Le système de types de Haskell aide à écrire des requêtes valides et sécurisées.
Exemple de base de données
Soit une base de données sqlite de films, avec une table décrivant les réalisateurs et une autre les films.
CREATE TABLE director (
INTEGER PRIMARY KEY AUTOINCREMENT,
dir_id NOT NULL,
dir_firstname TEXT NOT NULL
dir_lastname TEXT
);
CREATE TABLE movie (
INTEGER PRIMARY KEY AUTOINCREMENT,
mov_id INTEGER NOT NULL,
mov_dir NOT NULL,
mov_title TEXT INTEGER,
mov_year FOREIGN KEY(mov_dir) REFERENCES director(dir_id)
);
INSERT INTO director VALUES(1, 'Ted', 'Kotcheff');
INSERT INTO director VALUES(2, 'Stanley', 'Kubrick');
INSERT INTO movie VALUES(1, 1, 'First Blood', 1982);
INSERT INTO movie VALUES(2, 2, 'Paths of Glory', 1957);
INSERT INTO movie VALUES(3, 2, 'Full Metal Jacket', NULL);
On génère la base de données (movies.db
) avec la commande :
sqlite3 movies.db < movies.sql
Client mid-level (sqlite-simple)
La bibliothèque sqlite-simple est de type “mid-level”. Elle apporte les fonctionnalités nécessaires pour accéder à la base de données et écrire des requêtes à peu près sécurisées mais nécessite d’écrire du SQL.
Requête basique
Par exemple, pour récupérer le prénom et le nom des réalisateurs, on écrit la requête SQL correspondante et on l’exécute avec query_
. La bibliothèque sqlite-simple
vérifie que les données retournées par la base de données correspondent bien au type indiqué (ici [(Text, Text)]
).
selectDirectors :: Connection -> IO [(Text, Text)]
= query_ conn
selectDirectors conn "SELECT dir_firstname, dir_lastname FROM director"
On peut alors appeler notre fonction, lors d’une connection à une base de données :
main :: IO ()
= withConnection "movies.db" $ \conn -> do
main
putStrLn "\n**** selectDirectors ****"
>>= mapM_ print selectDirectors conn
Résultat :
**** selectDirectors ****
("Ted","Kotcheff")
("Stanley","Kubrick")
Typage
Dans l’exemple précédent, on a récupéré les lignes résultats avec un type pré-existant ([(Text, Text)]
) mais on peut aussi définir nos propres types. Par exemple, si on veut récupérer toutes les données d’un film à partir du prénom/nom du réalisateur, on peut définir un type Production
et instancier la classe FromRow
pour récupérer les champs nécessaires :
data Production = Production
dir_firstname :: Text
{ dir_lastname :: Text
, mov_title :: Text
, mov_year :: Maybe Int
,deriving (Generic, Show)
}
instance FromRow Production where
= Production <$> field <*> field <*> field <*> field fromRow
On peut ensuite récupérer ces données depuis la base de données : on écrit la jointure dans la requête SQL et on précise le type des résultats ([Production]
).
selectProductions :: (Text, Text) -> Connection -> IO [Production]
= query conn
selectProductions director conn "SELECT dir_firstname, dir_lastname, mov_title, mov_year \
\FROM director \
\INNER JOIN movie ON mov_dir = dir_id \
\WHERE dir_firstname = ? AND dir_lastname = ?"
director
En plus du type de retour, les types des paramètres de la requête sont également vérifiés, ce qui permet d’empêcher certaines attaques. Cependant, la structure même de la base de données (table, champs…) n’est vérifiée qu’à l’exécution.
Client type-safe (selda)
Selda est un EDSL permettant de manipuler des bases de données relationnelles. Il permet notamment de typer les tables et les requêtes, ce qui permet au compilateur de vérifier leur cohérence.
Requête basique
Avec selda
, on définit des types pour représenter les tables, un peu à la manière des ORM en Programmation Orientée Objet.
Par exemple pour les réalisateurs, on écrit le type Director
suivant, on instancie SqlRow
pour pouvoir récupérer des lignes résultats de ce type, et on construit une valeur correspondant à la table “director” dans la base de données.
data Director = Director
dir_id :: ID Director
{ dir_firstname :: Text
, dir_lastname :: Text
,deriving (Generic, Show)
}
instance SqlRow Director
directoryTable :: Table Director
= table "director" [#dir_id :- autoPrimary] directoryTable
Pour interroger la base de données, on utilise l’EDSL, qui redéfinit les fonctionnalités de SQL :
selectDirectors :: SeldaT SQLite IO [Director]
= query $ select directoryTable selectDirectors
Enfin, on exécute la requête lors d’une connexion à la base de donnés :
main :: IO ()
= withSQLite "movies.db" $ do
main
$ putStrLn "\n**** selectDirectors ****"
liftIO >>= liftIO . mapM_ print selectDirectors
Résultat :
**** selectDirectors ****
Director {dir_id = 1, dir_firstname = "Ted", dir_lastname = "Kotcheff"}
Director {dir_id = 2, dir_firstname = "Stanley", dir_lastname = "Kubrick"}
Typage
L’EDSL de selda
permet de profiter du système de typage de Haskell. Par exemple, les clés des tables sont des types à part entière, si bien que le compilateur peut vérifier les clés étrangères.
data Movie = Movie
mov_id:: ID Movie
{ mov_dir :: ID Director
, mov_title :: Text
, mov_year :: Maybe Int
,deriving (Generic, Show)
}
instance SqlRow Movie
movieTable :: Table Movie
= table "movie"
movieTable #mov_id :- autoPrimary
[ #mov_dir :- foreignKey directoryTable #dir_id ] ,
Pour les requêtes plus compliquées comme les jointures, selda
permet, via un “contexte de requête”, d’écrire des sélections/restrictions assez facilement. Par exemple, pour les productions d’un réalisateur donné :
selectProductions :: (Text, Text) -> SeldaT SQLite IO [Director :*: Movie]
= query $ do
selectProductions (firstname, lastname) <- select directoryTable
directors <- select movieTable
movies ! #mov_dir .== directors ! #dir_id)
restrict (movies ! #dir_firstname .== literal firstname)
restrict (directors ! #dir_lastname .== literal lastname)
restrict (directors return $ directors :*: movies
A noter que selda
peut aussi générer la base de données à partir des types définis via l’EDSL, donc sans avoir à écrire du code SQL pour initialiser la base.
Conclusion
Haskell a des bibliothèques pour accéder à différents systèmes de bases de données (MySQL, Postgres, SQLite, etc). Ces bibliothèques peuvent être classées en deux catégories : basées sur du SQL ou basées sur un DSL.
Les bibliothèques basées sur du SQL utilisent des requêtes SQL classiques, que l’on peut paramétrer. Comme le type des paramètres est spécifié, les données de/vers la base sont vérifiées, ce qui empêche certaines erreurs et attaques. Cependant ces vérifications ne sont réalisées qu’à l’exécution. De même, la validité des requêtes par rapport à la structure de la base n’est vérifiée qu’à l’exécution.
Les bibliothèques basées sur un DSL permettent de définir toute la structure de la base ainsi que les requêtes, via du code Haskell. Ceci permet de faire la plupart des vérifications dès l’étape de compilation.
Voir aussi : Which type-safe database library should you use?