在 better-sqlite3
中,.exec()
方法用于執行包含多個 SQL 語句的字符串。與預編譯語句相比,這種方法性能較差且安全性較低,但有時它是必要的,特別是當你需要從外部文件(如 SQL 腳本)中執行多個 SQL 語句時。
使用 .exec()
方法
以下是如何使用 .exec()
方法來執行從文件中讀取的 SQL 腳本,并確保正確處理錯誤和事務回滾。
示例代碼
假設你有一個名為 migrate-schema.sql
的 SQL 文件,其中包含多個 SQL 語句,以下是完整的示例代碼:
-- 創建 users 表
CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT,name TEXT NOT NULL,age INTEGER NOT NULL,created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);-- 創建 posts 表
CREATE TABLE IF NOT EXISTS posts (id INTEGER PRIMARY KEY AUTOINCREMENT,user_id INTEGER NOT NULL,title TEXT NOT NULL,content TEXT NOT NULL,created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,FOREIGN KEY (user_id) REFERENCES users(id)
);-- 插入一些初始用戶數據
INSERT INTO users (name, age) VALUES ('Alice', 28);
INSERT INTO users (name, age) VALUES ('Bob', 25);
INSERT INTO users (name, age) VALUES ('Charlie', 30);-- 插入一些初始帖子數據
INSERT INTO posts (user_id, title, content) VALUES (1, 'My First Post', 'This is my first post.');
INSERT INTO posts (user_id, title, content) VALUES (1, 'Another Post', 'This is another post.');
INSERT INTO posts (user_id, title, content) VALUES (2, 'Hello World', 'Hello everyone!');-- 更新 Alice 的年齡為 29
UPDATE users SET age = 29 WHERE name = 'Alice';-- 刪除 Bob 的所有帖子
DELETE FROM posts WHERE user_id = (SELECT id FROM users WHERE name = 'Bob');-- 查詢所有用戶及其帖子
SELECT u.id AS user_id, u.name, p.id AS post_id, p.title, p.content
FROM users u
LEFT JOIN posts p ON u.id = p.user_id;
const fs = require('fs');
const path = require('path');
const Database = require('better-sqlite3');// 打開數據庫連接
const db = new Database('mydb.sqlite');// 讀取 SQL 文件內容
const migrationFilePath = path.join(__dirname, 'migrate-schema.sql');
const migration = fs.readFileSync(migrationFilePath, 'utf8');try {// 開始事務db.exec('BEGIN TRANSACTION;');// 執行 SQL 文件中的所有語句db.exec(migration);// 提交事務db.exec('COMMIT;');console.log('Migration completed successfully.');
} catch (error) {// 如果發生錯誤,回滾事務db.exec('ROLLBACK;');console.error('An error occurred during migration:', error.message);
}// 關閉數據庫連接
db.close();
console.log('Database connection closed.');
詳細解釋
-
讀取 SQL 文件:
- 使用
fs.readFileSync()
讀取 SQL 文件的內容。這里我們使用path.join()
來確保路徑的兼容性。
- 使用
-
開始事務:
- 在執行 SQL 語句之前,首先調用
db.exec('BEGIN TRANSACTION;')
開始一個事務。這可以確保所有的 SQL 語句要么全部成功,要么全部失敗,從而保持數據的一致性。
- 在執行 SQL 語句之前,首先調用
-
執行 SQL 文件中的所有語句:
- 使用
db.exec(migration)
執行從文件中讀取的所有 SQL 語句。注意,.exec()
可以執行包含多個 SQL 語句的字符串。
- 使用
-
提交事務:
- 如果所有 SQL 語句都成功執行,則調用
db.exec('COMMIT;')
提交事務。
- 如果所有 SQL 語句都成功執行,則調用
-
錯誤處理和事務回滾:
- 如果在執行 SQL 語句的過程中發生錯誤,捕獲異常并調用
db.exec('ROLLBACK;')
回滾事務,以防止部分更新導致的數據不一致問題。
- 如果在執行 SQL 語句的過程中發生錯誤,捕獲異常并調用
-
關閉數據庫連接:
- 最后,調用
db.close()
關閉數據庫連接。
- 最后,調用
錯誤處理和日志記錄
為了更好地調試和維護,建議增加更多的錯誤處理和日志記錄。例如,可以在捕獲異常時記錄詳細的錯誤信息:
try {// 開始事務db.exec('BEGIN TRANSACTION;');// 執行 SQL 文件中的所有語句db.exec(migration);// 提交事務db.exec('COMMIT;');console.log('Migration completed successfully.');
} catch (error) {// 如果發生錯誤,回滾事務db.exec('ROLLBACK;');// 記錄詳細的錯誤信息console.error('An error occurred during migration:');console.error('Error message:', error.message);console.error('Stack trace:', error.stack);
}
注意事項
-
安全性:由于
.exec()
直接執行 SQL 字符串,因此存在 SQL 注入的風險。盡量避免直接將用戶輸入插入到.exec()
調用中。如果必須這樣做,請先進行嚴格的驗證和清理。 -
性能:與預編譯語句相比,
.exec()
的性能較差。如果可能的話,盡量使用預編譯語句來提高性能和安全性。 -
事務管理:當執行多個 SQL 語句時,務必使用事務來確保數據一致性。如果沒有使用事務,部分語句的成功執行可能會導致數據庫處于不一致狀態。
完整示例
以下是一個更完整的示例,展示了如何結合信號處理機制來確保在應用程序退出時正確關閉數據庫連接:
const fs = require('fs');
const path = require('path');
const process = require('process');
const Database = require('better-sqlite3');// 打開數據庫連接
const db = new Database('mydb.sqlite');// 監聽進程退出事件和其他終止信號
function handleExit() {try {db.close();console.log('Database connection closed gracefully.');} catch (error) {console.error('Error closing database:', error.message);}
}['exit', 'SIGINT', 'SIGTERM', 'SIGHUP'].forEach((signal) => {process.on(signal, handleExit);
});// 讀取 SQL 文件內容
const migrationFilePath = path.join(__dirname, 'migrate-schema.sql');
const migration = fs.readFileSync(migrationFilePath, 'utf8');try {// 開始事務db.exec('BEGIN TRANSACTION;');// 執行 SQL 文件中的所有語句db.exec(migration);// 提交事務db.exec('COMMIT;');console.log('Migration completed successfully.');
} catch (error) {// 如果發生錯誤,回滾事務db.exec('ROLLBACK;');console.error('An error occurred during migration:');console.error('Error message:', error.message);console.error('Stack trace:', error.stack);
}// 模擬長時間運行的任務
setTimeout(() => {console.log('Long-running task completed.');
}, 60000); // 1分鐘
通過這種方式,你可以確保在任何情況下都能正確關閉數據庫連接,并且在執行復雜的 SQL 腳本時保持數據的一致性和完整性。如果有更多問題或需要進一步的幫助,請隨時提問!