Utilisation serveur



Connexion globale

Pour utiliser sqlite dans un serveur bottle, il est préférable de créer une connexion globale à la base de données et de l'utiliser dans les différentes routes.

Création de la connexion, création et remplissage de la table ingredients (télécharger le code) :

import sqlite3

# Connexion à la base de données
db_conn = sqlite3.connect('data.db')
db_conn.execute('PRAGMA foreign_keys = ON')

# Préparation de la base de données
db_conn.execute('''
CREATE TABLE IF NOT EXISTS ingredients (
    id INTEGER PRIMARY KEY,
    name TEXT NOT NULL UNIQUE
);''')
try:
    with db_conn:
        db_conn.execute('''
        INSERT INTO ingredients (name)
        VALUES ('Mozzarella'),('Tomate'),('Basilic'),
               ('Jambon'),('Olive'),
                ('Oignon'),('Oeuf');
        ''')
except sqlite3.IntegrityError:
    # Si la table a déjà été remplie, on ne fait rien
    pass

Utilisation de la connexion

La connexion globale est ensuite utilisée dans les différentes routes. Exemple : affichage de la liste des ingrédients (télécharger le code) :

@route('/ingredients')
def ingredients():
    # utilisation de la référence à la connexion globale
    cursor = db_conn.cursor()
    cursor.execute('SELECT * FROM ingredients')
    rows = cursor.fetchall()
    out = "<h1>Liste des ingrédients</h1>"
    out += "<ul>"
    for row in rows:
        out += f"<li>{row[0]} : {row[1]}</li>"
    out += "</ul>"
    return out

En pratique

Modifiez le code ci-dessus pour :

  • utiliser un template pour la page /ingredients
  • en incluant un formulaire pour ajouter un nouvel ingrédient (via la méthode POST vers /ingredients)
  • en incluant un formulaire pour filtrer les ingrédients par nom (via la méthode GET vers /ingredients?q=Tomate par exemple)
  • en affichant la liste des ingrédients dans un tableau HTML
  • en affichant un message d'erreur si l'ingrédient existe déjà (vous pouvez utiliser un try/except pour gérer l'erreur d'intégrité)

Vue ingrédients

Correction

Template de la page /ingredients (télécharger le code) :

  <body>
    <h1>Liste des ingrédients</h1>
    % if error is not None:
    <p class="error">{{ error }}</p>
    % end
    <form action="/ingredients" method="post">
      <input
        type="text"
        name="name"
        placeholder="Nom de l'ingrédient"
        required
      />
      <button type="submit">Ajouter un ingrédient</button>
    </form>
    <form action="/ingredients" method="get">
      <input type="text" name="q" placeholder="Filtrer par nom" value="{{q}}" />
      <button type="submit">Filtrer</button>
    </form>
    <table>
      <thead>
        <tr>
          <th>ID</th>
          <th>Nom</th>
        </tr>
      </thead>
      <tbody>
        % for row in rows:
        <tr>
          <td>{{row[0]}}</td>
          <td>{{row[1]}}</td>
        </tr>
        % end
      </tbody>
    </table>
  </body>

Télécharger le code

from bottle import route, run, request, template
import sqlite3

# Connexion à la base de données
db_conn = sqlite3.connect('data.db')
db_conn.execute('PRAGMA foreign_keys = ON')

# Préparation de la base de données
db_conn.execute('''
CREATE TABLE IF NOT EXISTS ingredients (
    id INTEGER PRIMARY KEY,
    name TEXT NOT NULL UNIQUE
);''')
try:
    with db_conn:
        db_conn.execute('''
        INSERT INTO ingredients (name)
        VALUES ('Mozzarella'),('Tomate'),('Basilic'),
               ('Jambon'),('Olive'),
                ('Oignon'),('Oeuf');
        ''')
except sqlite3.IntegrityError:
    # Si la table a déjà été remplie, on ne fait rien
    pass

@route('/ingredients', method=['GET', 'POST'])
def ingredients():
    # utilisation de la référence à la connexion globale
    cursor = db_conn.cursor()
    q = request.query.get('q', '')
    if request.method == 'POST':
        name = request.forms.get('name')
        try:
            with db_conn:
                cursor.execute('INSERT INTO ingredients (name) VALUES (?)',
                               (name,))
        except sqlite3.IntegrityError:
            cursor.execute(
                'SELECT id, name FROM ingredients WHERE UPPER(name) LIKE ?',
                (f'%{q.upper()}%',))
            rows = cursor.fetchall()
            return template('ingredients.html',
                            error="L'ingrédient existe déjà",
                            rows=rows, q=q)
    cursor.execute('SELECT id, name FROM ingredients')
    rows = cursor.fetchall()
    return template('ingredients.html', rows=rows, q=q)

run(host='localhost', port=38083, debug=True, reloader=True)

Précautions

Par défaut, en mode développement, Bottle ne tourne que dans un seul thread et un seul processus. Ceci ne pose aucun problème pour SQLite. En revanche, pour un usage en production, notre application peut être lancée dans plusieurs threads voire processus. Dans ce cas, il faut faire attention à la gestion de la base de données. En effet, SQLite ne gère pas forcément bien les accès concurrents.

Pour vérifier le support des accès concurrents dans SQLITE (ref : https://docs.python.org/3/library/sqlite3.html#sqlite3.threadsafety, vous pouvez exécuter la commande suivante dans un terminal :

python -c "import sqlite3; print(sqlite3.threadsafety)"

Selon la valeur retournée, la gestion des accès concurrents peut être :

  • 0 : pas de gestion des accès concurrents, pas de partage du module, ni des connexions, ni des curseurs
  • 1 : gestion des accès concurrents par le module sqlite3, mais pas de partage des connexions ni des curseurs
  • 3 : gestion des accès concurrents par le module sqlite3 et partage des connexions et des curseurs

Si le threadsafety est à 0, il vaut mieux réfléchir à une autre solution (changer de base de données, ou changer de distribution Python).


Programme

  • NSI terminale : Système de gestion de bases de données relationnelles
  • NSI terminale : Langage SQL : requètes d'interrogation et de mise à jour d'une base de données