SQLiteは軽量で扱いやすい一方、トランザクションを正しく使わないとデータが中途半端な状態になりがちです。 特に複数行INSERT・複数テーブル更新・業務アプリの重要処理では、トランザクションは必須です。 この記事では、C#からSQLiteのトランザクションを扱う実務レベルのパターンをまとめます。
この記事でわかること
・SQLiteトランザクションの基本(BEGIN / COMMIT / ROLLBACK)
・C#(Microsoft.Data.Sqlite)での実装パターン
・複数INSERTを一括で高速に処理する方法
・エラー時に確実にロールバックする書き方
・Dapper / EF Core でのトランザクション
・業務アプリ向けベストプラクティス
・SQLiteトランザクションの基本(BEGIN / COMMIT / ROLLBACK)
・C#(Microsoft.Data.Sqlite)での実装パターン
・複数INSERTを一括で高速に処理する方法
・エラー時に確実にロールバックする書き方
・Dapper / EF Core でのトランザクション
・業務アプリ向けベストプラクティス
1. SQLiteトランザクションの基本
SQLiteのトランザクションは、基本的に次の3つで構成されます。
- BEGIN:トランザクション開始
- COMMIT:確定(全て反映)
- ROLLBACK:取り消し(全てなかったことに)
SQLレベルの例
BEGIN TRANSACTION;
INSERT INTO Users (Name, Age) VALUES ('A', 20);
INSERT INTO Users (Name, Age) VALUES ('B', 30);
COMMIT;
途中でエラーが起きた場合は ROLLBACK; を実行します。
2. C#(Microsoft.Data.Sqlite)での基本パターン
単純なトランザクション例
using Microsoft.Data.Sqlite;
var cs = "Data Source=sample.db";
using var con = new SqliteConnection(cs);
con.Open();
using var tran = con.BeginTransaction();
try
{
using (var cmd = new SqliteCommand("INSERT INTO Users (Name, Age) VALUES (@name, @age)", con, tran))
{
cmd.Parameters.AddWithValue("@name", "Taro");
cmd.Parameters.AddWithValue("@age", 25);
cmd.ExecuteNonQuery();
}
using (var cmd = new SqliteCommand("INSERT INTO Logs (Message) VALUES (@msg)", con, tran))
{
cmd.Parameters.AddWithValue("@msg", "User Taro created");
cmd.ExecuteNonQuery();
}
tran.Commit();
}
catch
{
tran.Rollback();
throw;
}
ポイント
BeginTransaction()で開始SqliteCommandにtransaction: tranを渡す- 成功時のみ
Commit()、例外時はRollback()
注意:
トランザクションを使わずに複数INSERTすると、途中でエラーが出たときに「半分だけ入っている」状態になります。
3. 複数行INSERTを一括で高速に処理する
大量INSERTは、1件ずつトランザクションを張ると非常に遅くなります。 1つのトランザクションでまとめてINSERTするのが定番パターンです。
var users = new[]
{
new { Name = "A", Age = 20 },
new { Name = "B", Age = 25 },
new { Name = "C", Age = 30 },
};
using var con = new SqliteConnection(cs);
con.Open();
using var tran = con.BeginTransaction();
try
{
foreach (var u in users)
{
using var cmd = new SqliteCommand(
"INSERT INTO Users (Name, Age) VALUES (@name, @age)", con, tran);
cmd.Parameters.AddWithValue("@name", u.Name);
cmd.Parameters.AddWithValue("@age", u.Age);
cmd.ExecuteNonQuery();
}
tran.Commit();
}
catch
{
tran.Rollback();
throw;
}
メリット
- 速度が大幅に向上する
- 途中で失敗しても全てロールバックできる
4. 非同期(async/await)+トランザクション
UIフリーズを避けるため、非同期APIと組み合わせることも多いです。
using var con = new SqliteConnection(cs);
await con.OpenAsync();
using var tran = await con.BeginTransactionAsync();
try
{
using (var cmd = new SqliteCommand(
"INSERT INTO Users (Name, Age) VALUES (@name, @age)", con, tran))
{
cmd.Parameters.AddWithValue("@name", "AsyncUser");
cmd.Parameters.AddWithValue("@age", 40);
await cmd.ExecuteNonQueryAsync();
}
await tran.CommitAsync();
}
catch
{
await tran.RollbackAsync();
throw;
}
5. Dapperでのトランザクション
Dapperは Execute / Query に transaction パラメータを渡すだけです。
using Dapper;
using Microsoft.Data.Sqlite;
using var con = new SqliteConnection(cs);
con.Open();
using var tran = con.BeginTransaction();
try
{
con.Execute(
"INSERT INTO Users (Name, Age) VALUES (@Name, @Age)",
new { Name = "DapperA", Age = 20 },
transaction: tran
);
con.Execute(
"INSERT INTO Users (Name, Age) VALUES (@Name, @Age)",
new { Name = "DapperB", Age = 30 },
transaction: tran
);
tran.Commit();
}
catch
{
tran.Rollback();
throw;
}
非同期版
await con.ExecuteAsync(
"INSERT INTO Users (Name, Age) VALUES (@Name, @Age)",
new { Name = "AsyncDapper", Age = 50 },
transaction: tran
);
6. EF Coreでのトランザクション
EF Coreは Database.BeginTransaction() を使います。
using var db = new AppDbContext();
using var tran = await db.Database.BeginTransactionAsync();
try
{
db.Users.Add(new User { Name = "EFUser1", Age = 20 });
db.Users.Add(new User { Name = "EFUser2", Age = 30 });
await db.SaveChangesAsync();
await tran.CommitAsync();
}
catch
{
await tran.RollbackAsync();
throw;
}
ポイント
SaveChanges()だけでも内部トランザクションは張られる- 複数コンテキストや外部処理と絡む場合は明示的トランザクションが有効
7. トランザクションとロック(database is locked)
SQLiteは同時書き込みに弱い
トランザクション中に別スレッド・別プロセスから書き込みが来ると
database is locked が発生しやすくなります。
対策
- 書き込みはできるだけ短いトランザクションでまとめる
- 大量INSERTは1つのトランザクションで一気に処理してすぐCOMMIT
- 同時書き込みが多い設計は避ける(キュー化・シリアライズ)
8. 業務アプリでのベストプラクティス
- 複数テーブル更新・複数行INSERTは必ずトランザクションを使う
- 成功時のみ
Commit()、例外時は必ずRollback() - トランザクションはできるだけ短く保つ(ロック時間を短く)
- 大量データは1トランザクションでまとめて高速化
- Dapper / EF Core でもトランザクションを明示的に扱える
まとめ:SQLite × トランザクションは“壊れないデータ”の必須技術
- BEGIN / COMMIT / ROLLBACK を正しく使えば、中途半端な状態を防げる
- C#からは
BeginTransaction()パターンが基本 - Dapper / EF Core でも同じ考え方で扱える
- ロックを避けるには「短く・まとめて・一気に」処理するのがコツ
SQLiteは軽量で導入しやすい一方、 トランザクション設計がそのまま信頼性に直結するDBです。 この記事をベースに、「速くて壊れない」更新処理を組み立ててみてください。