חיבור למסד נתונים
ה-API מהשיעור הקודם שומר הכול בזיכרון: כל הפעלה מחדש של השרת מוחקת את המשימות. כאן נחבר אותו ל-MySQL, אותו מסד נתונים שלמדתם בקורס SQL. בזכות שכבת ה-service, הנתיבים עצמם כמעט לא ישתנו.
הטבלה
CREATE DATABASE IF NOT EXISTS todo_app CHARACTER SET utf8mb4;
USE todo_app;
CREATE TABLE todos (
id INT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(200) NOT NULL,
done BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
החבילה: mysql2
npm install mysql2
# .env
DATABASE_URL=mysql://root:secret@localhost:3306/todo_app
// src/db.js
import mysql from 'mysql2/promise';
// Pool: כמה חיבורים פתוחים שמשותפים לכל הבקשות, במקום חיבור חדש לכל בקשה
export const pool = mysql.createPool({
uri: process.env.DATABASE_URL,
connectionLimit: 10,
});
ה-service, הפעם מול המסד
// src/todos/todos.service.js
import { pool } from '../db.js';
export async function listTodos({ done } = {}) {
if (done === undefined) {
const [rows] = await pool.query('SELECT * FROM todos ORDER BY created_at DESC');
return rows;
}
const [rows] = await pool.query('SELECT * FROM todos WHERE done = ? ORDER BY created_at DESC', [done]);
return rows;
}
export async function getTodo(id) {
const [rows] = await pool.query('SELECT * FROM todos WHERE id = ?', [id]);
return rows[0] ?? null;
}
export async function createTodo(title) {
const [result] = await pool.query('INSERT INTO todos (title) VALUES (?)', [title]);
return getTodo(result.insertId);
}
export async function updateTodo(id, { title, done }) {
const [result] = await pool.query(
'UPDATE todos SET title = COALESCE(?, title), done = COALESCE(?, done) WHERE id = ?',
[title ?? null, done ?? null, id]
);
return result.affectedRows ? getTodo(id) : null;
}
export async function deleteTodo(id) {
const [result] = await pool.query('DELETE FROM todos WHERE id = ?', [id]);
return result.affectedRows > 0;
}
בנתיבים משתנה רק דבר אחד: כל קריאה ל-service מקבלת await. שגיאה של המסד מגיעה לבד למטפל השגיאות, כי ב-Express 5 שגיאות async נתפסות.
todosRouter.get('/:id', async (req, res) => {
const todo = await service.getTodo(req.params.id);
if (!todo) throw new NotFoundError('המשימה');
res.json(todo);
});
SQL Injection: הכלל החשוב בשיעור
// ❌ לעולם לא: הקלט של המשתמש נכנס ישר לשאילתה
pool.query(`SELECT * FROM users WHERE email = '${req.body.email}'`);
// מספיק שמישהו ישלח: ' OR '1'='1 כדי לקבל את כל המשתמשים
// ✅ תמיד: סימני שאלה, והערכים בנפרד
pool.query('SELECT * FROM users WHERE email = ?', [req.body.email]);
כשהערכים נשלחים בנפרד מה-SQL, מסד הנתונים מתייחס אליהם כנתונים בלבד, אף פעם כקוד. זה נכון לכל ספרייה ולכל מסד נתונים.
טרנזקציה: כמה פעולות, הכול או כלום
export async function transfer(fromId, toId, amount) {
const connection = await pool.getConnection();
try {
await connection.beginTransaction();
await connection.query('UPDATE accounts SET balance = balance - ? WHERE id = ?', [amount, fromId]);
await connection.query('UPDATE accounts SET balance = balance + ? WHERE id = ?', [amount, toId]);
await connection.commit();
} catch (error) {
await connection.rollback();
throw error;
} finally {
connection.release(); // מחזירים את החיבור ל-Pool, גם בהצלחה וגם בכישלון
}
}
על המושג עצמו בהרחבה בשיעור טרנזקציות בקורס SQL.
ORM: כשלא רוצים לכתוב SQL ידני
בפרויקטים גדולים משתמשים הרבה פעמים ב-ORM או Query Builder, שמייצרים את ה-SQL מקוד JavaScript ומוסיפים בדיקת טיפוסים:
- Prisma - מגדירים סכמה בקובץ, ומקבלים לקוח עם השלמה אוטומטית.
- Drizzle - קרוב מאוד ל-SQL, קל ומהיר, מצוין עם TypeScript.
כדאי להכיר SQL "נקי" קודם, כמו בשיעור הזה: כך מבינים מה ה-ORM עושה, ויודעים לתקן כשמשהו איטי.
ומה עם PostgreSQL או MongoDB?
| מסד | חבילה | הערה |
|---|---|---|
| PostgreSQL | pg | אותו רעיון, עם $1, $2 במקום ? |
| SQLite | better-sqlite3 | קובץ אחד, בלי שרת. מצוין לפרויקטים קטנים ולבדיקות |
| MongoDB | mongodb / Mongoose | מסד מסמכים (לא טבלאות), בלי SQL |
בדקו את עצמכם
נסו לענות לבד לפני שאתם פותחים את התשובה.
-
למה משתמשים ב-
?בשאילתה ולא משרשרים את הקלט?הצגת התשובה
תשובה א. שרשור קלט של משתמש לשאילתה הוא אחת הפרצות הנפוצות בעולם.
-
מה היתרון של Pool חיבורים?
הצגת התשובה
תשובה ד. פתיחת חיבור היא פעולה יקרה.
-
למה חשוב
connection.release()ב-finally?הצגת התשובה
תשובה ב. חיבורים שלא מוחזרים נגמרים, והשרת נתקע.