フォーム読み込み中
2023年2月28日掲載
Google Cloud Firestore Datastoreモードは、自動スケーリングと高パフォーマンスを実現し、アプリケーション開発を簡素化するように構築された NoSQL ドキュメント データベースです。(Google Cloud公式サイトより引用)
本記事では、Google Cloud Firestore DatastoreモードとPython or Node.jsを使って、簡単なRESTful APIサービスを構築する方法をご紹介します。
Cloud Firestore Datastoreモードを使った、RESTful APIサービス作成方法としての流れ及び全体像は次の図の通りになります。
Google CloudのCloud Firestoreコンソール画面へアクセスし、新たにFirestoreリソースを作成します。チャットサービス(IM Service)を想定し、このテーブルを使用してメッセージを保存します。
Cloud FirestoreにはネイティブモードとDatastoreモードの2種類のどれかを剪定する必要があります。ネイティブモードはFirestoreの次期機能として Firebase Realtime Databaseとして利用することができる一方、Datastoreモードは昔あったGoogle Cloud Datastoreというプロダクトサービスをそのまま継承したもので、Cloud Datastoreで動作しているアプリケーションのコードやAPIを変更することなく、Firebaseで利用することができる物です。FirestoreのネイティブモードとDatastoreモードの違いについては、こちらのHelpドキュメントが参考になります。
<参考>
ネイティブ モードと Datastore モードからの選択
https://cloud.google.com/datastore/docs/firestore-or-datastore
データベースを選択します:CloudFirestoreまたはRealtimeDatabase
https://firebase.google.com/docs/database/rtdb-vs-firestore
Firestoreのモード選定画面に入ります。本記事では著者が慣れているDatastoreモードを選択します。ネイティブモードでも問題ありませんし、そこまで大差はないと思います。
配置するデータベースの場所を選択します。
データベース作成が完了すると、Firebaseの管理画面に直接入ることができます。
Cloud Firestore Datastoreモードのデータオブジェクトは、エンティティとして知られています。エンティティは、1つ以上の名前付きプロパティを持ち、各プロパティは1つ以上の値を持つことができます。
同じ種類のエンティティが同じプロパティを持つ必要はありません。また、あるプロパティのエンティティ値がすべて同じデータ型である必要はありません。
本番運用中のサービスがあり、すべてのメッセージがDatastoreに保存されていると仮定します。その場合、チャットグループ用とメッセージ用の2種類が必要です。メッセージは特定のチャットグループに属するので、チャットグループのエンティティはメッセージのエンティティの親になる必要があります。
新しい kind 名を 「ChatGroup」 と入力し、key は自動生成のままにしておきます。この種類はチャットグループに使用されます。
名前とメンバー数のようなチャットグループのプロパティを設定します。
一つのプロパティを設定し終えたら、「Done」ボタンをクリックします。
成功メッセージとともに、ターゲット・エンティティが正常に作成されます。
作成されたチャットグループエンティティのIDをクリックすると、エンティティデータの詳細が表示されます。Key literalの情報をコピーし、次のステップで使用します。
同じようにmessageという新しい種類のエンティティを作成します。今回は親をコピーされた情報に設定し、この2つのエンティティがリンクされるようにします。メッセージとして、エンティティはコンテンツや送信者などのプロパティを持つことになります。
異なる種類のエンティティを照会することができます。
特定のフィルターでクエリを実行することもできます。
一方、DatastoreはGQLによる問い合わせをサポートしています。
<参考>
GQL Reference
https://cloud.google.com/datastore/docs/reference/gql_reference
検索するプロパティに対して、シンプルインデックスまたはコンポジットインデックスがあることを確認します。また、大文字と小文字に注意します。そうしないと、データを取得することができません。
詳細ページでエンティティのプロパティを更新します。
この操作を行うと、ターゲットとなるエンティティの情報が更新されます。
詳細ページでは、削除操作にも対応しています。
「Delete」ボタンをクリックします。
ポップアップウィンドウの 「Confirm」ボタンをクリックします。
すると、対象のエンティティが削除されます。メッセージの種類のエンティティは1つだけなので、種類も同時に削除されます。
マネージドエクスポートおよびインポートサービスを使用して、Cloud Firestore DatastoreモードのエンティティをCloud Storageへエクスポートおよびインポートすることができます。これは、データのバックアップ、プロジェクト間でのデータ転送、またはBigQueryなどの他のサービスへのデータ転送に使用されます。
<参考>
Exporting and Importing Entities
https://cloud.google.com/datastore/docs/export-import-entities?hl=en
メニューから 「import/export」ページに入ります。
「Export」ボタンをクリックし、処理に入ります。
Cloud StorageのBucketのある「Browser」ボタンをクリックして、既存のBucket / folder を確認します。
特定のバケツフォルダを一つピックアップします。
「Select」ボタンをクリックして、パスを設定します。
完了すると、成功メッセージと生成されたエクスポートレコードがページに表示されます。
Cloud Storageにて選択したbucket folderを確認すると、そこに関連するデータファイルが表示されます。
Cloud Firestore Datastoreモードの操作や挙動は概ね把握できたと思うので、今度はNode.jsを使って、Cloud Firestore DatastoreモードによるRESTful APIを作ります。この構築にはCloud Firestore DatastoreモードのNode.js SDKが必要なので、Node.js SDKを事前に導入する必要があります。
Google Cloudでは、Cloud Firestore Datastoreモードの紹介ドキュメントとサンプルコードを提供しています。必要に応じてデフォルトのクレデンシャルやインデックスの設定を確認します。エラーが発生した場合は、本記事の後半に記載している、補足事項で確認いただければ幸いです。
<参考>
Cloud Firestore の Datastore モードでの使用https://cloud.google.com/appengine/docs/flexible/nodejs/using-cloud-datastore
Using Cloud Datastore
https://github.com/GoogleCloudPlatform/nodejs-docs-samples/tree/main/appengine/datastore
まずは上記Google Cloudが提供するサンプルコードを確認することから、構築してみます。以下のコマンドでサンプルコードを取得することができます。
git clone https://github.com/GoogleCloudPlatform/nodejs-docs-samples
cd nodejs-docs-samples/appengine/datastore
今回はCloud Firestore DatastoreモードでのローカルRESTful APIサーバを構築するため、コードを自分のプロジェクトフォルダにコピーし、後で更新作業を行います。
node_gcp_datastoreという新規プロジェクトフォルダを準備し、そこにpackage.jsonとapp.jsをコピーします。その後、依存関係をインストールし、npmコマンドでスクリプトを実行します。
Node.js v14.x.xを使用していない場合、Node.jsのバージョンに関する警告メッセージが表示されます。しかし、後で実行中にエラーが発生することはないので、そのまま無視して構いません。
`npm start`コマンドでサーバを起動し、ブラウザで8080番ポートのサーバにアクセスします。
スクリプトはブラウザへのアクセスを記録し、Cloud Firestore Datastoreにサイト訪問情報を新しいエンティティとして保存します。ブラウザとコンソールの両方でデータを確認することができます。
このサンプルコードでは、Node.js SDKを使用してDatastoreモードでFirestoreに接続し、動作させる方法をご紹介しています。参考までにその他の事例を確認することができます。
上述、本番運用中のサービス基盤シナリオを例にとると、ここでは既存のChatGroupエンティティが再利用されます。RESTful APIサーバを構築するために、プロジェクトフォルダ内に2つのスクリプトを用意します。
<参考>
Google Cloud Datastore: Node.js Samples
https://github.com/googleapis/nodejs-datastore/tree/main/samples
server.jsは、expressサーバーの設定とルートを定義しています。
"use strict";
const express = require('express');
const app = express();
const message = require('./message');
app.use(express.json());
app.get('/', (req, res) => {
res.end('Welcome to Datastore RESTful API demo!');
});
app.get('/api/msg', async (req, res) => {
var results = await message.listMessages();
res.json(results).end();
});
app.get('/api/msg/:group/:id', async (req, res) => {
var group = parseInt(req.params.group);
var id = parseInt(req.params.id);
var results = await message.getMessageById(group, id);
res.json(results).end();
});
app.post('/api/msg/:group', async (req, res) => {
var group = parseInt(req.params.group);
var sender = req.body.sender;
var msg = req.body.message;
var results = await message.saveMessage(group, sender, msg);
res.json(results).end();
});
app.delete('/api/msg/:group/:id', async (req, res) => {
var group = parseInt(req.params.group);
var id = parseInt(req.params.id);
var results = await message.deleteMessage(group, id);
res.json(results).end();
});
const port = process.env.PORT || 5000;
app.listen(port, () => console.log(`Listening on port ${port}`));
message.jsは、Datastoreのメッセージ・エンティティを操作するための関数を定義しています。サーバーを起動する前に、コード内でプロジェクト名を設定します。
"use strict";
const { Datastore } = require('@google-cloud/datastore');
// Instantiate a datastore client
const datastore = new Datastore({
projectId: 'xxxxxxxx',
});
/**
* List all the messages in the database
*/
async function listMessages() {
var query = datastore.createQuery('Message').order('timestamp', { descending: true }).limit(10);
var results = await datastore.runQuery(query);
return { "Status": "Success", "results": "Get target messages successfully.", "data": results };
}
/**
* Get target message with specific message id
*/
async function getMessageById(group, id) {
// var query = datastore.createQuery('Message').filter('id', '=', id);
// var results = await datastore.runQuery(query);
var message = datastore.key(['ChatGroup', group, 'Message', id]);
var results = await datastore.get(message);
if (null == results[0]) {
return { "Status": "Failed", "results": "No message under with the conditions.", "data": null };
} else {
return { "Status": "Success", "results": "Get target messages successfully.", "data": results[0] };
}
}
/**
* Save message with passed data
*/
async function saveMessage(group, sender, message) {
// var groupKey = datastore.key(['ChatGroup', group]);
var results = await datastore.save({
key: datastore.key(['ChatGroup', group, 'Message']),
data: {
sender: sender,
message: message,
timestamp: new Date(),
},
});
return { "Status": "Success", "results": "Save target message successfully.", "data": results };
}
/**
* Delete target message with specific message id
*/
async function deleteMessage(group, id) {
var message = datastore.key(['ChatGroup', group, 'Message', id]);
var results = await datastore.delete(message);
if (0 == results[0].indexUpdates) {
return { "Status": "Failed", "results": "Target message does not exist.", indexUpdates: results[0].indexUpdates };
} else {
return { "Status": "Success", "results": "Delete target message successfully.", indexUpdates: results[0].indexUpdates };
}
}
module.exports = { listMessages, getMessageById, saveMessage, deleteMessage }
スクリプトの起動設定にあるpackage.jsonを更新すると、server.jsからスクリプトが起動するようになります。
……
"scripts": {
"start": "node server.js",
"system-test": "mocha --exit test/*.test.js",
"test": "npm run system-test"
},
"dependencies": {
"@google-cloud/datastore": "^7.0.0",
"express": "^4.16.4"
},
……
最終的なプロジェクトの構成はこんな感じです。
.
├── node_modules
├── app.js
├── message.js
├── package.json
└── server.js
ここまで、コードら実行ファイルの準備ができたら、サーバーを起動し、RESTful APIをテストします。`npm start` コマンドを実行し、ポート5000で express サーバーを起動します。
`curl`コマンドでRESTful APIをテストします。冒頭のメッセージが無事取得できればOKです。
curl -X GET http://localhost:5000/
以下のコマンドでChatGroup IDとMessage IDを自身で更新してから、テストします。
Cloud Firestore Datastoreモードに新しいメッセージエンティティを保存します。
curl -H "Content-Type: application/json" -X POST -d "{\"sender\":\"bob\",\"message\":\"Testing message from Node.js .\"}" http://localhost:5000/api/msg/5634161670881280
IDの付いたメッセージデータを確認します。IDの値は、挿入APIやコンソールから取得することができます。コマンドを実行する前に変更します。
curl -X GET http://localhost:5000/api/msg/5634161670881280/5714489739575296
特定のIDを持つ対象エンティティが見つからない場合は、エラーメッセージと空のデータが表示されます。
メッセージの上位10件をリストアップし、タイムスタンプの値で並べます。
curl -X GET http://localhost:5000/api/msg
特定のIDのメッセージを削除してから、再度データの取得を試みます。
curl -X DELETE http://localhost:5000/api/msg/5634161670881280/5714489739575296
curl -X GET http://localhost:5000/api/msg/5634161670881280/5714489739575296
対象となるメッセージのエンティティが存在しない場合、削除関数はいくつかのエラーメッセージを返します。
Node.js によるRESTful APIは構築できたので、今度はPythonとflaskパッケージを使ってRESTful APIを作ってみます。この構築にはNode.js SDKと同様にCloud Firestore DatastoreモードのPython SDKが必要なので、Cloud Firestore Datastoreモード Python SDKを事前に導入する必要があります。Cloud Firestore Datastoreモードは、Python SDKを含む、複数のプログラミング言語用のSDKを提供しています。
<参考>
Cloud Firestore の Datastore モードでの使用https://cloud.google.com/appengine/docs/flexible/python/using-cloud-datastore
Python Google Cloud Datastore sample for Google App Engine Flexible Environment
https://github.com/GoogleCloudPlatform/python-docs-samples/tree/main/appengine/flexible/datastore
python_gcp_datastoreという仮想環境によるプロジェクトフォルダを準備し、以下のコマンドで仮想環境を起動します。
py -m venv .venv
.venv\scripts\activate
必要な依存パッケージをインストールし、requirements.txt に情報を保存します。
pip install google-cloud-datastore flask-restful
pip freeze > requirements.txt
その他、Python SDK の使用方法に関するサンプルもあります。ローカルの RESTful API サーバーを構築するために、サンプルコードを元にプロジェクトフォルダに2つのスクリプトを追加します。
<参考>
Python Client for Google Cloud Datastore API
https://github.com/googleapis/python-datastore/tree/main/samples/snippets
server.py は flask サーバーの設定とルートを定義しています。
from flask import Flask, jsonify
from flask_restful import Api, Resource, reqparse
from datastore import DatastoreManager
class Message(Resource):
def __init__(self):
self.manager = DatastoreManager()
super().__init__()
def get(self, group, id):
return jsonify(self.manager.read_message_by_id(int(group), int(id)))
def delete(self, group, id):
return jsonify(self.manager.delete_message_by_id(int(group), int(id)))
class Messages(Resource):
def __init__(self):
self.manager = DatastoreManager()
super().__init__()
def get(self, group):
return jsonify(self.manager.read_all_messages(int(group)))
def post(self, group):
args = reqparse.RequestParser() \
.add_argument('sender', type=str, location='json', required=True, help="Empty sender") \
.add_argument('message', type=str, location='json', required=True, help="Empty message") \
.parse_args()
return jsonify(self.manager.save_message(int(group), args['sender'], args['message']))
app = Flask(__name__)
api = Api(app, default_mediatype="application/json")
api.add_resource(Message, '/api/msg/<group>/<id>')
api.add_resource(Messages, '/api/msg/<group>')
app.run(host='0.0.0.0', port=5001, use_reloader=True)
datastore.py は、Google Cloud Datastore サービスと通信するための関数を定義しています。プロジェクト名を忘れずに設定します。
from google.cloud import datastore
import datetime
class DatastoreManager:
def __init__(self):
self.ds = datastore.Client(project='xxxxxxxx')
def save_message(self, group, sender, message):
entity = datastore.Entity(key=self.ds.key('ChatGroup', group, 'Message'))
entity.update({
'sender': sender,
'message': message,
'timestamp': datetime.datetime.now(tz=datetime.timezone.utc)
})
result = self.ds.put(entity)
return {"status": "Success", "results": "Save the target message successfully.", "data": result}
def read_all_messages(self, group):
groupKey = self.ds.key('ChatGroup', group)
msgQuery = self.ds.query(kind='Message', ancestor=groupKey, order=('-timestamp',))
result = list(msgQuery.fetch(limit=10))
return {"status": "Success", "results": "Get the top 10 messages successfully.", "data": result}
def read_message_by_id(self, group, id):
msgKey = self.ds.key('ChatGroup', group, 'Message', id)
result = self.ds.get(msgKey)
if None == result:
return {"status": "Failed", "errMessage": "No target data row."}
else:
return {"status": "Success", "results": "Get the target message successfully.", "data": result}
def delete_message_by_id(self, group, id):
msgKey = self.ds.key('ChatGroup', group, 'Message', id)
result = self.ds.delete(msgKey)
return {"status": "Success", "results": "Delete the target message successfully."}
これで、プロジェクトフォルダは次のようになります。
.
├── .venv
├── datastore.py
├── requirements.txt
└── server.py
ここまで、コードら実行ファイルの準備ができたら、サーバーを起動し、RESTful APIをテストします。`python server.py`コマンドを実行し、flask サーバを起動します。
curl コマンドで API 経由でテーブルに新しいメッセージを挿入できるかテストします。Node.js RESTful APIサーバーと同様、curlコマンドのチャットグループエンティティとメッセージエンティティのIDは、ご自身の環境に合わせて更新します。
既存のチャットグループ・エンティティの下に、新しいメッセージ・エンティティをDatastoreに挿入します。
curl -H "Content-Type: application/json" -X POST -d "{\"sender\":\"bob\",\"message\":\"Testing message from Python.\"}" http://localhost:5001/api/msg/5634161670881280
特定のメッセージIDを持つメッセージのエンティティを確認します。
curl -X GET http://localhost:5001/api/msg/5634161670881280/5635008819625984
特定のidのメッセージが存在しない場合は、エラーメッセージと空のデータが表示されます。
curl -X GET http://localhost:5001/api/msg/5634161670881280/563500881962598411
同じチャットグループ内のメッセージの上位10件をチェックし、タイムスタンプでデータを並べます。
curl -X GET http://localhost:5001/api/msg/5634161670881280
対象のメッセージを削除してから、再度メッセージを確認します。
curl -X DELETE http://localhost:5001/api/msg/5634161670881280/5665673409724416
curl -X GET http://localhost:5001/api/msg/5634161670881280/5665673409724416
以下のエラーは、デフォルトの認証情報がないために発生します。
開発用ワークステーションなどのローカルな開発環境でコードを実行する場合は、 Google アカウントに関連付けられた認証情報 (ユーザー認証情報とも呼ばれます) を使用するのが最良の方法です。
その後、デフォルトの認証情報はローカルに application_default_credentials.json として保存されます。このファイルはデフォルトの認証情報として読み込まれ、上記のエラーを修正します。参考までに詳細をご確認ください。
<参考>
アプリケーションのデフォルト認証情報に認証情報を提供する
https://cloud.google.com/docs/authentication/provide-credentials-adc#local-dev
エンティティキーは、エンティティを区別するために使用されます。エンティティを操作する前にキーを定義しておく必要があります。エンティティに親情報がある場合、キーに追加されます。
本記事では、Node.js SDKのdatastore.keyオブジェクトの構築関数の説明と例を示します。
Pythonなど他のプログラミング言語でも同様の機能があります。また、SDKのサンプルや紹介文、ヘルプドキュメントもご参照ください。
<参考>
エンティティ、プロパティ、キーhttps://cloud.google.com/datastore/docs/concepts/entities
/**
* Helper to create a Key object, scoped to the instance's namespace by
* default.
*
* You may also specify a configuration object to define a namespace and path.
*
* @param {object|string|array} [options] Key path. To specify or override a namespace,
* you must use an object here to explicitly state it.
* @param {string|array} [options.path] Key path.
* @param {string} [options.namespace] Optional namespace.
* @returns {Key} A newly created Key from the options given.
*
* @example
* ```
* <caption>Create an incomplete key with a kind value of `Company`.
* Since no Id is supplied, Datastore will generate one on save.</caption>
* const {Datastore} = require('@google-cloud/datastore');
* const datastore = new Datastore();
* const key = datastore.key('Company');
*
* ```
* @example
* ```
* <caption>Create a complete key with a kind value of `Company` and Id `123`.</caption>
* const {Datastore} = require('@google-cloud/datastore');
* const datastore = new Datastore();
* const key = datastore.key(['Company', 123]);
*
* ```
* @example
* ```
* <caption>If the ID integer is outside the bounds of a JavaScript Number
* object, create an Int.</caption>
* const {Datastore} = require('@google-cloud/datastore');
* const datastore = new Datastore();
* const key = datastore.key([
* 'Company',
* datastore.int('100000000000001234')
* ]);
*
* ```
* @example
* ```
* <caption>Create a complete key with a kind value of `Company` and name `Google`.
* Because the supplied Id is a string, Datastore will prefix it with "name=".
* Had the supplied Id been numeric, Datastore would prefix it with the standard, "id=".</caption>
* const {Datastore} = require('@google-cloud/datastore');
* const datastore = new Datastore();
* const key = datastore.key(['Company', 'Google']);
*
* ```
* @example
* ```
* <caption>Create a complete key from a provided namespace and path.</caption>
* const {Datastore} = require('@google-cloud/datastore');
* const datastore = new Datastore();
* const key = datastore.key({
* namespace: 'My-NS',
* path: ['Company', 123]
* });
*
* ```
* @example
* ```
* <caption>Create a complete key that specifies an ancestor. This will create a Team entity
* with a name of "Datastore", which belongs to the Company with the "name=Google" key.</caption>
* const {Datastore} = require('@google-cloud/datastore');
* const datastore = new Datastore();
* const key = datastore.key(['Company', 'Google', 'Team', 'Datastore']);
*
* ```
* @example
* ```
* <caption>Create a incomplete key that specifies an ancestor. This will create an Employee entity
* with an auto-generated Id, which belongs to the Company with the "name=Google" key.</caption>
* const {Datastore} = require('@google-cloud/datastore');
* const datastore = new Datastore();
* const key = datastore.key(['Company', 'Google', 'Employee']);
* ```
*/
「ValueError: Key must be complete」のようなエラーメッセージが出る場合、これはエンティティキーの構築関数に起因するものです。この場合、いくつかの値が欠落している可能性があります。必要な形式に基づいてパラメータを正しい値に更新すると、エラーは解決します。
時には、特定の親エンティティの下でクエリを実行する必要があります。上で開発した本番運用中サービスのデモを例にとると、最も一般的なケースは、特定のチャットグループからのメッセージを検索することです。そのため、同じチャットグループのエンティティを親として持つエンティティからクエリを実行する必要があります。
この場合、指定されたエンティティとその子孫に結果を限定する祖先クエリを使用する必要があります。
<参考>
データストアのクエリ#祖先クエリhttps://cloud.google.com/datastore/docs/concepts/queries#ancestor_queries
from google.cloud import datastore
# For help authenticating your client, visit
# https://cloud.google.com/docs/authentication/getting-started
client = datastore.Client()
# Query filters are omitted in this example as any ancestor queries with a
# non-key filter require a composite index.
ancestor = client.key("TaskList", "default")
query = client.query(kind="Task", ancestor=ancestor)
クエリにマッチするインデックスがない場合、以下のようなエラーメッセージが表示されます。また、推奨されるインデックスの設定も表示されるので、それを直接利用することもできます。
google.api_core.exceptions.FailedPrecondition: 400 no matching index found. recommended index is:
- kind: Message
ancestor: yes
properties:
- name: timestamp
direction: desc
プロジェクトフォルダにindex.yamlを用意し、それを元にgcloud CLIコマンドでインデックスを作成することができます。詳細は参考までにご確認ください。
<参考>
インデックス
https://cloud.google.com/datastore/docs/concepts/indexes
indexes:
- kind: Message
ancestor: yes
properties:
- name: timestamp
direction: desc
インデックスの設定ファイル名はindex.yamlでなければならないことに注意します。そうでない場合、いくつかのエラーが発生します。
また、インデックスの情報はコンソールからdatastore > indexesで確認することができました。インデックスの構築には多少時間がかかります。
この作業を行うと、インデックスのステータスが「Serving」に変更されるはずです。
インデックス作成処理が完了する前にクエリを実行した場合、関連するエラーメッセージが表示されます。
google.api_core.exceptions.FailedPrecondition: 400 The index for this query is not ready to serve. See the Datastore Indexes page in the Admin Console.
ローカル構成に基づいて、使用されていないDatastoreのインデックスを削除することができます。これを実現するために
ローカルのindex.yamlからインデックスの設定を削除します。
からインデックス設定を削除し、それを使って `gcloud datastore indexes cleanup` コマンドを実行します。
削除操作の後、未使用のインデックスはDatastoreから削除されます。
Cloud Firestore DatastroreモードとNode.js もしくは Pythonで始めるRESTful APIサービス構築方法をご紹介しました。この方法は初心者向け記事としても、チャットサービス(IM Service)向けにも適用できますし、10分もせずにRESTful APIサービスを完成することができます。
Cloud Firestore は NoSQLデータベースとして、Webやモバイルなどのアプリケーションから簡単にアクセスできるよう、Node.jsやPythonなど幅広い言語でのクライアントを提供しています。そのため、どんな言語でも通信プロトコルを意識せずにRESTful APIサービスとして展開することができます。RESTful APIサービスを構築してみたい方は是非参考にしてみるといいでしょう。
条件に該当するページがございません