Getting Started: Cloud SQL

This guide extends the code sample used in Handling user submitted data by storing and retrieving data using Google Cloud SQL.

Cloud SQL is one storage option available with App Engine that can be easily integrated into apps and store relational text data. Compare Cloud SQL, Cloud Datastore, and Cloud Storage and choose the one that meets your app's requirements.

This sample builds upon a series of guides and shows how to store, update and delete blog post data in Cloud SQL.

Before you begin

Configure your development environment and create your App Engine project.

Creating a Cloud SQL instance and database

To create a Cloud SQL instance and a database:

  1. Create a Second Generation Cloud SQL instance and configure the root user. You can use the root user to connect but Google recommends that you create a user.
  2. Using the Cloud SDK, get the Cloud SQL instance connection name to use as a connection string in your application code:
    gcloud sql instances describe [INSTANCE_NAME]
    Record the value returned for connectionName. You can also find this value in the Instance details page of the Google Cloud Platform Console. For example, in the Cloud SDK output:
    gcloud sql instances describe [INSTANCE_NAME]
      connectionName: project1:us-central1:instance1
  3. Create a database in your Cloud SQL instance. In this example, the database is named content.
    gcloud sql databases create content --instance=[INSTANCE_NAME]

    For more information, see Creating a database.

Setting up a database connection

To set application system properties for the Cloud SQL instance connection name, database, user, and password:

  1. Add the Cloud SQL instance connection name, database, user, and password in the top-level <properties> section of your project's pom.xml file.

    <properties>
      [...]
      <INSTANCE_CONNECTION_NAME>[INSTANCE_CONNECTION_NAME]</INSTANCE_CONNECTION_NAME>
      <user>[USERNAME]</user>
      <password>[PASSWORD]</password>
      <database>[DATABASE-NAME]</database>
    </properties>
    
  2. Set the Cloud SQL connection properties in the appengine-web.xml so the app can access the data.

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

Connecting to Cloud SQL and creating tables

You must create a Connection object in the servlet init() method to handle the connection to the Cloud SQL instance:

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

The init() method sets up a connection to Cloud SQL and then creates the content and user tables if they do not exist. After the init() method, the app is ready to serve and store new data.

In the snippet, the table creation SQL statements are stored in String variables, which are executed within the servlet's init() through the call to executeUpdate method. Notice this won't create those tables if they already exist.

The two tables created in the snippet are named content and user: content holds the specifics of each blog post, while user contains information on the author, as shown here:

Table: content

Field Type
post_id INT (auto increment, primary key)
author_id INT
timestamp DATETIME
title VARCHAR (256)
body VARCHAR (1337)

Table: user

Field Type
user_id INT (auto increment, primary key)
user_fullname VARCHAR (64)

Retrieving initial data to show in a form

A common use case is to pre-populate a form with data stored in the database, for use in user selections. For example:

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

In the code snippet above, the servlet queries the Cloud SQL database to retrieve a list of user IDs and author names. These are stored as (id, full name) tuples in a hash map. The servlet then forwards the user and hash map to /form.jsp, which processes the hash map of author names as shown in the next section.

Supporting database interactions in a form

The following snippet uses JavaServer Pages (JSP) to display to the user the initial data from the author name hash map passed in from the servlet, and uses that data in a selection list. The form also lets the user create and update existing data.

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

In the snippet above, the form is populated when the page loads with the hash map of author names passed from the servlet. The form uses JavaServer Pages Standard Tag Library (JSTL) when and otherwise operations provides if..else logic and forEach loops through the hash map passed from the servlet.

The JSP page in the snippet above contains a form for creating new blog posts and updating existing posts. Notice that the form can send the data to handlers at either /create or /update depending on whether the user is creating or updating a blog post.

For more information on how to use forms, see Handling POST data.

Storing records

The following snippet shows how to build a new record from data supplied by the user in the form, and store in in the database. The example shows a SQL INSERT statement built out from the data submitted in the blog post creation form described in the previous section:

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

The code snippet uses takes the user input and runs it through jSoup to sanitize it. Using jSoup and PreparedStatement mitigate the possibility of XSS and SQL injection attacks.

The createPostSql variable contains the INSERT query with ? as placeholders for values that will be assigned using PreparedStatement.set() method.

Note the order of the table fields as these are referenced in PreparedStatement set methods. For example, the author_id is a field of type INT, so setInt() must be used to set the author_id.

Retrieving records

The following snippet shows a servlet's doGet() method that fetches the rows from the table of blog posts and prints them out.

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

The results from the SELECT statement is put into a ResultSet, which is iterated upon using the ResultSet.get() method. Note the ResultSet.get() method getString that correspond to the table schema defined earlier.

For this example, each post has an [Update] and a [Delete] link, which is used to initiate post updates and deletions respectively. To obfuscate the post's ID, the identifier is encoded in Base64.

Updating records

The following snippet shows how to update an existing record:

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 this snippet, when the user clicks the [Update] link on a blog post, it displays the JSP form used to create a new post but now this is pre-populated with the existing post's title and content. The author's name is not displayed in the sample because it won't change.

Updating a post is similar to creating a post, except that the SQL UPDATE query is used instead of INSERT.

After running executeUpdate(), the user is redirected to a confirmation page in the snippet.

Deleting records

Deleting a row, a blog post in this example, requires the removal of a row from the target table, which is the content table in the example. Each record is identified by its ID, which is the post_id value in the sample code. You use this ID as the filter in the DELETE query:

After running executeUpdate(), the user is redirected to a confirmation page.

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

}

After decoding the post's ID, the snippet will delete a single post from the posts table.

Deploying to App Engine

You can deploy your app to App Engine using Maven.

Go to the root directory of your project and type:

mvn appengine:deploy

After Maven deploys your app, open a web browser tab automatically at your new app by typing:

gcloud app browse

What's next

Cloud SQL is useful for storing text-based data; however, if you want to store rich media such as images you should consider using Cloud Storage.

Using Cloud Storage

Send feedback about...

App Engine standard environment for Java