Este guia amplia o exemplo de código usado em Como processar dados enviados pelo usuário com o armazenamento e recuperação de dados com o uso do Google Cloud SQL.
O Cloud SQL é uma opção de armazenamento disponível com o App Engine que pode armazenar dados de texto relacional e ser facilmente integrada a aplicativos. Compare o Cloud SQL, o Cloud Datastore e o Cloud Storage e escolha aquele que atenda aos requisitos do seu aplicativo.
Nesta amostra, oferecemos uma série de guias e mostramos como armazenar, atualizar e excluir dados de postagem do blog no Cloud SQL.
Antes de começar
Configure o ambiente de desenvolvimento e crie seu projeto do App Engine.
Como criar uma instância e um banco de dados do Cloud SQL
Para criar uma instância do Cloud SQL e um banco de dados:
- Crie uma instância do Cloud SQL de segunda geração e configure o usuário raiz. Use o usuário raiz para se conectar, mas o Google recomenda criar um usuário.
- Usando o Cloud SDK, veja o nome da conexão da instância do Cloud SQL para usar como uma string de conexão no código do seu aplicativo:
gcloud sql instances describe [INSTANCE_NAME]
Registre o valor retornado paraconnectionName
. Você também encontra esse valor na página de "Detalhes da instância" no Console do Google Cloud Platform. Por exemplo, na saída do Cloud SDK:gcloud sql instances describe [INSTANCE_NAME] connectionName: project1:us-central1:instance1
-
Crie um banco de dados em sua instância do Cloud SQL. Neste exemplo, o banco de dados é chamado de
content
.gcloud sql databases create content --instance=[INSTANCE_NAME]
Para mais informações, consulte Como criar um banco de dados.
Como configurar uma conexão de banco de dados
Para definir propriedades do sistema de aplicativos para nome da conexão da instância do Cloud SQL, banco de dados, usuário e senha:
Adicione o nome da conexão da instância do Cloud SQL, banco de dados, usuário e senha na seção
<properties>
de nível superior do arquivopom.xml
do projeto.<properties> [...] <INSTANCE_CONNECTION_NAME>[INSTANCE_CONNECTION_NAME]</INSTANCE_CONNECTION_NAME> <user>[USERNAME]</user> <password>[PASSWORD]</password> <database>[DATABASE-NAME]</database> </properties>
Defina as propriedades de conexão do Cloud SQL no arquivo
appengine-web.xml
para que o app acesse os dados.<appengine-web-app xmlns="http://appengine.google.com/ns/1.0"> <threadsafe>true</threadsafe> <runtime>java8</runtime> <use-google-connector-j>true</use-google-connector-j> </appengine-web-app>
Como se conectar ao Cloud SQL e criar tabelas
Crie um objeto Connection
(em inglês) no método init()
do servlet para processar a conexão com a 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 conexão com o Cloud SQL e cria as tabelas de content
e user
se elas não existirem. Após o método init()
, o app está pronto para suprir e armazenar novos dados.
No snippet, as instruções SQL de criação de tabela são armazenadas nas variáveis String
, que são executadas no init()
do servlet por meio da chamada ao método executeUpdate
. As tabelas não serão criadas se elas já existirem.
As duas tabelas criadas no snippet são denominadas posts
e users
. posts
armazena as especificidades de cada postagem do blog, enquanto users
contêm informações sobre o autor, como mostrado a seguir:
Tabela: posts
Campo | Tipo |
---|---|
post_id | INT (incremento automático, chave primária) |
author_id | INT |
timestamp | DATETIME |
title | VARCHAR (256) |
body | VARCHAR (1337) |
Tabela: users
Campo | Tipo |
---|---|
user_id | INT (incremento automático, chave primária) |
user_fullname | VARCHAR (64) |
Como recuperar dados iniciais para mostrar em um formulário
Um caso de uso comum é pré-preencher um formulário com dados armazenados no banco de dados, para uso em seleções de usuários. 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.get529ggInt("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 snippet de código acima, o servlet consulta o banco de dados do Cloud SQL para recuperar uma lista de IDs de usuários e nomes de autores. Eles são armazenados como tuplas (id, full
name)
em um mapa hash. O servlet então encaminha o usuário e o mapa hash para /form.jsp
, que processa o mapa hash dos nomes dos autores, conforme mostrado na próxima seção.
Interações de banco de dados de suporte em um formulário
O snippet a seguir usa JavaServer Pages (JSP) para exibir ao usuário os dados iniciais do mapa de hash do nome do autor transmitido do servlet e usa esses dados em uma lista de seleção. O formulário também permite ao usuário 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 snippet 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 as operações when
e otherwise
do JavaServer Pages Standard Tag Library (JSTL), fornece a lógica if..else
e repete forEach
pelo mapa hash transmitido do servlet.
A página JSP no snippet acima contém um formulário para criar novas postagens de blog e atualizar postagens existentes. Observe que o formulário pode enviar os dados aos gerenciadores em /create
ou /update
dependendo de o usuário estar criando ou atualizando uma postagem no blog.
Para mais informações sobre como usar formulários, consulte Como processar dados POST.
Como armazenar registros
O snippet a seguir mostra como criar um registro com base em dados fornecidos pelo usuário no formulário e armazená-lo no banco de dados. O exemplo mostra uma instrução INSERT
SQL compilada a partir dos dados enviados no formulário de criação de postagem do blog descrito na seçã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 snippet de código usa a entrada do usuário e a executa por meio de jSoup para limpá-lo. O uso do jSoup e PreparedStatement
diminui a possibilidade de ataques de injeção de XSS e SQL.
A variável createPostSql
contém a consulta INSERT
com ?
como placeholders de valores que serão atribuídos com o uso do método PreparedStatement.set()
.
Observe a ordem dos campos da tabela, porque eles são referenciados nos métodos definidos por PreparedStatement
. Por exemplo, o author_id
é um campo do tipo INT. Sendo assim, setInt()
precisa ser usado para definir o author_id
.
Como recuperar registros
O snippet a seguir mostra o método doGet()
do servlet que extrai as linhas da tabela de postagens de blog 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 instrução SELECT
são colocados em um ResultSet
, que é iterado com o uso do método ResultSet.get()
. Observe o método ResultSet.get()
getString
que corresponde ao esquema da tabela definido anteriormente.
Neste exemplo, cada postagem tem um link [Update]
e um [Delete]
, que são usados para iniciar atualizações e exclusões de postagens, respectivamente. Para ofuscar o código da postagem, o identificador é codificado em Base64
.
Como atualizar registros
O snippet a seguir mostra como atualizar um registro 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);
}
}
Nesse snippet, quando o usuário clica no link [Atualizar] em uma postagem no blog, ele exibe o formulário JSP usado para criar uma nova postagem, mas agora está pré-preenchido com o título e conteúdo da postagem existente. O nome do autor não é exibido no exemplo porque ele não muda.
As operações de atualização e de criação de uma postagem são semelhantes, exceto que a consulta UPDATE
SQL é usada em vez de INSERT
.
Depois de executar executeUpdate()
, o usuário é redirecionado para uma página de confirmação no snippet.
Como excluir registros
A exclusão de uma linha (neste exemplo, uma postagem de blog) requer a remoção de uma linha da tabela de destino, que é a tabela content
no exemplo. Cada registro é identificado pelo seu código, que é o valor post_id
no código de exemplo. Use esse código como o filtro na consulta DELETE
:
Depois de executar executeUpdate()
, o usuário é 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 a decodificação do código da postagem, o snippet exclui uma única publicação da tabela posts
.
Como implantar no App Engine
Implante o app no App Engine usando o Maven.
Acesse o diretório raiz do projeto e digite:
mvn appengine:deploy
Depois que o Maven implantar o aplicativo, digite o comando a seguir para abrir uma guia do navegador da Web automaticamente em seu novo aplicativo:
gcloud app browse
A seguir
O Cloud SQL é útil para armazenar dados baseados em texto. No entanto, se você quiser armazenar rich media, como imagens, precisará considerar o uso do Cloud Storage.
Para saber mais sobre o uso de filas de tarefas para executar tarefas assíncronas, siga um exemplo de uso da API Images e redimensione as imagens carregadas neste guia.