アプリ開発の初期はSQLiteで十分でも、 ユーザー数増加・データ量増加・サーバー化などのタイミングで SQL Server / PostgreSQL へ移行したいというケースは非常に多いです。 しかし、DB依存のコードがアプリ全体に散らばっていると、移行コストは爆発します。
そこで重要なのが、最初からマルチDB対応のアーキテクチャを設計しておくこと。 この記事では、SQLiteを起点に、将来のDB移行に強い構成を実務目線で解説します。
・マルチDB対応の基本戦略
・Repository + Unit of Work の役割
・SQL抽象化の方法(Dapper/EF Core)
・接続層・SQL層の分離
・DBごとの差異(LIMIT/OFFSET、AUTO INCREMENTなど)
・業務アプリ向けベストプラクティス
1. マルチDB対応の基本戦略
マルチDB対応の本質は、次の3つを分離することです。
- 接続層(Connection Factory)
- SQL層(Repository)
- トランザクション層(Unit of Work)
この3つを分離すれば、DB移行時に差し替えるのは最小限で済みます。
2. 接続層(Connection Factory)を分離する
DBごとに接続クラスが異なるため、 接続生成を1箇所に集約するのが最重要です。
■ IConnectionFactory
public interface IConnectionFactory
{
IDbConnection CreateConnection();
}
■ SQLite実装
public class SqliteConnectionFactory : IConnectionFactory
{
private readonly string _cs;
public SqliteConnectionFactory(string cs) => _cs = cs;
public IDbConnection CreateConnection()
=> new SqliteConnection(_cs);
}
■ SQL Server実装
public class SqlServerConnectionFactory : IConnectionFactory
{
private readonly string _cs;
public SqlServerConnectionFactory(string cs) => _cs = cs;
public IDbConnection CreateConnection()
=> new SqlConnection(_cs);
}
アプリ側は DB の種類を一切意識しません。
3. Repository層でSQLを抽象化する
RepositoryはDBごとの差異を吸収する層です。 UIやサービス層はRepositoryだけを呼び出します。
■ IUserRepository
public interface IUserRepository
{
Task<User?> GetByIdAsync(int id);
Task<IEnumerable<User>> GetAllAsync();
Task AddAsync(User user);
}
■ SQLite実装(Dapper)
public class SqliteUserRepository : IUserRepository
{
private readonly IConnectionFactory _factory;
public SqliteUserRepository(IConnectionFactory factory)
=> _factory = factory;
public async Task<User?> GetByIdAsync(int id)
{
using var con = _factory.CreateConnection();
return await con.QueryFirstOrDefaultAsync<User>(
"SELECT Id, Name, Age FROM Users WHERE Id=@id", new { id });
}
}
■ SQL Server実装(Dapper)
SQL Serverでは LIMIT が使えないため、TOP を使うなど差異を吸収します。
public class SqlServerUserRepository : IUserRepository
{
private readonly IConnectionFactory _factory;
public SqlServerUserRepository(IConnectionFactory factory)
=> _factory = factory;
public async Task<User?> GetByIdAsync(int id)
{
using var con = _factory.CreateConnection();
return await con.QueryFirstOrDefaultAsync<User>(
"SELECT TOP 1 Id, Name, Age FROM Users WHERE Id=@id", new { id });
}
}
4. Unit of Workでトランザクションを統合
Unit of Work(UoW)は、 複数Repositoryの操作を1つのトランザクションにまとめる仕組みです。
■ IUnitOfWork
public interface IUnitOfWork : IAsyncDisposable
{
IUserRepository Users { get; }
IOrderRepository Orders { get; }
Task CommitAsync();
Task RollbackAsync();
}
■ SQLite実装
public class SqliteUnitOfWork : IUnitOfWork
{
private readonly IDbConnection _con;
private readonly IDbTransaction _tran;
public IUserRepository Users { get; }
public IOrderRepository Orders { get; }
public SqliteUnitOfWork(IConnectionFactory factory)
{
_con = factory.CreateConnection();
_con.Open();
_tran = _con.BeginTransaction();
Users = new SqliteUserRepository(factory);
Orders = new SqliteOrderRepository(factory);
}
public Task CommitAsync() => Task.Run(() => _tran.Commit());
public Task RollbackAsync() => Task.Run(() => _tran.Rollback());
public ValueTask DisposeAsync()
{
_tran.Dispose();
_con.Dispose();
return ValueTask.CompletedTask;
}
}
SQL Server版も同じ構造で作れます。
5. DBごとの差異をどう吸収するか?
マルチDB対応で最も重要なのは、DBごとの差異をRepositoryで吸収すること。
■ 代表的な差異一覧
| 項目 | SQLite | SQL Server | PostgreSQL |
|---|---|---|---|
| 自動採番 | INTEGER PRIMARY KEY | IDENTITY | SERIAL / IDENTITY |
| LIMIT | LIMIT | TOP / OFFSET FETCH | LIMIT |
| 日付 | TEXT(ISO8601) | datetime2 | timestamp |
| 外部キー | PRAGMA foreign_keys=ON | 標準 | 標準 |
これらはすべてRepository層で吸収すれば、 アプリ本体はDB差異を一切意識しなくて済みます。
6. 実務アプリ向けベストプラクティス
- 接続層(Connection Factory)を必ず分離する
- SQLはRepositoryに閉じ込める
- Unit of Workでトランザクション境界を統一
- SQL差異(LIMIT/TOPなど)はRepositoryで吸収
- アプリ本体はDBを意識しない構造にする
- 将来のDB移行は Repository + UoW の差し替えだけで完了
まとめ:マルチDB対応は“最初の設計”で決まる
- SQLiteで始めても、SQL Server/PostgreSQLへ移行しやすい
- Repository + UoW がアプリ寿命を大幅に伸ばす
- 接続層・SQL層・トランザクション層の分離が鍵
「今はSQLiteだけど、将来どうなるかわからない」 そんなアプリこそ、マルチDB対応設計が必須です。 この記事のアーキテクチャをベースに、 長期運用に耐えるデータアクセス層を構築してみてください。