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);
JavaScriptSQLインジェクションを防ぐためには、
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インジェクション防止・トランザクション)とスケーラビリティ(接続プール)の両方を満たした“実務レベルのアプリ連携”に近づいていく。

