Erste Schritte: Cloud SQL

Dieser Leitfaden erweitert das Codebeispiel, das in Von Nutzern übermittelte Daten verarbeiten verwendet wird, indem Daten mit Google Cloud SQL gespeichert und abgerufen werden.

Cloud SQL ist eine Speicheroption in App Engine. Es kann problemlos in Anwendungen integriert werden und relationale Textdaten speichern. Vergleichen Sie Cloud SQL, Cloud Datastore und Cloud Storage und wählen Sie die Option aus, die den Anforderungen Ihrer Anwendung entspricht.

Folgendes Beispiel baut auf einer Reihe von Leitfäden auf und zeigt, wie Blogpost-Daten in Cloud SQL gespeichert, aktualisiert und gelöscht werden.

Vorbereitung

Konfigurieren Sie Ihre Entwicklungsumgebung und erstellen Sie Ihr App Engine-Projekt.

Cloud SQL-Instanz erstellen und Verbindung zur Datenbank herstellen

Sie müssen eine Cloud SQL-Instanz erstellen und über die App Engine-App eine Verbindung zu ihr herstellen. Anweisungen zur Verbindung mit Cloud SQL finden Sie unter Verbindung zu App Engine herstellen.

Tabellen erstellen

Sie müssen ein Connection-Objekt in der Servlet-Methode init() erstellen, um die Verbindung zur Cloud SQL-Instanz zu verarbeiten:

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.
  }
}

Die Methode init() stellt eine Verbindung zu Cloud SQL her und erstellt anschließend die content- und user-Tabellen, wenn sie nicht vorhanden sind. Nach Ausführung der Methode init() kann die Anwendung neue Daten bereitstellen und speichern.

Im Snippet werden die SQL-Anweisungen zur Tabellenerstellung in String-Variablen gespeichert, die innerhalb der init() des Servlets durch den Aufruf der Methode executeUpdate ausgeführt werden. Beachten Sie dabei, dass die Tabellen nicht erstellt werden, wenn sie bereits vorhanden sind.

Die beiden im Snippet erstellten Tabellen heißen posts und users. posts enthält die Details zu jedem Blogpost, während users Informationen zum Autor enthält, wie im folgenden Beispiel gezeigt:

Tabelle: posts

Feld Typ
post_id INT (automatisches Inkrement, Primärschlüssel)
author_id INT
timestamp DATETIME
title VARCHAR (256)
body VARCHAR (1337)

Tabelle: users

Feld Typ
user_id INT (automatisches Inkrement, Primärschlüssel)
user_fullname VARCHAR (64)

Initialdaten zur Anzeige in einem Formular abrufen

Ein häufiger Anwendungsfall besteht darin, ein Formular mit in der Datenbank gespeicherten Daten im Voraus auszufüllen, um diese Nutzern zur Auswahl anzubieten. Beispiel:

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);
  }
}

Im oben angegebenen Code-Snippet fragt das Servlet die Cloud SQL-Datenbank ab, um eine Liste mit Nutzer-IDs und Autorennamen abzurufen. Diese werden als (id, full name)-Tupel in einer Hashtabelle gespeichert. Das Servlet leitet dann den Nutzer und die Hashtabelle an /form.jsp weiter, die die Hashtabelle der Autorennamen verarbeitet, wie im nächsten Abschnitt beschrieben.

Unterstützung von Datenbankinteraktionen innerhalb eines Formulars

Das folgende Snippet verwendet JavaServer Pages (JSP), um Nutzern die Initialdaten aus der Hashtabelle der Autorennamen anzuzeigen, die vom Servlet übergeben wurde. Die Daten werden in einer Auswahlliste angezeigt. Mit diesem Formular kann der Nutzer auch Daten erstellen oder vorhandene Daten aktualisieren.

<%@ 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>

Im oben angegebenen Snippet wird das Formular ausgefüllt, sobald die Seite mit der Hashtabelle der Autorennamen lädt, die vom Servlet übergeben wurde. Das Formular verwendet die Vorgänge when und otherwise der JavaServer Pages Standard Tag Library (JSTL) und bietet if..else-Logik sowie forEach-Schleifen über die vom Servlet übergebene Hashtabelle.

Die JSP-Seite im Snippet oben enthält ein Formular, um neue Blogposts zu erstellen oder vorhandene Posts zu aktualisieren. Beachten Sie, dass das Formular die Daten entweder an Handler in /create oder /update sendet. Dies hängt davon ab, ob der Nutzer einen Blogpost erstellt oder aktualisiert.

Weitere Informationen zur Verwendung von Formularen finden Sie unter POST-Daten verarbeiten.

Datensätze speichern

Das folgende Snippet zeigt, wie ein neuer Datensatz aus Daten erstellt wird, die vom Nutzer im Formular eingegeben wurden, und wie der Datensatz in der Datenbank gespeichert wird. Das Beispiel zeigt eine SQL INSERT-Anweisung, die aus den Daten erstellt wird, die entsprechend der Beschreibung im vorherigen Abschnitt über das Formular zum Erstellen von Blogposts gesendet wurden:

// 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);
  }
}

Das Code-Snippet verwendet die Nutzereingabe, um diese zur Bereinigung über jSoup auszuführen. Die Verwendung von jSoup und PreparedStatement verringert die Wahrscheinlichkeit von XSS- und SQL-Injection-Angriffen.

Die Variable createPostSql enthält die INSERT-Abfrage mit ? als Platzhalter für Werte, die mit der Methode PreparedStatement.set() zugewiesen werden.

Beachten Sie die Reihenfolge der Tabellenfelder, auf die in den Set-Methoden zu PreparedStatement verwiesen wird. Beispiel: author_id ist ein Feld vom Typ INT. Daher muss setInt() zum Festlegen von author_id verwendet werden.

Datensätze abrufen

Das folgende Snippet zeigt die Methode doGet() eines Servlets, die die Zeilen aus der Tabelle der Blogposts abruft und sie ausgibt.

// 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);
  }
}

Die Ergebnisse der SELECT-Anweisung werden in ein ResultSet abgelegt, das mit der Methode ResultSet.get() iteriert wird. Beachten Sie die ResultSet.get()-Methode getString, die dem zuvor definierten Tabellenschema entspricht.

In diesem Beispiel hat jeder Post einen [Update]- und einen [Delete]-Link, der zum Aktualisieren bzw. Löschen von Posts verwendet wird. Der Bezeichner wird in Base64 codiert, um die ID des Posts zu verschleiern.

Datensätze aktualisieren

Das folgende Snippet zeigt, wie ein vorhandener Datensatz aktualisiert wird:

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);
  }
}

Klickt der Nutzer in diesem Snippet in einem Blogpost auf den Link [Aktualisieren], wird das JSP-Formular angezeigt, das zum Erstellen eines neuen Posts verwendet wird. Es ist jedoch bereits mit dem Titel und Inhalt des vorhandenen Posts ausgefüllt. Der Name des Autors wird im Beispiel nicht angezeigt, da er sich nicht ändert.

Das Aktualisieren eines Posts ähnelt dem Erstellen eines Posts, mit der Ausnahme, dass die SQL-Abfrage UPDATE anstelle von INSERT verwendet wird.

Nach der Ausführung von executeUpdate() wird der Nutzer zu einer Bestätigungsseite im Snippet weitergeleitet.

Datensätze löschen

Das Löschen einer Zeile, in diesem Beispiel eines Blogposts, erfordert das Entfernen einer Zeile aus der Zieltabelle. In diesem Beispiel ist das die Tabelle content. Jeder Datensatz wird anhand seiner ID identifiziert, die im Beispielcode der Wert post_id ist. Sie verwenden diese ID als Filter in der Abfrage DELETE.

Nach der Ausführung von executeUpdate() wird der Nutzer auf eine Bestätigungsseite weitergeleitet:

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);
  }

}

Nach der Decodierung der ID des Posts löscht das Snippet einen einzelnen Post aus der Tabelle mit den posts.

In App Engine bereitstellen

Sie können Ihre Anwendung mithilfe von Maven in App Engine bereitstellen.

Wechseln Sie zum Stammverzeichnis Ihres Projekts und geben Sie Folgendes ein:

mvn package appengine:deploy -Dapp.deploy.projectId=PROJECT_ID

Ersetzen Sie PROJECT_ID durch die ID Ihres Google Cloud-Projekts. Wenn in der Datei pom.xml bereits Ihre Projekt-ID angegeben ist, müssen Sie das Attribut -Dapp.deploy.projectId nicht in dem von Ihnen ausgeführten Befehl einfügen.

Nachdem Ihre Anwendung von Maven bereitgestellt wurde, sollte automatisch ein Webbrowser-Tab in Ihrer neuen Anwendung geöffnet werden. Geben Sie dazu Folgendes ein:

gcloud app browse

Weitere Informationen

Cloud SQL eignet sich zum Speichern von textbasierten Daten. Wenn Sie jedoch Rich Media wie Bilder speichern möchten, können Sie stattdessen Cloud Storage verwenden (empfohlen).

Als Nächstes erfahren Sie, wie Sie asynchrone Aufgaben mithilfe von Aufgabenwarteschlangen ausführen. Dazu verwenden Sie ein Beispiel, in dem die Größe der Bilder, die im Rahmen dieser Anleitung hochgeladen wurden, mit der Images API geändert wird.