Primeiros passos: Cloud SQL

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 o projeto do App Engine.

Como criar uma instância do Cloud SQL e conectar-se ao banco de dados

Você precisará criar uma instância do Cloud SQL e configurar uma conexão com ela a partir do aplicativo do App Engine. Veja instruções sobre como se conectar ao Cloud SQL em Conectando-se ao App Engine.

Criar tabelas

Crie um objeto Connection 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 content e user se elas não existirem. Após o método init(), o aplicativo estará pronto para exibir e armazenar novos dados.

No snippet, as instruções SQL de criação de tabela são armazenadas em variáveis String, que são executadas no init() do servlet por meio da chamada para o método executeUpdate. As tabelas não serão criadas se elas já existirem.

As duas tabelas criadas no snippet são chamadas de posts e users: posts contém os detalhes de cada postagem do blog, enquanto users contém informações sobre o autor, conforme mostrado aqui:

Tabela: postagens

Campo Tipo
post_id INT (incremento automático, chave primária)
author_id INT
timestamp DATETIME
title VARCHAR (256)
body VARCHAR (1337)

Tabela: usuários

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.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 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 de hash. Em seguida, o servlet encaminha o usuário e o mapa de hash para /form.jsp, que processa o mapa de 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 da JavaServer Pages Standard Tag Library (JSTL), fornece a lógica if..else e os loops forEach por meio do mapa de 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 para os gerenciadores em /create ou /update, conforme o usuário está criando ou atualizando uma postagem do 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. Usar o jSoup e o PreparedStatement reduz a possibilidade de ataques de injeção de XSS e SQL.

A variável createPostSql contém a consulta INSERT com ? como marcadores para valores que serão atribuídos usando o método PreparedStatement.set().

Observe a ordem dos campos da tabela conforme eles são referenciados nos métodos do conjunto PreparedStatement. Por exemplo, o author_id é um campo do tipo INT. Portanto, setInt() precisa ser usado para definir o author_id.

Como recuperar registros

O snippet a seguir mostra um método doGet() do servlet que busca as linhas da tabela de postagens do 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 getString do método ResultSet.get() que corresponde ao esquema de tabela definido anteriormente.

Para este exemplo, cada postagem tem um link [Update] e [Delete], que é usado para iniciar atualizações e exclusões de postagens, respectivamente. Para ofuscar o ID 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, uma postagem do blog neste exemplo, requer a remoção de uma linha da tabela de destino, que é a tabela content do exemplo. Cada registro é identificado pelo ID, que é o valor post_id no exemplo de código. Use esse ID como 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);
  }

}

Depois de decodificar o ID da postagem, o snippet excluirá uma única postagem 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 package appengine:deploy -Dapp.deploy.projectId=PROJECT_ID

Substitua PROJECT_ID pelo ID do projeto do Cloud. Se o arquivo pom.xmlespecificar o ID do projeto, não será necessário incluir a propriedade -Dapp.deploy.projectId no comando executado.

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.