このガイドでは、ユーザーが送信したデータの処理で使ったコードサンプルを変更して、Google Cloud SQL でデータの保存と取得を行います。
Cloud SQL は App Engine で利用できるストレージ オプションの 1 つで、簡単にアプリに統合したり、テキストデータを保存したりできます。Cloud SQL、Cloud Datastore、Cloud Storage を比較して、アプリの要件に最適なオプションを選択してください。
このサンプルは一連のガイドを通じて作成されています。ここでは、サンプルを使用して、Cloud SQL でブログ投稿データの保存、更新、削除を行う方法を学習します。
始める前に
開発環境を構成し、App Engine プロジェクトを作成します。
Cloud SQL インスタンスを作成してデータベースに接続する
Cloud SQL インスタンスを作成し、App Engine アプリからそのインスタンスへの接続を設定する必要があります。Cloud SQL に接続する手順については、App Engine への接続をご覧ください。
テーブルを作成する
Cloud SQL インスタンスへの接続を処理するには、サーブレット init()
メソッドで Connection
オブジェクトを作成する必要があります。
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.
}
}
init()
メソッドは Cloud SQL への接続を設定し、content
テーブルと user
テーブルを作成します(テーブルが存在しない場合)。init()
メソッドの実行後、アプリで新しいデータの配信と保存が可能になります。
スニペットでは、テーブル作成の SQL ステートメントが String
変数に保存されています。このステートメントは、executeUpdate
メソッドの呼び出しによりサーブレット内の init()
で実行されます。テーブルが存在する場合、これらのテーブルは作成されません。
スニペットで作成された 2 つのテーブルは、それぞれ posts
と users
という名前が付けられます。posts
には各ブログ投稿の詳細が格納され、users
には作成者に関する情報が格納されます。
テーブル: posts
フィールド | 型 |
---|---|
post_id | INT(自動増分、プライマリー キー) |
author_id | INT |
timestamp | DATETIME |
title | VARCHAR (256) |
body | VARCHAR (1337) |
テーブル: users
フィールド | 型 |
---|---|
user_id | INT(自動増分、プライマリー キー) |
user_fullname | VARCHAR (64) |
フォームに表示する初期データを取得する
一般的な利用方法としては、ユーザーが選択できるように、データベースに保存されているデータを自動的にフォームに挿入する方法があります。次に例を示します。
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);
}
}
上のコード部分では、サーブレットが Cloud SQL データベースにクエリを送信し、ユーザー ID と作者名のリストを取得します。これらは (id, full
name)
タプルとしてハッシュマップに保存されます。次に、サーブレットはユーザーとハッシュマップを /form.jsp
に転送します。これにより、次のセクションで示すように、作成者名のハッシュマップが処理されます。
フォームで可能なデータベース操作
次のスニペットでは、JavaServer Pages(JSP)を使用して、サーブレットから渡されたハッシュマップから作者名の初期データを取得し、ユーザーに表示します。このフォームでは、既存データの作成や更新も可能です。
<%@ 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>
上のスニペットでは、サーブレットから渡された作者名のハッシュマップを使用してページが読み込まれ、フォームが更新されます。このフォームでは、JavaServer Pages Standard Tag Library(JSTL)の when
と otherwise
オペレーションを使用し、サーブレットから渡されたハッシュマップに if..else
ロジックと forEach
ループを実行しています。
上のスニペットの JSP ページには、新しいブログ投稿の作成や既存の投稿の更新に使用するフォームが含まれています。このフォームは、ユーザーがブログ投稿を作成しているか更新しているかに応じて、/create
または /update
でハンドラにデータを送信できることに注意してください。
フォームの使用方法については、POST データの処理をご覧ください。
レコードを保存する
次のスニペットでは、ユーザーがフォームで送信したデータから新しいレコードを作成し、データベースに格納しています。この例では、前のセクションで説明したブログ投稿作成フォームで送信されたデータから SQL INSERT
ステートメントを作成しています。
// 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);
}
}
このコード スニペットでは、ユーザーからの入力を取得し、jSoup を実行して削除しています。jSoup と PreparedStatement
を使用することで、XSS や SQL インジェクション攻撃を回避できます。
createPostSql
変数には、PreparedStatement.set()
メソッドを使用して割り当てられる値のプレースホルダとして ?
を持つ INSERT
クエリが含まれています。
テーブル フィールドは PreparedStatement
set メソッドで参照されています。フィールドの順番に注意してください。たとえば、author_id
は INT 型のフィールドなので、author_id
を設定するには setInt()
を使用する必要があります。
レコードを取得する
次のスニペットでは、サーブレットの doGet()
メソッドがブログ投稿テーブルから行を取得し、出力しています。
// 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);
}
}
SELECT
ステートメントが ResultSet
に格納されます。これは、ResultSet.get()
メソッドが使用されるたびに繰り返し実行されます。前に定義したテーブル スキーマに対応する ResultSet.get()
メソッド getString
に注意してください。
この例では、各投稿に [Update]
リンクと [Delete]
リンクがあります。これは、投稿の更新と削除をそれぞれ開始するために使用されます。投稿の ID を難読化するため、ID が Base64
でエンコードされます。
レコードを更新する
次のスニペットでは、既存のレコードを更新しています。
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);
}
}
ユーザーがブログ投稿で [Update] リンクをクリックすると、新しい投稿を作成する JSP フォームが表示されますが、今回は既存の投稿のタイトルとコンテンツが挿入されています。作者名は変更されないため、このサンプルでは表示されません。
投稿の更新は、投稿の作成と似ていますが、INSERT
ではなく SQL UPDATE
クエリを使用します。
executeUpdate()
を実行すると、ユーザーはスニペットの確認ページにリダイレクトされます。
レコードを削除する
この例の行、ブログ投稿を削除するには、ターゲット テーブル(この例では content
)から行を削除する必要があります。各レコードは ID で識別されます。これはサンプルコードの post_id
値です。DELETE
クエリで、この ID をフィルタとして使用します。
executeUpdate()
の実行後、確認ページに移動します。
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);
}
}
投稿 ID をデコードした後に、posts
テーブルから 1 つの投稿が削除されます。
App Engine へのデプロイ
Maven を使用して App Engine にアプリをデプロイします。
プロジェクトのルート ディレクトリに移動し、次のように入力します。
mvn package appengine:deploy -Dapp.deploy.projectId=PROJECT_ID
PROJECT_ID は、Google Cloud プロジェクトの ID に置き換えます。pom.xml
ファイルですでにプロジェクト ID を指定している場合は、実行するコマンドに -Dapp.deploy.projectId
プロパティを含める必要はありません。
Maven によってアプリがデプロイされた後、次のように入力すると、新しいアプリでウェブブラウザのタブが自動的に開きます。
gcloud app browse
次のステップ
Cloud SQL は、テキストベースのデータを保存するのに便利です。ただし、画像などのリッチメディアを保存する場合は、Cloud Storage の使用を検討する必要があります。
次に、タスクキューの使い方について学びます。ここでは、このガイドでアップロードした画像のサイズを変更するため、Images API を使用するサンプルを用いて、非同期タスクを実行します。