Linux Professional Institute Learning Logo.
main contentにスキップ
  • ホーム
    • 全てのリソース
    • LPI学習教材
    • コントリビューターになる
    • Publishing Partners
    • Publishing Partnerになる
    • About
    • FAQ
    • コントリビューター
    • Roadmap
    • 連絡先
  • LPI.org
035.3 レッスン 1
課題 031: ソフトウェア開発とWebテクノロジー
031.1 ソフトウェア開発の基本
  • 031.1 レッスン 1
031.2 Webアプリケーションのアーキテクチャ
  • 031.2 レッスン 1
031.3 HTTP の基礎
  • 031.3 レッスン 1
課題 032: HTMLドキュメントマークアップ
032.1 HTMLドキュメントの構造
  • 032.1 レッスン 1
032.2 HTMLのセマンティックスとドキュメント階層
  • 032.2 レッスン 1
032.3 HTMLにおける参照と埋め込みリソース
  • 032.3 レッスン 1
032.4 HTMLフォーム
  • 032.4 レッスン 1
課題 033: CSSコンテンツ スタイリング
033.1 CSSの基本
  • 033.1 レッスン 1
033.2 CSSセレクターとスタイルアプリケーション
  • 033.2 レッスン 1
033.3 CSSスタイリング
  • 033.3 レッスン 1
033.4 CSSボックスモデルとレイアウト
  • 033.4 レッスン 1
課題 034: JavaScriptプログラミング
034.1 JavaScriptの実行と構文
  • 034.1 レッスン 1
034.2 JavaScriptデータ構造
  • 034.2 レッスン 1
034.3 JavaScriptの制御構造と関数
  • 034.3 レッスン 1
  • 034.3 レッスン 2
034.4 WebサイトのコンテンツとスタイリングのJavaScript操作
  • 034.4 レッスン 1
課題 035: NodeJSサーバープログラミング
035.1 NodeJSの基本
  • 035.1 レッスン 1
035.2 NodeJS Expressの基本
  • 035.2 レッスン 1
  • 035.2 レッスン 2
035.3 SQLの基本
  • 035.3 レッスン 1
How to get certified
  1. 課題 035: NodeJSサーバープログラミング
  2. 035.3 SQLの基本
  3. 035.3 レッスン 1

035.3 レッスン 1

Certificate:

Web開発の要点

Version:

1.0

Topic:

035 NodeJSサーバプログラミング

Objective:

035.3 SQLの基本

Lesson:

1 of 1

はじめに

独自の関数を書いて永続的なストレージを実装することもできますが、データベース管理システムを使用した方が、開発のスピードアップやテーブル形式のデータのセキュリティと安定性の確保につながる場合があります。相互に関連したテーブルで構成されたデータを保存するための最も一般的な戦略は、特にそれらのテーブルが頻繁にクエリおよび更新される場合には、 Structured Query Language (SQL)をサポートするリレーショナルデータベースをインストールすることです。Node.jsは様々なSQLデータベース管理システムをサポートしています。Node.js Expressで採用されている移植性とユーザースペースでの実行という原則に従うと、この種のHTTPサーバで使用されるデータの永続的な保存にはSQLiteが適切な選択肢となります。

SQL

データベースに特化した言語として、Structured Query Languageがあります。書き込みや読み出しの操作は、 ステートメント や クエリ と呼ばれる文で表現されます。ステートメントもクエリも、操作を実行するための条件を定義した 句 で構成されています。

例えば、名前やメールアドレスは、 name と email のフィールドを含むデータベースのテーブルに格納することができます。データベースには複数のテーブルを含めることができるので、各テーブルには固有の名前を付ける必要があります。名前とメールアドレスのテーブルに contacts という名前を使うと、以下の ステートメント で新しいレコードを挿入することができます。

INSERT INTO contacts (name, email) VALUES ("Carol", "carol@example.com");

この挿入文は、データが挿入されるテーブルとフィールドを定義する INSERT INTO 句で構成されます。2番目の句である VALUES は、挿入される値を設定します。句を大文字にする必要はありませんが、ステートメントやクエリ内のSQLキーワードを認識しやすくするために、一般的には大文字にします。

contactsテーブルへのクエリは、 SELECT 句を使って同様に行います。

SELECT email FROM contacts;
dave@example.com
carol@example.com

この例では、 SELECT email 句は、 contacts テーブルのエントリから1つのフィールドを選択します。また、 WHERE 句はクエリを特定の行に制限します。

SELECT email FROM contacts WHERE name = "Dave";
dave@example.com

SQLには他にも多くの句があり、後のセクションでそのいくつかを見ていきます。しかしその前に、SQLデータベースをNode.jsと統合する方法を確認する必要があります。

SQLite

SQLiteは、SQLデータベースの機能をアプリケーションに組み込むための、おそらく最もシンプルなソリューションです。他の一般的なデータベース管理システムとは異なり、SQLiteはクライアントが接続するデータベースサーバではありません。代わりに、SQLite は、開発者が従来のファイルのようにデータベースを作成するための一連の関数を提供します。Node.js Expressで実装されたHTTPサーバの場合、このファイルは通常、サーバスクリプトと同じディレクトリに置かれます。

Node.jsでSQLiteを使う前に、 sqlite3 モジュールをインストールする必要があります。サーバのインストールディレクトリ、つまり実行するNode.jsスクリプトがあるディレクトリで、以下のコマンドを実行します。

$ npm install sqlite3

SQLiteをサポートするモジュールには、 better-sqlite3 などいくつかありますが、その使い方は sqlite3 とは微妙に異なることに注意してください。このレッスンの例は sqlite3 モジュールを対象としていますので、他のモジュールを選択した場合は期待通りに動作しない可能性があります。

データベースを開く

Node.js ExpressサーバがSQLデータベースとどのように連携するかを示すために、クッキーで識別されたクライアントが送信したメッセージを保存・表示するスクリプトを書いてみましょう。メッセージはHTTP POSTメソッドでクライアントから送信され、サーバのレスポンスは、クライアントから要求されたフォーマットに応じて、JSONまたはHTML(テンプレートから)としてフォーマットされます。このレッスンでは、HTTPメソッド、クッキー、テンプレートの使い方については詳しく説明しません。ここで紹介するコードスニペットは、これらの機能が設定され、利用可能なNode.js Expressサーバがすでにあることを前提としています。

クライアントが送信したメッセージを保存する最も簡単な方法は、以前に送信した各メッセージを、各クライアントに固有の識別キーと関連付けて、グローバル配列に保存することです。このキーは、クッキーとしてクライアントに送信することができ、今後、以前のメッセージを取得するためのリクエストの際に、サーバに提示されます。

しかし、この方法には弱点があります。メッセージはグローバル配列にのみ保存されるため、現在のサーバセッションが終了すると、すべてのメッセージが失われてしまいます。これは、データが永続的に保存され、サーバが再起動されても失われないという、データベースを使用する利点のひとつです。

index.js ファイルをメインのサーバスクリプトとして、 sqlite3 モジュールを組み込んで、データベースとなるファイルを以下のように指示します。

const sqlite3 = require('sqlite3')
const db = new sqlite3.Database('messages.sqlite3');

まだ存在していない場合は、 index.js ファイルと同じディレクトリに messages.sqlite3 ファイルが作成されます。この1つのファイルの中に、すべての構造体とそれぞれのデータが格納されます。スクリプトで実行されるすべてのデータベース操作は、 messages.sqlite3 ファイルを開く新しい sqlite3 オブジェクトに与えられる名前である db 定数によって仲介されます。

テーブルの構造

少なくとも1つのテーブルが作成されるまでは、データベースにデータを挿入することはできません。テーブルは CREATE TABLE というステートメントで作成されます。

db.run('CREATE TABLE IF NOT EXISTS messages (id INTEGER PRIMARY KEY AUTOINCREMENT, uuid CHAR(36), message TEXT)')

データベース内でSQL文を実行するには、 db.run() メソッドを使います。ステートメント自体はメソッドのパラメータとして書かれます。SQL文をコマンドラインプロセッサで入力する際にはセミコロンで終わらせなければなりませんが、プログラム内でパラメータとして渡されるステートメントではセミコロンは省略可能です。

run メソッドは、スクリプトが node index.js で実行されるたびに実行されるので、SQL文には条件句 IF NOT EXISTS が含まれており、将来の実行時に messages テーブルがすでに存在している場合にエラーが発生しないようになっています。

messages テーブルを構成するフィールドは、 id 、 uuid 、および message です。 id フィールドはテーブルの各エントリを識別するためのユニークな整数なので、 PRIMARY KEY として作成されます。主キーはNULLにすることはできませんし、同じテーブルに同じ主キーが2つ存在することもありません。したがって、ほとんどすべてのSQLテーブルは、テーブルの内容を追跡するために主キーを持っています。新しいレコードの主キーの値を明示的に選択することは可能ですが(テーブル内にまだ存在していない場合に限ります)、キーは自動的に生成される方が便利です。この目的のためには、 id フィールドの AUTOINCREMENT フラグを使用します。

Note

SQLiteで主キーを明示的に設定することはオプションです。なぜなら、SQLite自身が主キーを自動的に作成するからです。SQLiteのドキュメントには次のように書かれています。: “SQLiteでは、テーブルの行は通常、64ビットの符号付き整数 ROWID を持ち、これは同じテーブルのすべての行の中で一意です。テーブルに INTEGER PRIMARY KEY 型のカラムがある場合、そのカラムは ROWID のエイリアスになります。その後、4つの異なる名前のいずれかを使用して ROWID にアクセスできます。上記のオリジナルの3つの名前、または INTEGER PRIMARY KEY カラムに与えられた名前です。これらの名前はすべてお互いのエイリアスであり、どのようなコンテキストでも同じように機能します。”

uuid と message のフィールドには、それぞれクライアントの識別情報とメッセージの内容が格納されます。 CHAR(36) 型のフィールドには36文字の固定長のテキストが格納され、 TEXT 型のフィールドには任意の長さのテキストが格納されます。

データ入力

このサンプルサーバの主な機能は、メッセージを送信したクライアントにリンクしたメッセージを保存することです。クライアントは、HTTP POSTメソッドで送信したリクエストのボディにある message フィールドにメッセージを送信します。クライアントの識別情報は、 uuid というクッキーに入っています。この情報をもとに、データベースに新しいメッセージを挿入するためのExpressルートを記述します。

app.post('/', (req, res) => {

  let uuid = req.cookies.uuid

  if ( uuid === undefined )
    uuid = uuidv4()

  // Insert new message into the database
  db.run('INSERT INTO messages (uuid, message) VALUES (?, ?)', uuid, req.body.message)

  // If an error occurrs, err object contains the error message.
  db.all('SELECT id, message FROM messages WHERE uuid = ?', uuid, (err, rows) => {

    let expires = new Date(Date.now());
    expires.setDate(expires.getDate() + 30);
    res.cookie('uuid', uuid, { expires: expires })

    if ( req.headers.accept == "application/json" )
      res.json(rows)
    else
      res.render('index', {title: "My messages", rows: rows})

  })

})

今回、 db.run() メソッドは挿入文を実行しますが、 uuid と req.body.message はステートメントの行に直接書かれていないことに注意してください。その代わり、クエスチョンマークが値の代わりになっています。各クエスチョンマークは、 db.run() メソッド内のSQL文に続くパラメータに対応しています。

データベースで実行されるステートメントの中でプレースホルダーとしてクエスチョンマークを使うと、SQLiteはステートメントの静的な要素とその変数データを簡単に区別することができます。この戦略により、SQLiteはステートメントの一部である変数の内容を エスケープ または サニタイズ することができ、 SQLインジェクション と呼ばれる一般的なセキュリティ侵害を防ぐことができます。この攻撃では、悪意のあるユーザーがSQL文を変数データに挿入し、その文が不用意に実行されることを期待します。サニタイズは、データ内の危険な文字を無効にすることで、この攻撃を阻止します。

クエリ

サンプルコードに示されているように、私たちの意図は、新しいメッセージをデータベースに挿入し、以前に送信されたメッセージのリストを生成するために、同じルートを使用することです。 db.all() メソッドは、クエリで定義された基準にマッチするテーブルのすべてのエントリのコレクションを返します。

db.run() とは異なり、 db.all() では、最後のパラメータで指定された矢印の関数で処理されるレコードのリストが生成されます。

(err, rows) => {}

この関数は、2つのパラメータを受け取ります。 err と rows です。 err パラメータは、クエリの実行を妨げるようなエラーが発生した場合に使用されます。成功すると、すべてのレコードが rows の配列に格納されます。この配列の各要素は、テーブルの1つのレコードに対応するオブジェクトです。このオブジェクトのプロパティは、クエリで指定されたフィールド名に対応しています。 uuid と message です。

rows の配列は、JavaScriptのデータ構造です。そのため、Expressが提供するメソッド( res.json() や res.render() など)を使ってレスポンスを生成する際に使用できます。EJSのテンプレート内でレンダリングすると、従来のループですべてのレコードをリストアップすることができます。

<ul>
<% rows.forEach( (row) => { %>
<li><strong><%= row.id %></strong>: <%= row.message %></li>
<% }) %>
</ul>

クエリが返したすべてのレコードを rows 配列に入れる代わりに、 db.each() メソッドで各レコードを個別に処理したほうが便利な場合もあります。 db.each() メソッドの構文は db.all() メソッドと似ていますが、 (err, row) ⇒ {} の row パラメータは一度にひとつのレコードにマッチします。

データベースの内容の変更

これまでのところ、クライアントはサーバ上のメッセージを追加したり、問い合わせたりすることしかできません。クライアントは以前に送信したメッセージの id を知っているので、特定のレコードを修正する関数を実装することができます。修正したメッセージは、HTTP POSTメソッドのルートに送ることもできますが、今回はリクエストパスにクライアントが与えた id をキャッチするルートパラメータを付けます。

app.post('/:id', (req, res) => {
  let uuid = req.cookies.uuid

  if ( uuid === undefined ){
    uuid = uuidv4()
    // 401 Unauthorized
    res.sendStatus(401)
  }
  else {

    // Update the stored message
    // using named parameters
    let param = {
      $message: req.body.message,
      $id: req.params.id,
      $uuid: uuid
    }

    db.run('UPDATE messages SET message = $message WHERE id = $id AND uuid = $uuid', param, function(err){

      if ( this.changes > 0 )
      {
        // A 204 (No Content) status code means the action has
        // been enacted and no further information is to be supplied.
        res.sendStatus(204)
      }
      else
        res.sendStatus(404)

    })
  }
})

このルートでは、既存のレコードを修正するために UPDATE と WHERE 句を使用する方法を説明します。これまでの例との重要な違いは、 名前付きパラメータ を使用していることです。ここでは、値をひとつのオブジェクト ( param ) にまとめて、それぞれの値を指定する代わりに db.run() メソッドに渡しています。この場合、フィールド名(前に $ が付いている)がオブジェクトのプロパティになります。名前付きパラメータでは、クエスチョンマークの代わりにフィールド名(前に $ が付いている)をプレースホルダーとして使用できます。

例題のようなステートメントは、 WHERE 句によって課された条件がテーブルのいくつかのレコードにマッチしなかった場合、データベースに修正を加えることはありません。ステートメントによってレコードが変更されたかどうかを評価するには、 db.run() メソッドの最後のパラメータとしてコールバック関数を使用します。この関数内では、変更されたレコードの数を this.changes から取得することができます。この場合、アロー関数は使用できないことに注意してください。なぜなら、 function(){} という形式の通常の関数のみが この オブジェクトを定義するからです。

レコードを削除する方法は、修正する方法と非常によく似ています。例えば、削除するメッセージを特定するために、引き続き :id ルートパラメータを使用することができますが、今回は、クライアントの HTTP DELETEメソッドによって呼び出されるルートで次のようになります。

app.delete('/:id', (req, res) => {
  let uuid = req.cookies.uuid

  if ( uuid === undefined ){
    uuid = uuidv4()
    res.sendStatus(401)
  }
  else {
    // Named parameters
    let param = {
    $id: req.params.id,
    $uuid: uuid
    }

    db.run('DELETE FROM messages WHERE id = $id AND uuid = $uuid', param, function(err){
      if ( this.changes > 0 )
        res.sendStatus(204)
      else
        res.sendStatus(404)
    })
  }
})

テーブルからレコードを削除するには、 DELETE FROM 句を使用します。今回もコールバック関数を使って、テーブルから何件のエントリーが削除されたかを評価しました。

データベースを閉じる

一度定義すると、スクリプトの実行中にいつでも db オブジェクトを参照することができます。なぜなら、データベースファイルは現在のセッション中ずっと開いたままだからです。スクリプトの実行中にデータベースを閉じることは一般的ではありません。

しかし、サーバプロセスの終了時に突然データベースを閉じてしまわないように、データベースを閉じる機能があると便利です。可能性は低いですが、データベースを突然シャットダウンすると、インメモリデータがまだファイルにコミットされていない場合、不整合が発生する可能性があります。たとえば、ユーザーが Ctrl+C キーボードショートカットを押してスクリプトを終了させた場合、データ損失を伴う突然のデータベースシャットダウンが発生する可能性があります。

先ほどのCtrl+Cのシナリオでは、 process.on() メソッドがオペレーティングシステムから送られてくるシグナルを傍受し、データベースとサーバの両方を秩序立ててシャットダウンすることができます。

process.on('SIGINT', () => {
  db.close()
  server.close()
  console.log('HTTP server closed')
})

Ctrl+Cショートカットは、OSの SIGINT シグナルを呼び出し、ターミナルのフォアグラウンドプログラムを終了させます。 SIGINT シグナルを受信してプロセスを終了する前に、システムはコールバック関数( process.on() メソッドの最後のパラメータ)を呼び出します。コールバック関数の内部には、データベースを閉じるための db.close() メソッドや、Expressのインスタンス自体を潔く閉じるための server.close() など、任意のクリーンアップコードを配置することができます。

演習

  1. SQLデータベースのテーブルにおける主キーの目的は何ですか?

  2. db.all() と db.each() の違いは何ですか?

  3. プレースホルダーを使用し、クライアントから送られてきたデータをSQL文やクエリに直接含めないことが重要なのはなぜですか?

発展演習

  1. sqlite3 モジュールのどのメソッドを使えば、クエリが複数のエントリにマッチしても、テーブルのエントリを1つだけ返すことができますか?

  2. コールバック関数のパラメータとして渡された rows 配列に、 db.all() で作成したクエリの結果が含まれているとします。 rows の最初の位置にある price というフィールドをコールバック関数内で参照するにはどうすればよいでしょうか?

  3. db.run() メソッドは、 INSERT INTO などのデータベース修正文を実行します。テーブルに新しいレコードを挿入した後、新しく挿入されたレコードの主キーを取得するにはどうすればよいでしょうか。

まとめ

このレッスンでは、Node.js Expressアプリケーション内でのSQLデータベースの基本的な使用方法について説明しました。 sqlite3 モジュールは、SQLiteデータベースに永続的なデータを保存するシンプルな方法を提供します。この方法では、1つのファイルにデータベース全体が格納され、専用のデータベースサーバを必要としません。このレッスンでは、以下の概念と手順を説明しました。

  • Node.jsからデータベース接続を確立する方法

  • シンプルなテーブルを作成する方法と主キーの役割

  • スクリプトの中で新しいデータを追加するための INSERT INTO SQL 文の使用

  • 標準的なSQLiteのメソッドとコールバック関数を使用したSQLクエリ

  • UPDATE および DELETE SQLステートメントを使用したデータベース内のデータの変更

演習の回答

  1. SQLデータベースのテーブルにおける主キーの目的は何ですか?

    主キーとは、データベーステーブル内の各レコードを一意に識別するためのフィールドです。

  2. db.all() と db.each() の違いは何ですか?

    db.all() メソッドは、クエリに対応するすべてのエントリを含む単一の配列を用いてコールバック関数を呼び出します。 db.each() メソッドは、各結果行に対してコールバック関数を呼び出します。

  3. プレースホルダーを使用し、クライアントから送られてきたデータをSQL文やクエリに直接含めないことが重要なのはなぜですか?

    プレースホルダーを使用すると、ユーザーが送信したデータは、クエリやステートメントに含まれる前にエスケープされます。これにより、データベース上で任意の操作を行おうとして変数データの中にSQL文を入れる、SQLインジェクション攻撃を防ぐことができます。

発展演習の回答

  1. sqlite3 モジュールのどのメソッドを使えば、クエリが複数のエントリにマッチしても、テーブルのエントリを1つだけ返すことができますか?

    db.get() メソッドは、 db.all() と同じ構文ですが、クエリに対応する最初のエントリのみを返します。

  2. コールバック関数のパラメータとして渡された rows 配列に、 db.all() で作成したクエリの結果が含まれているとします。 rows の最初の位置にある price というフィールドをコールバック関数内で参照するにはどうすればよいでしょうか?

    rows の各項目はオブジェクトで、そのプロパティはデータベースのフィールド名に対応しています。ですから、最初の結果の price フィールドの値は rows[0].price になります。

  3. db.run() メソッドは、 INSERT INTO などのデータベース修正文を実行します。テーブルに新しいレコードを挿入した後、新しく挿入されたレコードの主キーを取得するにはどうすればよいでしょうか。

    db.run() メソッドのコールバック関数としては、 function(){} という形式の正規関数が使えます。この関数の内部では、 this.lastID プロパティに、最後に挿入されたレコードの主キーの値が格納されます。

Linux Professional Insitute Inc. All rights reserved. 学習資料をご覧ください: https://learning.lpi.org
ここでの作成物は、Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International Licenseの下でライセンスされています。

Linux Professional Insitute Inc. All rights reserved. 学習資料をご覧ください: https://learning.lpi.org
ここでの作成物は、Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International Licenseの下でライセンスされています。

LPIは非営利団体です。

© 2023 Linux Professional Institute(LPI)は、オープンソースプロフェッショナル向けのグローバルな認定基準およびキャリアサポート組織です。200,000人以上の認定保持者を擁する、世界初かつ最大のベンダー中立Linuxおよびオープンソース認定機関です。LPIは180か国以上で認定プロフェッショナルを擁し、複数の言語で試験を実施し、何百ものトレーニングパートナーを擁しています。

私たちの目的は、オープンソースの知識とスキルの認定資格を世界中からアクセスできるようにすることで、誰にとっても経済的で創造的な機会を可能にすることです。

  • LinkedIn
  • flogo-RGB-HEX-Blk-58 Facebook
  • Twitter
  • お問い合わせ
  • 個人情報とCookieポリシー

間違いを見つけたり、このページを改善したいですか? 私たちに知らせてください。.

© 1999–2023 The Linux Professional Institute Inc. All rights reserved.