Ce guide complète l'exemple de code utilisé sur la page consacrée à la gestion des données envoyées par les utilisateurs en stockant et en récupérant des données à l'aide de Google Cloud SQL.
Cloud SQL est une option de stockage disponible avec App Engine qui s'intègre facilement aux applications et qui peut stocker des données de texte relationnelles. Comparez Cloud SQL, Cloud Datastore et Cloud Storage pour choisir la solution qui répond aux besoins de votre application.
Cet exemple s'appuie sur une série de guides et montre comment stocker, mettre à jour et supprimer des données d'articles de blog dans Cloud SQL.
Avant de commencer
Configurez votre environnement de développement et créez un projet App Engine.
Créer une instance Cloud SQL et se connecter à la base de données
Vous devez créer une instance Cloud SQL et configurer une connexion à partir de votre application App Engine. Pour obtenir des instructions sur la connexion à Cloud SQL, consultez la section Se connecter à App Engine.
Créer des tables
Vous devez créer un objet Connection
dans la méthode init()
du servlet pour gérer la connexion à l'instance Cloud SQL :
Connection conn; // Cloud SQL connection
// Cloud SQL table creation commands
final String createContentTableSql =
"CREATE TABLE IF NOT EXISTS posts ( post_id INT NOT NULL "
+ "AUTO_INCREMENT, author_id INT NOT NULL, timestamp DATETIME NOT NULL, "
+ "title VARCHAR(256) NOT NULL, "
+ "body VARCHAR(1337) NOT NULL, PRIMARY KEY (post_id) )";
final String createUserTableSql =
"CREATE TABLE IF NOT EXISTS users ( user_id INT NOT NULL "
+ "AUTO_INCREMENT, user_fullname VARCHAR(64) NOT NULL, "
+ "PRIMARY KEY (user_id) )";
@Override
public void init() throws ServletException {
try {
String url = System.getProperty("cloudsql");
try {
conn = DriverManager.getConnection(url);
// Create the tables so that the SELECT query doesn't throw an exception
// if the user visits the page before any posts have been added
conn.createStatement().executeUpdate(createContentTableSql); // create content table
conn.createStatement().executeUpdate(createUserTableSql); // create user table
// Create a test user
conn.createStatement().executeUpdate(createTestUserSql);
} catch (SQLException e) {
throw new ServletException("Unable to connect to SQL server", e);
}
} finally {
// Nothing really to do here.
}
}
La méthode init()
configure une connexion à Cloud SQL, puis crée les tables content
et user
si elles n'existent pas. Après la méthode init()
, l'application est prête à être diffusée et peut stocker de nouvelles données.
Dans l'extrait, les instructions SQL de création de tables sont stockées dans des variables String
, qui sont exécutées dans la méthode init()
du servlet via l'appel à la méthode executeUpdate
. Notez que les tables ne seront pas créées si elles existent déjà.
Les deux tables créées dans l'extrait sont nommées posts
et users
: posts
contient les spécificités de chaque article de blog, tandis que users
contient des informations sur l'auteur, comme illustré ci-dessous :
Table : posts
Champ | Type |
---|---|
post_id | INT (incrémentation automatique, clé primaire) |
author_id | INT |
timestamp | DATETIME |
title | VARCHAR (256) |
body | VARCHAR (1337) |
Table : users
Champ | Type |
---|---|
user_id | INT (incrémentation automatique, clé primaire) |
user_fullname | VARCHAR (64) |
Récupérer les données initiales pour les afficher dans un formulaire
Un cas d'utilisation courant consiste à pré-remplir un formulaire avec des données stockées dans la base de données, afin de s'en servir dans les sélections d'utilisateurs. Exemple :
Connection conn;
final String getUserId = "SELECT user_id, user_fullname FROM users";
Map<Integer, String> users = new HashMap<Integer, String>();
@Override
public void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// Find the user ID from the full name
try (ResultSet rs = conn.prepareStatement(getUserId).executeQuery()) {
while (rs.next()) {
users.put(rs.getInt("user_id"), rs.getString("user_fullname"));
}
req.setAttribute("users", users);
req.getRequestDispatcher("/form.jsp").forward(req, resp);
} catch (SQLException e) {
throw new ServletException("SQL error", e);
}
}
Dans l'extrait de code ci-dessus, le servlet interroge la base de données Cloud SQL pour récupérer une liste d'ID utilisateur et de noms d'auteurs. Ceux-ci sont stockés sous forme de tuples (id, full
name)
dans une table de hachage. Le servlet transmet ensuite l'utilisateur et la table de hachage à /form.jsp
, qui traite la table de hachage des noms d'auteur comme indiqué dans la section suivante.
Assurer la compatibilité des interactions de base de données dans un formulaire
L'extrait suivant se sert de la technologie JSP (JavaServer Pages) pour présenter à l'utilisateur les données initiales de la table de hachage des noms d'auteurs transmise à partir du servlet, et exploite ces données dans une liste de sélection. Le formulaire permet également à l'utilisateur de créer et de mettre à jour des données.
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn"%>
<div>
<c:choose>
<c:when test="${id == null}">
<h2>Create a new blog post</h2>
<form method="POST" action="/create">
</c:when>
<c:otherwise>
<h2><c:out value="${pagetitle}" /></h2>
<form method="POST" action="/update">
<input type="hidden" name="blogContent_id" value="${id}">
</c:otherwise>
</c:choose>
<div>
<label for="title">Title</label>
<input type="text" name="blogContent_title" id="title" size="40" value="${title}" />
</div>
<div>
<label for="author">Author</label>
<select name="blogContent_id">
<c:forEach items="${users}" var="user">
<option value="${user.key}">${user.value}</option>
</c:forEach>
</select>
<input type="text" name="blogContent_author" id="author" size="40" value="${author}" />
</div>
<div>
<label for="description">Post content</label>
<textarea name="blogContent_description" id="description" rows="10" cols="50">${body}</textarea>
</div>
<button type="submit">Save</button>
</form>
</div>
Dans l'extrait de code ci-dessus, le formulaire est rempli au chargement de la page, à l'aide de la table de hachage des noms d'auteurs transmise à partir du servlet. Le formulaire utilise les opérations when
et otherwise
de la bibliothèque de balises standards des pages JavaServer (JSTL). Il fournit une logique if..else
et des boucles forEach
via la table de hachage transmise par le servlet.
Dans l'extrait ci-dessus, la page JSP contient un formulaire permettant de créer des articles de blog et de mettre à jour les articles existants. Notez que le formulaire peut envoyer les données aux gestionnaires via /create
ou /update
, selon que l'utilisateur crée ou met à jour un article de blog.
Pour en savoir plus sur l'utilisation des formulaires, consultez la page Gérer des données POST.
Stocker les enregistrements
L'extrait suivant montre comment créer un enregistrement à partir des données fournies par l'utilisateur dans le formulaire et comment les stocker dans la base de données. Dans cet exemple, nous avons une instruction SQL INSERT
générée à partir des données qui ont été envoyées dans le formulaire de création d'article de blog décrit dans la section précédente :
// Post creation query
final String createPostSql =
"INSERT INTO posts (author_id, timestamp, title, body) VALUES (?, ?, ?, ?)";
@Override
public void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// Create a map of the httpParameters that we want and run it through jSoup
Map<String, String> blogContent =
req.getParameterMap()
.entrySet()
.stream()
.filter(a -> a.getKey().startsWith("blogContent_"))
.collect(
Collectors.toMap(
p -> p.getKey(), p -> Jsoup.clean(p.getValue()[0], Whitelist.basic())));
// Build the SQL command to insert the blog post into the database
try (PreparedStatement statementCreatePost = conn.prepareStatement(createPostSql)) {
// set the author to the user ID from the user table
statementCreatePost.setInt(1, Integer.parseInt(blogContent.get("blogContent_id")));
statementCreatePost.setTimestamp(2, new Timestamp(new Date().getTime()));
statementCreatePost.setString(3, blogContent.get("blogContent_title"));
statementCreatePost.setString(4, blogContent.get("blogContent_description"));
statementCreatePost.executeUpdate();
conn.close(); // close the connection to the Cloud SQL server
// Send the user to the confirmation page with personalised confirmation text
String confirmation = "Post with title " + blogContent.get("blogContent_title") + " created.";
req.setAttribute("confirmation", confirmation);
req.getRequestDispatcher("/confirm.jsp").forward(req, resp);
} catch (SQLException e) {
throw new ServletException("SQL error when creating post", e);
}
}
L'extrait de code se sert de l'entrée de l'utilisateur et l'exécute via jSoup pour la nettoyer. L'utilisation de jSoup et de PreparedStatement
atténue le risque d'attaques par injection XSS et SQL.
La variable createPostSql
contient la requête INSERT
avec ?
comme espaces réservés pour les valeurs qui seront attribuées à l'aide de la méthode PreparedStatement.set()
.
Notez l'ordre des champs de la table, car ils sont référencés dans les méthodes de l'ensemble PreparedStatement
. Par exemple, author_id
est un champ de type INT, donc setInt()
doit être utilisé pour définir author_id
.
Récupérer les enregistrements
L'extrait suivant montre la méthode doGet()
d'un servlet, qui consiste à récupérer les lignes de la table des articles de blog et à les imprimer.
// Preformatted HTML
String headers =
"<!DOCTYPE html><meta charset=\"utf-8\"><h1>Welcome to the App Engine Blog</h1><h3><a href=\"blogpost\">Add a new post</a></h3>";
String blogPostDisplayFormat =
"<h2> %s </h2> Posted at: %s by %s [<a href=\"/update?id=%s\">update</a>] | [<a href=\"/delete?id=%s\">delete</a>]<br><br> %s <br><br>";
@Override
public void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// Retrieve blog posts from Cloud SQL database and display them
PrintWriter out = resp.getWriter();
out.println(headers); // Print HTML headers
try (ResultSet rs = conn.prepareStatement(selectSql).executeQuery()) {
Map<Integer, Map<String, String>> storedPosts = new HashMap<>();
while (rs.next()) {
Map<String, String> blogPostContents = new HashMap<>();
// Store the particulars for a blog in a map
blogPostContents.put("author", rs.getString("users.user_fullname"));
blogPostContents.put("title", rs.getString("posts.title"));
blogPostContents.put("body", rs.getString("posts.body"));
blogPostContents.put("publishTime", rs.getString("posts.timestamp"));
// Store the post in a map with key of the postId
storedPosts.put(rs.getInt("posts.post_id"), blogPostContents);
}
// Iterate the map and display each record's contents on screen
storedPosts.forEach(
(k, v) -> {
// Encode the ID into a websafe string
String encodedID = Base64.getUrlEncoder().encodeToString(String.valueOf(k).getBytes());
// Build up string with values from Cloud SQL
String recordOutput =
String.format(blogPostDisplayFormat, v.get("title"), v.get("publishTime"),
v.get("author"), encodedID, encodedID, v.get("body"));
out.println(recordOutput); // print out the HTML
});
} catch (SQLException e) {
throw new ServletException("SQL error", e);
}
}
Les résultats de l'instruction SELECT
sont placés dans un élément ResultSet
, qui est itéré lors de l'utilisation de la méthode ResultSet.get()
. Notez l'objet getString
de la méthode ResultSet.get()
correspondant au schéma de table défini précédemment.
Pour cet exemple, chaque article comporte un lien [Update]
et un lien [Delete]
, qui permettent, respectivement, de lancer les mises à jour et les suppressions d'articles. Pour obscurcir l'ID de l'article, celui-ci est encodé au format Base64
.
Mettre à jour les enregistrements
L'extrait suivant montre comment mettre à jour un enregistrement existant :
final String updateSql = "UPDATE posts SET title = ?, body = ? WHERE post_id = ?";
@Override
public void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// Create a map of the httpParameters that we want and run it through jSoup
Map<String, String> blogContent =
req.getParameterMap()
.entrySet()
.stream()
.filter(a -> a.getKey().startsWith("blogContent_"))
.collect(
Collectors.toMap(
p -> p.getKey(), p -> Jsoup.clean(p.getValue()[0], Whitelist.basic())));
// Build up the PreparedStatement
try (PreparedStatement statementUpdatePost = conn.prepareStatement(updateSql)) {
statementUpdatePost.setString(1, blogContent.get("blogContent_title"));
statementUpdatePost.setString(2, blogContent.get("blogContent_description"));
statementUpdatePost.setString(3, blogContent.get("blogContent_id"));
statementUpdatePost.executeUpdate(); // Execute update query
conn.close();
// Confirmation string
final String confirmation = "Blog post " + blogContent.get("blogContent_id") + " has been updated";
req.setAttribute("confirmation", confirmation);
req.getRequestDispatcher("/confirm.jsp").forward(req, resp);
} catch (SQLException e) {
throw new ServletException("SQL error", e);
}
}
Dans cet extrait, lorsque l'utilisateur clique sur le lien [Mettre à jour] d'un article de blog, le formulaire JSP permettant de créer un article s'affiche. Cependant, ce formulaire est désormais pré-rempli avec le titre et le contenu de l'article existant. Le nom de l'auteur ne s'affiche pas dans l'exemple, car il ne change pas.
La mise à jour et la création d'un article sont deux processus semblables, si ce n'est que la requête SQL INSERT
est remplacée par UPDATE
pour la mise à jour.
Après avoir exécuté executeUpdate()
, l'utilisateur est redirigé vers une page de confirmation dans l'extrait.
Supprimer les enregistrements
La suppression d'une ligne (d'un article de blog ici) nécessite de supprimer une ligne de la table cible, qui est la table content
dans l'exemple. Chaque enregistrement est identifié par son ID, qui correspond à la valeur post_id
dans l'exemple de code. Vous devez utiliser cet ID comme filtre dans la requête DELETE
:
Après l'exécution de executeUpdate()
, l'utilisateur est redirigé vers une page de confirmation.
final String deleteSql = "DELETE FROM posts WHERE post_id = ?";
@Override
public void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
Map<String, String[]> userData = req.getParameterMap();
String[] postId = userData.get("id");
String decodedId = new String(Base64.getUrlDecoder().decode(postId[0])); // Decode the websafe ID
try (PreparedStatement statementDeletePost = conn.prepareStatement(deleteSql)) {
statementDeletePost.setString(1, postId[0]);
statementDeletePost.executeUpdate();
final String confirmation = "Post ID " + postId[0] + " has been deleted.";
req.setAttribute("confirmation", confirmation);
req.getRequestDispatcher("/confirm.jsp").forward(req, resp);
} catch (SQLException e) {
throw new ServletException("SQL error", e);
}
}
Après avoir décodé l'ID de l'article, l'extrait supprimera un seul article de la table posts
.
Déployer sur App Engine
Vous pouvez déployer votre application sur App Engine à l'aide de Maven.
Accédez au répertoire racine de votre projet, puis saisissez la commande suivante :
mvn package appengine:deploy -Dapp.deploy.projectId=PROJECT_ID
Remplacez PROJECT_ID par l'ID de votre projet Google Cloud. Si votre ID de projet est déjà inclus dans le fichier pom.xml
, vous n'avez pas besoin d'inclure la propriété -Dapp.deploy.projectId
dans la commande que vous exécutez.
Lorsque Maven a déployé votre application, saisissez la commande ci-dessous pour ouvrir un onglet de navigateur Web automatiquement dans votre nouvelle application :
gcloud app browse
Étape suivante
Cloud SQL est une solution pratique pour stocker des données textuelles. Toutefois, si vous souhaitez stocker du contenu multimédia riche, tel que des images, nous vous recommandons d'utiliser Cloud Storage.
Ensuite, découvrez comment utiliser les files d'attente de tâches pour exécuter des tâches asynchrones en suivant un exemple d'utilisation de l'API Images afin de redimensionner les images transférées dans ce guide.