Este guia expande o exemplo de código usado em Processamento de dados enviados pelos utilizadores através do armazenamento e da obtenção de dados com o Google Cloud SQL.
O Cloud SQL é uma opção de armazenamento disponível com o App Engine que pode ser facilmente integrada em apps e armazenar dados de texto relacionais. Compare o Cloud SQL, o Cloud Datastore e o Cloud Storage e escolha o que cumpre os requisitos da sua app.
Este exemplo baseia-se numa série de guias e mostra como armazenar, atualizar e eliminar dados de publicações de blogue no Cloud SQL.
Antes de começar
Configure o ambiente de programação e crie o projeto do App Engine.
Criar uma instância do Cloud SQL e estabelecer ligação à base de dados
Tem de criar uma instância do Cloud SQL e configurar uma ligação à mesma a partir da sua app do App Engine. Para obter instruções sobre como estabelecer ligação ao Cloud SQL, consulte o artigo Estabelecer ligação ao App Engine.
Criar tabelas
Tem de criar um objeto
Connection no método init() do servlet para processar a ligação à instância do 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.
}
}
O método init() configura uma ligação ao Cloud SQL e, em seguida, cria as tabelas content e user, se não existirem. Após o método init(), a app está pronta para publicar e armazenar novos dados.
No fragmento, as declarações SQL de criação de tabelas são armazenadas em String
variáveis, que são executadas no init() do servlet através da chamada ao método executeUpdate. Tenha em atenção que isto não cria essas tabelas se já existirem.
As duas tabelas criadas no fragmento são denominadas posts e users: posts
contém os detalhes de cada publicação no blogue, enquanto users contém informações sobre o
autor, conforme mostrado aqui:
Tabela: publicações
| Campo | Tipo |
|---|---|
| post_id | INT (incremento automático, chave principal) |
| author_id | INT |
| timestamp | DATA/HORA |
| título | VARCHAR (256) |
| body | VARCHAR (1337) |
Tabela: utilizadores
| Campo | Tipo |
|---|---|
| user_id | INT (incremento automático, chave principal) |
| user_fullname | VARCHAR (64) |
Obter dados iniciais para apresentar num formulário
Um exemplo de utilização comum é pré-preencher um formulário com dados armazenados na base de dados para utilização em seleções de utilizadores. Por exemplo:
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);
}
}
No fragmento de código acima, o servlet consulta a base de dados do Cloud SQL para
obter uma lista de IDs de utilizadores e nomes de autores. Estes são armazenados como tuplos (id, full
name) num mapa de hash. Em seguida, o servlet encaminha o utilizador e o mapa de hash para
/form.jsp, que processa o mapa de hash dos nomes dos autores, conforme mostrado na secção seguinte.
Suporte de interações com bases de dados num formulário
O fragmento seguinte usa JavaServer Pages (JSP) para apresentar ao utilizador os dados iniciais do mapa hash do nome do autor transmitido do servlet e usa esses dados numa lista de seleção. O formulário também permite ao utilizador criar e atualizar dados existentes.
<%@ 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>
No fragmento acima, o formulário é preenchido quando a página é carregada com o mapa hash dos nomes dos autores transmitidos do servlet. O formulário usa a biblioteca de etiquetas padrão (JSTL) when e as operações otherwise fornecem lógica if..else e forEach ciclos através do mapa hash transmitido do servlet.
A página JSP no fragmento acima contém um formulário para criar novas publicações no blogue e atualizar as publicações existentes. Tenha em atenção que o formulário pode enviar os dados para processadores em /create ou /update, consoante o utilizador esteja a criar ou a atualizar uma publicação no blogue.
Para mais informações sobre como usar formulários, consulte o artigo Processamento de dados POST.
Armazenamento de registos
O fragmento seguinte mostra como criar um novo registo a partir dos dados fornecidos pelo utilizador no formulário e armazená-lo na base de dados. O exemplo mostra uma declaração SQL INSERT
criada a partir dos dados enviados no formulário de criação de publicações no blogue
descrito na secção anterior:
// 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);
}
}
O fragmento do código usa a entrada do utilizador e executa-a através do jSoup para a limpar. Usar o jSoup e PreparedStatement
mitigar a possibilidade de ataques de injeção SQL e XSS.
A variável createPostSql contém a consulta INSERT com ? como marcadores de posição para valores que vão ser atribuídos através do método PreparedStatement.set().
Tenha em atenção a ordem dos campos da tabela, uma vez que são referenciados nos métodos de definição PreparedStatement. Por exemplo, author_id é um campo do tipo INT, pelo que tem de usar setInt() para definir author_id.
A obter registos
O fragmento seguinte mostra o método doGet() de um servlet que obtém as linhas da tabela de publicações do blogue e as imprime.
// 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);
}
}
Os resultados da declaração SELECT são colocados num ResultSet, que é
iterado através do método ResultSet.get(). Tenha em atenção o ResultSet.get()métodogetString que corresponde ao esquema da tabela definido anteriormente.
Neste exemplo, cada publicação tem um link [Update] e um link [Delete], que são usados para iniciar atualizações e eliminações de publicações, respetivamente. Para ocultar o ID da publicação, o identificador é codificado em Base64.
A atualizar registos
O fragmento seguinte mostra como atualizar um registo existente:
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);
}
}
Neste fragmento, quando o utilizador clica no link [Atualizar] numa publicação no blogue, é apresentado o formulário JSP usado para criar uma nova publicação, mas agora este está pré-preenchido com o título e o conteúdo da publicação existente. O nome do autor não é apresentado na amostra porque não vai ser alterado.
A atualização de uma publicação é semelhante à criação de uma publicação, exceto que a consulta SQL UPDATE
é usada em vez de INSERT.
Após a execução de executeUpdate(), o utilizador é redirecionado para uma página de confirmação no fragmento.
Eliminar registos
A eliminação de uma linha, uma publicação no blogue neste exemplo, requer a remoção de uma linha da tabela de destino, que é a tabela content no exemplo. Cada registo é identificado pelo respetivo ID, que é o valor post_id no código de exemplo. Use este ID como filtro na consulta DELETE:
Após a execução de executeUpdate(), o utilizador é redirecionado para uma página de confirmação.
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);
}
}
Após descodificar o ID da publicação, o fragmento elimina uma única publicação da tabela posts.
Implementação no App Engine
Pode implementar a sua app no App Engine através do Maven.
Aceda ao diretório raiz do seu projeto e escreva:
mvn package appengine:deploy -Dapp.deploy.projectId=PROJECT_ID
Substitua PROJECT_ID pelo ID do seu Google Cloud projeto. Se o seu ficheiro pom.xml já
especificar o seu
ID do projeto, não precisa de incluir a propriedade -Dapp.deploy.projectId no
comando que executar.
Depois de o Maven implementar a sua app, é aberto automaticamente um separador do navegador de Internet na nova app. Para tal, escreva:
gcloud app browse
O que se segue?
O Cloud SQL é útil para armazenar dados baseados em texto. No entanto, se quiser armazenar conteúdo multimédia avançado, como imagens, deve considerar usar o Cloud Storage.
Em seguida, saiba como usar filas de tarefas para realizar tarefas assíncronas seguindo um exemplo de utilização da API Images para redimensionar as imagens carregadas neste guia.