Connexions à une base de données Shiny : DBI, le package pool et un SQL sans injection

Lisez et écrivez dans une vraie base de données depuis Shiny avec DBI, protégez vos requêtes contre l’injection, et utilisez le package pool pour que les sessions concurrentes n’épuisent pas vos connexions.

Programming
Shiny

Connectez une application Shiny à une vraie base de données avec DBI et le package pool. Ouvrez une connexion, exécutez du SQL sans injection avec des paramètres et sqlInterpolate, gérez les connexions pour que les sessions concurrentes ne fuient jamais ni ne les épuisent, encapsulez les écritures dans une transaction, et remplacez l’exemple RSQLite par Postgres ou MariaDB avec la même API.

Date de publication

16 juillet 2026

Modifié

16 juillet 2026

AstucePoints clés
  • DBI est l’unique interface ; le pilote se branche en dessous. dbConnect(), dbGetQuery() et dbWriteTable() sont les mêmes verbes que vous soyez sur SQLite, Postgres ou MariaDB — vous ne changez que le pilote.
  • Ne collez jamais une saisie utilisateur dans une requête. Construisez du SQL paramétré avec des marqueurs params = ou sqlInterpolate() — c’est ce qui empêche une entrée Shiny de se transformer en injection SQL.
  • Le package pool est la réponse pour la production. Une nouvelle connexion par reactive est lente ; une seule connexion partagée casse sous des utilisateurs concurrents. dbPool() gère un ensemble de connexions et les distribue en toute sécurité — avec les mêmes verbes DBI.
  • Branchez le pool une seule fois, et nettoyez-le. Créez le pool au démarrage de l’application et enregistrez onStop(function() poolClose(pool)) pour que les connexions soient libérées à l’arrêt de l’application.
  • The runtime is the judge. Chaque requête de cette page s’exécute contre une vraie base de données SQLite et renvoie les lignes affichées — copiez n’importe quel bloc et reproduisez-les vous-même.

Introduction

Votre application Shiny lit un CSV. Cela fonctionnait quand le fichier était petit — mais il compte désormais deux millions de lignes et se charge lentement, ou plusieurs personnes doivent l’éditer en restant synchronisées, ou les données vivent dans un système que votre application ne possède pas. C’est le moment où un fichier plat ne suffit plus et où il vous faut une base de données.

Le correctif naïf aggrave les choses. Si vous ouvrez une nouvelle connexion à l’intérieur de chaque reactive, chaque requête paie le coût complet de connexion-puis-fermeture et votre application rame. Si, à l’inverse, vous ouvrez une seule connexion au démarrage et la partagez, une poignée d’utilisateurs concurrents se marchent dessus sur le même handle — et une connexion qui meurt emporte toute l’application avec elle. Les deux modes de défaillance sont réels, et tous deux ont le même remède : connectez-vous via DBI (la DataBase Interface de R — l’API commune qu’implémente chaque pilote de base de données R) et gérez les connexions avec le package pool.

Cette leçon utilise RSQLite pour l’exemple exécutable car il ne nécessite aucun serveur — toute la base de données est un fichier (ou vit en mémoire). Tout ce que vous apprenez se transpose sans changement à Postgres ou MariaDB ; seul le pilote change. Nous allons nous connecter, lire et écrire, exécuter du SQL sans injection (la sécurité des requêtes qui compte vraiment en production), mettre les connexions en pool pour la concurrence, et encapsuler les écritures dans une transaction.

Quand vous n’en avez pas besoin. Une petite table de référence en lecture seule — une correspondance de codes pays, une liste d’options figée — se contente très bien d’être embarquée en .rds ou en CSV et lue une fois au démarrage. Recourez à une base de données quand les données sont volumineuses, changent, sont écrites par plus d’un processus, ou doivent survivre à la session de l’application. En deçà de ce seuil, un fichier est l’outil le plus simple et le bon.

NotePrérequis

Il vous faut une application Shiny fonctionnelle où brancher tout cela, et les trois packages installés :

install.packages(c("DBI", "RSQLite", "pool"))

Un peu de SQL (Structured Query Language — le langage avec lequel vous interrogez une base de données) aide : une instruction SELECT col FROM table WHERE condition lit des lignes dans une table. Si vous savez lire cette ligne, vous êtes prêt. Gérer les identifiants et secrets (mots de passe de base de données, chaînes de connexion) et ajouter connexion/authentification sont des sujets à part entière — la leçon sur la sécurité en production les traite, et cette leçon fait délibérément l’impasse dessus.

NoteCopiez n’importe quel bloc et exécutez-le en local

Chaque requête ci-dessous s’exécute contre une vraie base de données SQLite — créée sur disque ou en mémoire — et renvoie les lignes affichées ; rien n’est simulé. Ceci est une page statique, donc le code n’est pas exécuté ici : copiez n’importe quel bloc sur votre machine et exécutez-le localement pour reproduire la sortie. Quand dbGetQuery() renvoie les mêmes décomptes que vous voyez ici, et que poolClose() libère proprement les connexions, vous avez prouvé que le motif fonctionne de bout en bout. The runtime is the judge.

Se connecter, écrire et lire

Une session de base de données commence avec dbConnect() : vous passez un pilote (quelle base de données) et les détails de connexion (quelle instance de base de données). Pour RSQLite, l’« instance » est simplement un chemin de fichier — ou ":memory:" pour une base de données jetable qui ne vit que le temps de votre session R :

library(DBI)

con <- dbConnect(RSQLite::SQLite(), "my-app.sqlite")   # a file on disk
# con <- dbConnect(RSQLite::SQLite(), ":memory:")       # or in-memory, for a demo

RSQLite::SQLite() est le pilote ; DBI est l’interface qu’il implémente. Cette séparation est tout l’intérêt — le code que vous écrivez ensuite est du DBI, donc il se moque que le pilote soit justement SQLite.

Placez des données dans la base avec dbWriteTable(), puis vérifiez qu’elles y sont arrivées avec dbListTables() :

dbWriteTable(con, "mtcars", mtcars)   # write the built-in mtcars as a table

dbListTables(con)
#> [1] "mtcars"

Maintenant, interrogez-la. dbGetQuery() envoie un SELECT SQL et renvoie le résultat sous forme de data frame simple — ici, la consommation moyenne en miles par gallon par nombre de cylindres :

dbGetQuery(con, "
  SELECT cyl, COUNT(*) AS n, ROUND(AVG(mpg), 2) AS mean_mpg
  FROM mtcars
  GROUP BY cyl
  ORDER BY cyl
")
#>   cyl  n mean_mpg
#> 1   4 11    26.66
#> 2   6  7    19.74
#> 3   8 14    15.10

La base de données a fait l’agrégation et a renvoyé trois lignes. dbReadTable(con, "mtcars") récupère une table entière sous forme de data frame quand vous voulez tout, et dbGetQuery() sert au SELECT ciblé que vous utiliserez le plus. Quand vous avez terminé, fermez la connexion pour que la base de données la libère :

dbDisconnect(con)

C’est toute la surface CRUD (create, read, update, delete) dont la plupart des applications ont besoin — écrire, lire, interroger — au travers de quatre fonctions DBI. La documentation DBI est la référence pour l’ensemble complet.

Garder les requêtes à l’abri des injections

Voici l’erreur qui transforme une application de données en faille de sécurité. Vous avez une entrée Shiny, et vous construisez une requête en la collant dans une chaîne de caractères :

# ILLUSTRATIVE — never write this. It is unsafe.
query <- paste0("SELECT COUNT(*) AS n FROM mtcars WHERE cyl = ", input$cyl)
dbGetQuery(con, query)

Si input$cyl vaut 6, cela fonctionne par hasard. Mais un utilisateur contrôle cette entrée, et rien ne l’empêche de taper 8; DROP TABLE mtcars. Votre chaîne devient deux instructions, et la seconde supprime votre table. C’est l’injection SQL, et coller une saisie utilisateur dans une chaîne est très exactement comme cela qu’elle survient. (L’exemple ci-dessus est illustratif — ne l’exécutez pas contre une table à laquelle vous tenez.)

La règle est simple : une saisie utilisateur n’entre jamais dans une requête par concaténation de chaînes. DBI vous donne deux idiomes sûrs. Le premier, les marqueurs de position — un ? dans le SQL, les vraies valeurs étant passées séparément à params, si bien que le pilote les traite comme des données, jamais comme du SQL :

dbGetQuery(con, "SELECT COUNT(*) AS n FROM mtcars WHERE cyl = ?", params = list(6))
#>   n
#> 1 7

Le second est sqlInterpolate(), qui construit la chaîne de requête pour vous en toute sécurité, en citant et en échappant chaque valeur selon les règles de la connexion :

sql <- sqlInterpolate(con, "SELECT COUNT(*) AS n FROM mtcars WHERE cyl = ?cyl", cyl = 8)
sql
#> <SQL> SELECT COUNT(*) AS n FROM mtcars WHERE cyl = 8

dbGetQuery(con, sql)
#>    n
#> 1 14

Les deux sont sûrs parce que la valeur est prise en charge par le pilote, non insérée dans du texte. Dans une application Shiny, vous passeriez input$cyl là où figure le 6 ou le 8 ci-dessus — c’est le mécanisme qui la sécurise, quoi que l’utilisateur tape. Les requêtes sans injection sont la moitié « écriture de requêtes » de la sécurité des bases de données ; la moitié « gestion des identifiants et authentification » se trouve dans la leçon sur la sécurité en production.

Mettre les connexions en pool pour les sessions concurrentes

Maintenant, le problème de production. Une application Shiny sert de nombreux utilisateurs à la fois, et les reactives se déclenchent constamment. Vous avez deux options tentantes mais fausses : ouvrir un nouveau dbConnect() dans chaque reactive (correct, mais lent — se connecter coûte cher et vous le faites à chaque frappe), ou ouvrir une seule connexion au démarrage et la partager (rapide, mais une connexion unique ne peut pas servir en toute sécurité des sessions concurrentes, et si elle tombe votre application est morte).

Le package pool résout le dilemme. dbPool() crée un ensemble géré de connexions : il les ouvre paresseusement, en distribue une pour chaque requête, la reprend et la réutilise — si bien qu’une requête est rapide (pas de nouvelle connexion) et que les sessions concurrentes obtiennent chacune leur propre connexion active. Surtout, vous utilisez exactement les mêmes verbes DBI sur l’objet pool que ceux que vous utilisiez sur une connexion :

library(pool)

pool <- dbPool(RSQLite::SQLite(), dbname = ":memory:")

dbWriteTable(pool, "iris", iris)                       # same verb, pool object
dbGetQuery(pool, "SELECT Species, COUNT(*) AS n FROM iris GROUP BY Species")
#>      Species  n
#> 1     setosa 50
#> 2 versicolor 50
#> 3  virginica 50

poolClose(pool)                                        # release every connection

dbGetQuery(), dbWriteTable(), dbExecute() — tous fonctionnent sur pool exactement comme sur con. C’est le principe de conception : faire passer une application qui fonctionne d’une connexion brute à un pool est souvent une modification d’une seule ligne. La documentation du package pool est la référence.

Le brancher dans une application Shiny. Créez le pool une seule fois au démarrage de l’application — en haut de app.R, ou dans global.R (voir organisation du code pour l’endroit où placer le code de démarrage) — pour que toutes les sessions le partagent. Puis enregistrez onStop() pour que le pool soit fermé à l’arrêt de l’application, libérant ses connexions :

library(shiny)
library(pool)

pool <- dbPool(RSQLite::SQLite(), dbname = "my-app.sqlite")
onStop(function() poolClose(pool))          # clean up when the app stops

server <- function(input, output, session) {
  output$by_cyl <- renderTable({
    dbGetQuery(pool, "SELECT cyl, COUNT(*) AS n FROM mtcars GROUP BY cyl")
  })
}

Créez une fois, partagez entre les sessions, fermez à l’arrêt. Oublier le nettoyage onStop() est la fuite classique — l’application se termine mais pas les connexions, et elles s’accumulent sur le serveur de base de données.

Encapsuler les écritures liées dans une transaction

Certaines écritures n’ont de sens qu’ensemble. Enregistrer un paiement, c’est insérer une ligne dans accounts et une ligne dans ledger — si la première réussit et la seconde échoue, vos données sont désormais fausses. Une transaction rend le groupe atomique : soit toutes les instructions sont validées ensemble, soit aucune. Avec pool, poolWithTransaction() exécute un bloc comme une seule transaction et annule le tout si quoi que ce soit à l’intérieur produit une erreur :

poolWithTransaction(pool, function(conn) {
  dbExecute(conn, "INSERT INTO accounts (id, balance) VALUES (1, 100)")
  dbExecute(conn, "INSERT INTO ledger (account_id, delta) VALUES (1, 100)")
})

Si le second dbExecute() échoue, la première insertion est annulée — vous ne vous retrouvez jamais avec une modification à moitié écrite. Recourez à une transaction dès que deux écritures ou plus doivent réussir ou échouer comme un tout.

Utiliser une autre base de données

Tout ce qui précède était en RSQLite, mais l’interface DBI est identique d’une base de données à l’autre — vous changez le pilote dans dbConnect() (ou dbPool()) et le reste de votre code reste intact :

Base de données Package du pilote Appel du pilote
SQLite (fichier / en mémoire) RSQLite RSQLite::SQLite()
PostgreSQL RPostgres RPostgres::Postgres()
MySQL / MariaDB RMariaDB RMariaDB::MariaDB()
SQL Server et autres odbc odbc::odbc()

Pour une base de données serveur, vous passez des détails de connexion au lieu d’un chemin de fichier — host, port, dbname, user, password — et ces secrets proviennent de variables d’environnement, non de votre code source (là encore, la leçon sur la sécurité en production). Le code de dbGetQuery(), dbWriteTable(), du pooling et des transactions reste exactement tel que vous l’avez écrit. C’est le bénéfice de coder contre DBI : prototypez sur SQLite, déployez sur Postgres, changez une ligne. Le guide bases de données avec R de Posit est une bonne référence pour choisir et configurer une base de données de production.

Problèmes fréquents

Connexions fuitées — vous avez oublié de les fermer. Votre application tourne un moment, puis la base de données refuse les nouvelles connexions (« too many clients »). La cause est un pool ou une connexion ouverte mais jamais libérée. Associez toujours dbPool() à onStop(function() poolClose(pool)) dans une application Shiny, et un simple dbConnect() à un dbDisconnect() correspondant. Les connexions sont une ressource serveur limitée ; ouvrez-les, utilisez-les, rendez-les.

Une saisie utilisateur a cassé (ou attaqué) votre requête. Vous avez construit du SQL avec paste0() et une entrée Shiny, et soit cela a produit une erreur sur une valeur inattendue, soit — pire — cela a exécuté quelque chose qui n’aurait pas dû l’être. Ne concaténez jamais une entrée dans du SQL. Utilisez des marqueurs params = list(...) ou sqlInterpolate() pour que la valeur soit traitée comme des données, non comme du texte de requête.

« Database is locked » avec SQLite sous des écritures concurrentes. SQLite n’autorise qu’une seule écriture à la fois, si bien qu’une application chargée avec plusieurs sessions écrivant en même temps rencontre des erreurs de verrou. Un pool ne corrige pas cela — la limite vient de SQLite, pas du pool. Pour une application à dominante de lecture, SQLite convient ; pour des écritures concurrentes, passez à une base de données serveur (Postgres, MariaDB) qui gère de nombreuses écritures simultanées, avec le même code DBI.

Questions fréquentes

Ouvrez une connexion avec DBI : con <- dbConnect(RSQLite::SQLite(), "my-app.sqlite") pour SQLite, ou le pilote correspondant (RPostgres::Postgres(), RMariaDB::MariaDB()) pour une base de données serveur. Interrogez-la ensuite avec dbGetQuery(con, "SELECT ...") et écrivez avec dbWriteTable(). Dans une vraie application, ne vous connectez pas dans chaque reactive — créez un pool une fois au démarrage avec dbPool() et utilisez les mêmes verbes DBI dessus.

Le package pool gère un ensemble de connexions de base de données pour qu’une application Shiny multi-utilisateurs soit à la fois rapide et sûre. Ouvrir une nouvelle connexion dans chaque reactive est lent ; partager une seule connexion casse sous des sessions concurrentes. dbPool() distribue et réutilise les connexions automatiquement, et vous appelez les mêmes verbes dbGetQuery()/dbWriteTable() sur l’objet pool. Créez-le une fois au démarrage de l’application et fermez-le avec onStop(function() poolClose(pool)).

Ne construisez jamais une requête en collant une entrée Shiny dans la chaîne SQL. Utilisez plutôt des paramètres : dbGetQuery(con, "SELECT * FROM t WHERE id = ?", params = list(input$id)), ou sqlInterpolate(con, "... WHERE id = ?id", id = input$id). Les deux passent la valeur au pilote comme donnée, si bien que même une entrée malveillante telle que 1; DROP TABLE t ne peut pas s’exécuter comme du SQL. La concaténation de chaînes avec une saisie utilisateur est la vulnérabilité ; les requêtes paramétrées sont la solution.

SQLite est un fichier unique sans serveur — parfait pour le développement, les démos et les applications à dominante de lecture. Sa limite est qu’un seul processus peut écrire à la fois, si bien qu’une application chargée avec des écritures concurrentes rencontre des erreurs « database is locked ». PostgreSQL (ou MariaDB) tourne comme un serveur conçu pour de nombreuses lectures et écritures simultanées — le bon choix pour une application de production, à dominante d’écriture, multi-utilisateurs. Comme les deux parlent DBI, vous pouvez prototyper sur SQLite et basculer le pilote vers Postgres plus tard sans presque aucune modification de code.

Oui — c’est tout l’avantage. dbConnect(), dbGetQuery(), dbWriteTable(), dbExecute() et les fonctions de pool sont l’interface DBI, et chaque pilote l’implémente. Vous ne changez que l’argument du pilote dans dbConnect()/dbPool() (RSQLite::SQLite()RPostgres::Postgres()) et passez des détails de connexion au lieu d’un chemin de fichier. Le code de requête et de pooling reste identique.

Testez vos connaissances

Vous avez une base de données SQLite en mémoire dans laquelle la table mtcars a été écrite. (1) Créez un pool pour elle. (2) Écrivez une requête qui compte combien de voitures ont un nombre donné de cylindres, où ce nombre de cylindres provient d’une variable chosen_cyl — faite de façon sans injection. (3) Fermez le pool. Supposez chosen_cyl <- 6.

Créez le pool avec dbPool(RSQLite::SQLite(), dbname = ":memory:"), puis dbWriteTable(pool, "mtcars", mtcars). Pour la requête sûre, utilisez un marqueur ? avec params = list(chosen_cyl) — jamais paste0(). Fermez avec poolClose(pool).

library(DBI)
library(pool)

pool <- dbPool(RSQLite::SQLite(), dbname = ":memory:")
dbWriteTable(pool, "mtcars", mtcars)

chosen_cyl <- 6
dbGetQuery(
  pool,
  "SELECT COUNT(*) AS n FROM mtcars WHERE cyl = ?",
  params = list(chosen_cyl)
)
#>   n
#> 1 7

poolClose(pool)

Le marqueur ? avec params = list(chosen_cyl) est ce qui la rend sans injection : la valeur est passée au pilote comme une donnée, donc elle ne peut jamais être interprétée comme du SQL. sqlInterpolate(pool, "... WHERE cyl = ?cyl", cyl = chosen_cyl) serait une alternative tout aussi sûre. Notez que les mêmes verbes dbGetQuery()/dbWriteTable() fonctionnent sur l’objet pool exactement comme sur une connexion brute.

Pourquoi le package pool est-il préféré à l’ouverture d’un dbConnect() à l’intérieur de chaque reactive dans une application Shiny multi-utilisateurs ?

A. Il chiffre automatiquement la connexion à la base de données.
B. Il gère un ensemble réutilisable de connexions — si bien que les requêtes sont rapides (pas de nouvelle connexion à chaque fois) et que les sessions concurrentes obtiennent chacune une connexion active et sûre.
C. Il réécrit votre SQL pour prévenir l’injection.
D. Il vous permet de ne plus fermer les connexions du tout.

B. Ouvrir un dbConnect() dans chaque reactive paie le coût complet de connexion à chaque requête (lent), tandis que partager une seule connexion casse sous des sessions concurrentes. dbPool() gère un ensemble de connexions, en distribuant une par requête et en la réutilisant — rapide et sûr en concurrence. A est faux (pool n’ajoute pas de chiffrement — cela relève de la configuration de votre pilote/connexion) ; C est faux (la sécurité contre l’injection vient des requêtes paramétrées, pas de pool) ; D est faux — vous fermez toujours le pool avec onStop(function() poolClose(pool)).

Conclusion

Connecter une application Shiny à une base de données, c’est quatre verbes DBI — dbConnect(), dbWriteTable(), dbReadTable(), dbGetQuery() — plus deux disciplines qui séparent un prototype d’une application de production. Premièrement, ne laissez jamais une saisie utilisateur atteindre une requête sous forme de texte : utilisez des marqueurs params = ou sqlInterpolate() pour qu’une entrée Shiny ne puisse pas devenir une injection SQL. Deuxièmement, gérez les connexions avec pool : créez un pool au démarrage, utilisez les mêmes verbes DBI dessus, et fermez-le avec onStop() pour que les sessions concurrentes restent rapides et que les connexions ne fuient jamais. Encapsulez les écritures groupées dans poolWithTransaction() quand elles doivent être tout-ou-rien. Et parce que tout est du DBI, l’exemple RSQLite d’ici devient une application Postgres ou MariaDB en changeant une seule ligne de pilote. Les identifiants, les secrets et l’authentification sont la pièce suivante — la leçon sur la sécurité en production prend le relais là.

Leçons connexes

  • Construisez à partir de l’architecture qui l’entoure : Organisation du code Shiny montre où placer le code de démarrage comme votre dbPool() (global.R), et Modules Shiny vous permet de cloisonner la logique de base de données en composants réutilisables et nommés dans leur propre espace de noms. Pour la moitié « gestion des identifiants et authentification » de la sécurité des bases de données — mots de passe, chaînes de connexion, identifiants de connexion — voyez sécurité en production des applications Shiny, que cette leçon relie délibérément plutôt que de la ré-enseigner.
  • Allez plus loin : Applications web Shiny — tout le cursus Shiny. · Science computationnelle et IA — le pilier.
Cette page vous a-t-elle été utile ?

Prouvez que vous savez le faire. Maîtrisez toute la série Architecture Shiny — suivez votre parcours, construisez des projets et obtenez un certificat.

Commencer gratuitement →

Passez à Pro — Prova illimité sur vos propres données et un certificat vérifiable qui atteste la compétence.

dès 15 $/mois facturé annuellement

Passer à Pro →

✓ Vous êtes Pro — continuez. The runtime is the judge.

Recevez les nouvelles leçons R & Python par e-mail

Pratique, reproductible, sans spam. Désinscription à tout moment.

Double opt-in. Nous ne partageons jamais votre e-mail.

Partager cette pageXLinkedInRedditHN

Réutilisation

Citation

BibTeX
@online{2026,
  author = {},
  title = {Connexions à une base de données Shiny : DBI, le package pool
    et un SQL sans injection},
  date = {2026-07-16},
  url = {https://www.datanovia.com/learn/programming/shiny/architecture/database-connections},
  langid = {fr}
}
Veuillez citer ce travail comme suit :
“Connexions à une base de données Shiny : DBI, le package pool et un SQL sans injection.” 2026. July 16. https://www.datanovia.com/learn/programming/shiny/architecture/database-connections.