MySQL | SQLite経験者向け、30日で習得するMySQL:実務応用 - Day24 アプリ連携

SQL MySQL
スポンサーリンク

Day24 後半のゴール

「“安全に書き込めるアプリ”を、自分で組み立てられるようになる」

前半で「アプリから MySQL に接続して SELECT する」まではイメージできました。
後半では、いよいよ INSERT / UPDATE / DELETE といった「書き込み」を扱います。

ここでのゴールはこうです。

プレースホルダ(バインド変数)を使って、安全に値を埋め込める
トランザクションの基本(まとめて成功・まとめて失敗)を理解する
接続プールのイメージを掴み、「毎回生接続」はやめようと思える

そして何より、「SQLインジェクションを絶対に許さない」という感覚を、体に刻みます。


プレースホルダで値を埋める

「文字列連結でSQLを作るのは“絶対にやってはいけない”」

まず、一番大事な話からいきます。

ユーザー入力をそのまま文字列連結して SQL を作るのは、
SQLインジェクションの原因になるので、絶対にやってはいけません。

例えば、こういう書き方はアウトです。

// ダメな例(Node.js)
const name = req.query.name; // ユーザー入力
const sql = `SELECT id, name FROM users WHERE name = '${name}'`;
JavaScript
# ダメな例(Python)
name = request.args.get('name')
sql = f"SELECT id, name FROM users WHERE name = '{name}'"
Python

ユーザーが '; DROP TABLE users; -- のような文字列を入れたら、
本当にテーブルが消える可能性があります。

これを防ぐために、必ず「プレースホルダ(バインド変数)」を使います。


Node.js でのプレースホルダ

「? に値を渡すだけで、エスケープと型付けを任せる」

mysql2 では、? をプレースホルダとして使います。

SELECT の例(条件付き)

const [rows] = await connection.execute(
  'SELECT id, name FROM users WHERE name = ?',
  [name]  // ここに配列で渡す
);
JavaScript

ここで重要なのは、

SQL文字列と値を“分けて”渡している
ライブラリ側が値を適切にエスケープしてくれる

という点です。

INSERT の例

const [result] = await connection.execute(
  'INSERT INTO users (name, email) VALUES (?, ?)',
  [name, email]
);
console.log('inserted id =', result.insertId);
JavaScript

SQLインジェクションを防ぐためには、

SQLの形(構造)はコードで固定する
値だけをプレースホルダ経由で渡す

というスタイルを徹底します。


Python でのプレースホルダ

「%s を使うが、“文字列フォーマットではない”ことに注意」

mysql-connector-python では、%s をプレースホルダとして使います。
ただし、Python の文字列フォーマットとは別物です。

SELECT の例

cursor = connection.cursor(dictionary=True)
sql = 'SELECT id, name FROM users WHERE name = %s'
cursor.execute(sql, (name,))
rows = cursor.fetchall()
Python

(name,) のようにタプルで渡します。

INSERT の例

sql = 'INSERT INTO users (name, email) VALUES (%s, %s)'
cursor.execute(sql, (name, email))
connection.commit()
Python

ここでも、

SQL文字列は固定
値は第二引数で渡す

という構造を守ることで、
ライブラリが安全にエスケープしてくれます。


トランザクションの基本

「“全部成功するか、全部なかったことにするか”」

次に、書き込みで絶対に押さえておきたいのがトランザクションです。

例えば、ECサイトで「注文を登録する」処理を考えます。

orders に1件 INSERT
order_items に複数件 INSERT
products の在庫を減らす UPDATE

この3つのどれかが失敗したら、「注文が中途半端な状態」になってしまいます。
それを防ぐのがトランザクションです。

イメージとしては、

トランザクション開始
複数のSQLを実行
全部成功したら COMMIT(確定)
どれか失敗したら ROLLBACK(全部なかったことに)

という流れです。


Node.js でのトランザクション

「beginTransaction → commit / rollback の流れを型で覚える」

mysql2/promise での典型的な書き方はこうです。

const mysql = require('mysql2/promise');

async function createOrder(userId, items) {
  const connection = await mysql.createConnection({ /* 接続情報 */ });

  try {
    await connection.beginTransaction();

    const [orderResult] = await connection.execute(
      'INSERT INTO orders (user_id, total_amount, status, ordered_at, created_at, updated_at) VALUES (?, ?, ?, NOW(), NOW(), NOW())',
      [userId, 0, 'pending']
    );
    const orderId = orderResult.insertId;

    let totalAmount = 0;

    for (const item of items) {
      const { productId, quantity, unitPrice } = item;
      totalAmount += quantity * unitPrice;

      await connection.execute(
        'INSERT INTO order_items (order_id, product_id, quantity, unit_price, created_at, updated_at) VALUES (?, ?, ?, ?, NOW(), NOW())',
        [orderId, productId, quantity, unitPrice]
      );

      await connection.execute(
        'UPDATE products SET stock = stock - ? WHERE id = ?',
        [quantity, productId]
      );
    }

    await connection.execute(
      'UPDATE orders SET total_amount = ?, status = ? WHERE id = ?',
      [totalAmount, 'paid', orderId]
    );

    await connection.commit();
    await connection.end();
    return orderId;
  } catch (err) {
    await connection.rollback();
    await connection.end();
    throw err;
  }
}
JavaScript

ここでの重要ポイントは、

beginTransaction から commit までを try ブロックに入れる
途中でエラーが出たら catch で rollback する
最後に必ず接続を閉じる

という「型」をそのまま覚えてしまうことです。


Python でのトランザクション

「autocommit を切って、commit / rollback を自分で呼ぶ」

mysql-connector-python では、デフォルトで autocommit が OFF のことが多いですが、
明示的に制御する方が分かりやすいです。

import mysql.connector

def create_order(user_id, items):
    connection = mysql.connector.connect(
        host='localhost',
        user='sample_user',
        password='secret_password',
        database='sample_app'
    )
    connection.autocommit = False

    try:
        cursor = connection.cursor()

        cursor.execute(
            '''
            INSERT INTO orders (user_id, total_amount, status, ordered_at, created_at, updated_at)
            VALUES (%s, %s, %s, NOW(), NOW(), NOW())
            ''',
            (user_id, 0, 'pending')
        )
        order_id = cursor.lastrowid

        total_amount = 0

        for product_id, quantity, unit_price in items:
            total_amount += quantity * unit_price

            cursor.execute(
                '''
                INSERT INTO order_items (order_id, product_id, quantity, unit_price, created_at, updated_at)
                VALUES (%s, %s, %s, %s, NOW(), NOW())
                ''',
                (order_id, product_id, quantity, unit_price)
            )

            cursor.execute(
                'UPDATE products SET stock = stock - %s WHERE id = %s',
                (quantity, product_id)
            )

        cursor.execute(
            'UPDATE orders SET total_amount = %s, status = %s WHERE id = %s',
            (total_amount, 'paid', order_id)
        )

        connection.commit()
        cursor.close()
        connection.close()
        return order_id

    except Exception as e:
        connection.rollback()
        cursor.close()
        connection.close()
        raise e
Python

ここでも、

commit するまではDBに確定しない
エラーが出たら rollback で全部取り消す

という「全部成功か全部失敗か」の世界を作っています。


接続プールのイメージ

「“リクエストごとに生接続”はスケールしない」

前半・後半の例では、分かりやすさのために

処理のたびに createConnection して end する

という書き方をしましたが、
実際のWebアプリでは、これだと効率が悪くなります。

そこで使うのが「接続プール(connection pool)」です。

イメージとしては、

あらかじめ一定数の接続を作ってプールしておく
リクエストが来たら、その中から1本借りて使う
使い終わったら返して、次のリクエストが再利用する

という仕組みです。

Node.js での接続プールのイメージ

const mysql = require('mysql2/promise');

const pool = mysql.createPool({
  host: process.env.DB_HOST,
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  database: process.env.DB_NAME,
  waitForConnections: true,
  connectionLimit: 10,
  queueLimit: 0,
});

async function getUsers() {
  const [rows] = await pool.execute('SELECT id, name FROM users');
  return rows;
}
JavaScript

アプリ全体で pool を1つ作っておき、
各処理は pool.execute を呼ぶだけ、という形にします。

Python でも、ライブラリやフレームワーク側でプールを持つことが多いです。
Day24 の段階では、

「毎回生接続を作るのではなく、プールを使うのが普通」

という感覚だけ持っておけば十分です。


Day24 後半のまとめ

アプリから MySQL に“書き込み”を行うときに一番重要なのは、「ユーザー入力を文字列連結でSQLに埋め込まない」ことであり、Node.js なら ? プレースホルダ+配列、Python なら %s プレースホルダ+タプルを使って、SQLの形はコードで固定し、値だけを別引数で渡すことでSQLインジェクションを防ぐのが大前提になる。
そのうえで、複数のINSERT/UPDATEをまとめて扱う処理(注文登録など)は、Node.js では beginTransaction → 複数の execute → commit / rollback、Python では autocommit=False → 複数の execute → commit / rollback という「全部成功か全部失敗か」のトランザクションパターンを型で覚え、さらに実際のWebアプリでは毎回 createConnection するのではなく、接続プールを1つ作って pool.execute(...) のように再利用する構成にすることで、安全性(SQLインジェクション防止・トランザクション)とスケーラビリティ(接続プール)の両方を満たした“実務レベルのアプリ連携”に近づいていく。

タイトルとURLをコピーしました