Questa guida estende l'esempio di codice utilizzato in Gestione dei dati inviati dall'utente archiviando e recuperando i dati utilizzando Google Cloud SQL.
Cloud SQL è un'opzione di archiviazione disponibile con App Engine che facilmente integrabili nelle app e archiviano dati di testo relazionali. Confronta Cloud SQL, Cloud Datastore e Cloud Storage e scegli quello che soddisfa i requisiti della tua app.
Questo esempio si basa su una serie di guide e mostra come archiviare, aggiornare ed eliminare i dati dei post del blog in Cloud SQL.
Prima di iniziare
Configura il tuo ambiente di sviluppo e crea il tuo progetto App Engine.
Creazione di un'istanza Cloud SQL e connessione al database
Dovrai creare un'istanza Cloud SQL e configurare una connessione dall'app di App Engine. Per istruzioni sulla connessione a Cloud SQL, consulta Connessione ad App Engine.
Creazione di tabelle
Devi creare un oggetto
Connection
nel metodo init()
del servlet per gestire la connessione all'istanza 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.
}
}
Il metodo init()
configura una connessione a Cloud SQL, quindi crea
content
e user
tabelle se non esistono. Dopo il metodo init()
, l'app è pronta per pubblicare e archiviare nuovi dati.
Nello snippet, le istruzioni SQL di creazione della tabella sono archiviate in String
che vengono eseguite all'interno delle impostazioni init()
del servlet tramite la chiamata
executeUpdate
. Tieni presente che queste tabelle non verranno create se esistono già.
Le due tabelle create nello snippet sono denominate posts
e users
: posts
contiene le specifiche di ciascun post del blog, mentre users
contiene informazioni sui
dell'autore, come mostrato qui:
Tabella: post
Campo | Tipo |
---|---|
post_id | INT (incremento automatico, chiave primaria) |
author_id | INT |
timestamp | DATETIME |
titolo | VARCHAR (256) |
corpo | VARCHAR (1337) |
Tabella: utenti
Campo | Tipo |
---|---|
user_id | INT (incremento automatico, chiave primaria) |
user_fullname | VARCHAR (64) |
Recupero dei dati iniziali da mostrare in un modulo
Un caso d'uso comune è precompilare un modulo con dati archiviati nel database, da utilizzare nelle selezioni degli utenti. Ad esempio:
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);
}
}
Nello snippet di codice riportato sopra, il servlet esegue una query sul database Cloud SQL
recuperare un elenco di ID utente e nomi degli autori. Questi vengono memorizzati come tuple (id, full
name)
in una mappa hash. Il servlet inoltra quindi l'utente e la mappa hash al
/form.jsp
, che elabora la mappa hash dei nomi degli autori come mostrato nel prossimo
.
Supporto delle interazioni con i database in un modulo
Lo snippet seguente utilizza JavaServer Pages (JSP) per visualizzare all'utente i dati iniziali della mappa hash del nome dell'autore passata dal servlet e utilizza questi dati in un elenco di selezione. Il modulo consente inoltre all'utente di creare e aggiornare i dati esistenti.
<%@ 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>
Nello snippet riportato sopra, il modulo viene compilato quando la pagina viene caricata con l'hash
mappa dei nomi degli autori trasmessi dal servlet. Il modulo utilizza le operazioni when
e otherwise
della libreria di tag standard JSTL (JavaServer Pages) per fornire la logica if..else
e forEach
esegue il loop nella mappa hash passata dal servlet.
La pagina JSP nello snippet riportato sopra contiene un modulo per creare nuovi post del blog
e aggiornare quelli esistenti. Tieni presente che il modulo può inviare i dati ai gestori
in /create
o /update
, a seconda che l'utente stia creando o
aggiornare un post del blog.
Per ulteriori informazioni su come utilizzare i moduli, consulta Gestione dei dati POST.
Archiviazione dei record
Il seguente snippet mostra come creare un nuovo record dai dati forniti
dell'utente nel modulo e lo archivi nel database. L'esempio mostra un INSERT
SQL
dichiarazione creata a partire dai dati inviati nel modulo di creazione dei post del blog
descritti nella sezione precedente:
// 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);
}
}
Lo snippet di codice prende l'input utente e lo esegue
jSoup per sanificarlo. L'utilizzo di jSoup e PreparedStatement
attenua la possibilità di attacchi XSS e SQL injection.
La variabile createPostSql
contiene la query INSERT
con ?
come
segnaposto per i valori che verranno assegnati utilizzando il metodo PreparedStatement.set()
.
Tieni presente l'ordine dei campi della tabella a cui viene fatto riferimento nei metodi di impostazione di PreparedStatement
. Ad esempio, author_id
è un campo di tipo INT, quindi per impostare author_id
deve essere utilizzato setInt()
.
Recupero dei record
Il seguente snippet mostra il metodo doGet()
di un servlet che recupera le righe
dalla tabella dei post del blog e li stampa.
// 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);
}
}
I risultati dell'istruzione SELECT
vengono inseriti in un ResultSet
, che viene sottoposto a un'iterazione utilizzando il metodo ResultSet.get()
. Prendi nota del ResultSet.get()
metodo getString
che corrisponde allo schema della tabella definito in precedenza.
In questo esempio, ogni post ha un link [Update]
e un link [Delete]
, ovvero
utilizzata rispettivamente per avviare gli aggiornamenti e le eliminazioni dei post. Offuscare
ID del post, l'identificatore è codificato in Base64
.
Aggiornamento dei record
Il seguente snippet mostra come aggiornare un record esistente:
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);
}
}
In questo snippet, quando l'utente fa clic sul link [Aggiorna] in un post del blog, visualizza il modulo JSP utilizzato per creare un nuovo post, che ora è precompilato con il titolo e i contenuti del post esistente. Il nome dell'autore non viene visualizzato nel campione perché non cambierà.
L'aggiornamento di un post è simile alla creazione di un post, ad eccezione del fatto che l'SQL UPDATE
anziché INSERT
viene utilizzata la query.
Dopo aver eseguito executeUpdate()
, l'utente viene reindirizzato a una pagina di conferma
nello snippet.
Eliminazione dei record in corso...
Per eliminare una riga, ovvero un post del blog in questo esempio, è necessario rimuovere una riga da
la tabella di destinazione, ovvero la tabella content
nell'esempio. Ogni record è identificato dal proprio ID, ovvero dal valore post_id
nel codice di esempio. Utilizzi
questo ID come filtro nella query DELETE
:
Dopo aver eseguito executeUpdate()
, l'utente viene reindirizzato a una pagina di conferma.
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);
}
}
Dopo aver decodificato l'ID del post, lo snippet eliminerà un singolo post dalla tabellaposts
.
Deployment in App Engine
Puoi eseguire il deployment della tua app in App Engine utilizzando Maven.
Vai alla directory principale del progetto e digita:
mvn package appengine:deploy -Dapp.deploy.projectId=PROJECT_ID
Sostituisci PROJECT_ID con l'ID del tuo progetto Google Cloud. Se
il tuo pom.xml
file
specifica
ID progetto, non devi includere la proprietà -Dapp.deploy.projectId
nell'
il comando che esegui.
Dopo che Maven ha implementato la tua app, apri automaticamente una scheda del browser web nella nuova app digitando:
gcloud app browse
Passaggi successivi
Cloud SQL è utile per archiviare dati basati su testo. Tuttavia, se vuoi archiviare contenuti multimediali avanzati come le immagini, ti consigliamo di utilizzare Cloud Storage.
Scopri di più sull'utilizzo delle code di attività eseguire attività asincrone seguendo un esempio di utilizzo dell'API Images per ridimensiona le immagini caricate in questa guida.