Drift는 Flutter와 Dart에서 SQLite 데이터베이스를 쉽게 다룰 수 있게 해주는 ORM(Object-Relational Mapping) 라이브러리입니다.
SQL 문법을 간단히 사용하면서 Dart 코드 내에서 데이터베이스를 구성하고, 쿼리를 작성하고, 타입 안전성을 확보하는 등 다양한 편의 기능을 제공합니다. Drift의 특징과 장단점을 정리해 보겠습니다.
Drift는 타입 안전성을 제공하여, 데이터베이스 쿼리를 작성할 때 타입 오류를 컴파일 단계에서 잡아줍니다. 이를 통해 런타임 오류를 줄이고 안정성을 높일 수 있습니다.
Drift는 Dart에 최적화되어 있어 Flutter 프로젝트에서 별도 언어 없이 SQLite를 사용할 수 있습니다. SQL 쿼리를 Dart 코드처럼 작성할 수 있고, Dart의 정적 타입 검사 덕분에 오류를 빠르게 발견할 수 있습니다.
Drift는 데이터베이스 테이블과 관련된 코드를 자동 생성해 줍니다.
이를 통해 테이블 정의와 관련된 코드를 효율적으로 관리하고,
코드베이스가 커지더라도 쉽게 유지보수가 가능합니다.
버전 관리를 위한 마이그레이션 기능을 지원하여 데이터베이스 스키마가 변경될 때 안전하게 데이터를 이전할 수 있습니다.
MigrationStrategy를 사용해 데이터베이스 업데이트 시 필요한 로직을 쉽게 추가할 수 있습니다.
Drift는 데이터가 변경될 때 즉시 반영되는 Stream을 지원합니다.
이를 통해 데이터베이스 변경이 UI에 실시간으로 반영될 수 있어, 실시간으로 데이터 변화를 감지하는 앱을 구현할 수 있습니다.
Drift는 Android, iOS, Windows, MacOS 등 다양한 플랫폼을 지원하며, 플랫폼에 맞는 최적화 기능도 제공해 범용성을 높입니다.
코드 자동 생성
테이블 정의, 쿼리 작성 등의 코드가 자동으로 생성되기 때문에 반복적인 작업이 줄어들고 코드의 유지보수가 용이합니다.
반응형 UI 구현 용이
Stream을 통해 데이터가 변경되면 즉시 UI에 반영되도록 할 수 있어, 실시간 데이터 업데이트가 필요한 앱에 적합합니다.
강력한 타입 검사 및 오류 방지
타입 안전성 덕분에 컴파일 단계에서 많은 오류를 잡을 수 있으며, 런타임 에러가 발생할 가능성을 줄입니다.
SQL과 Dart의 조합
Drift는 SQL 쿼리문을 작성할 수 있게 해주는 동시에, Dart 코드로 복잡한 쿼리를 만들 수 있는 장점을 제공합니다.
퍼포먼스 제한
Drift는 SQLite를 사용하기 때문에 복잡하고 대규모의 데이터 처리가 필요한 경우 성능에 제한이 있을 수 있습니다.
코드 생성의 의존성
자동 생성된 코드에 의존하기 때문에, 버그 수정이나 특정 기능을 변경할 때는 자동 생성된 코드를 직접 수정할 수 없어서 어려움을 겪을 수 있습니다.
제한된 지원 기능
Drift는 SQLite를 기반으로 동작하기 때문에, NoSQL 데이터베이스와 같은 다른 유형의 데이터베이스를 사용해야 하는 경우 적합하지 않습니다.
반응형 UI 구현에 적합하고, 코드 생성 및 타입 안전성에서 큰 이점을 제공합니다.
다만, 대규모 데이터를 다루거나 고성능이 요구되는 경우에는 SQLite의 한계를 염두에 둬야 합니다.
Dart의 ORM(Object-Relational Mapping) 라이브러리 Drift를 사용해 SQLite와 상호작용합니다.
Drift는 앱의 데이터베이스와 테이블을 관리하는 설정을 하려고 합니다.
먼저 데이터베이스 테이블을 정의하고,
데이터베이스 접근 객체를 생성하여 여러 기능(예: 일정 생성, 조회, 수정, 삭제 등)을 제공합니다.
ScheduleTable과 CategoryTable이라는 두 개의 테이블을 다루고 있으며, 주요 기능을 하나씩 살펴보겠습니다.
// final database = AppDatabase(); database 인스턴스로 사용하는 코드이다.
part 'drift.g.dart';
(tables: [
ScheduleTable,
CategoryTable,
])
class AppDatabase extends _$AppDatabase {
AppDatabase() : super(_openConnection());
//CategoryTableCompanion : 업데이트 or 데이터를 생성할 때 사용
Future<int> createCategory(CategoryTableCompanion data) =>
into(categoryTable).insert(data);
//자동으로 생성되는 클래스 CategoryTableData (CategoryTable+Data)
Future<List<CategoryTableData>> getCategories() =>
select(categoryTable).get();
Future<ScheduleWithCategory> getScheduleById(int id) {
final query = select(scheduleTable).join([
innerJoin(
categoryTable,
categoryTable.id.equalsExp(
scheduleTable.colorId,
),
),
])
..where(scheduleTable.id.equals(id));
return query.map((row) {
final schedule = row.readTable(scheduleTable);
final category = row.readTable(categoryTable);
return ScheduleWithCategory(category: category, schedule: schedule);
}).getSingle();
//getSingle()은 쿼리 결과 중에서 단일 결과만 가져오는 함수다.
//쿼리가 오직 하나의 행을 반환할 때 사용하며, 특정 id에 해당하는 단일 일정 정보를 가져오는 데 적합하다.
}
Future<int> updateScheduleById(int id, ScheduleTableCompanion data) =>
(update(scheduleTable)..where((table) => table.id.equals(id)))
.write(data);
Future<List<ScheduleTableData>> getSchedules(
DateTime date,
) =>
(select(scheduleTable)..where((table) => table.date.equals(date))).get();
Stream<List<ScheduleWithCategory>> streamSchedules(
DateTime date,
) {
final query = select(scheduleTable).join([
innerJoin(
categoryTable,
categoryTable.id.equalsExp(
scheduleTable.colorId,
),
),
])
..where(scheduleTable.date.equals(date));
return query.map((row) {
final schedule = row.readTable(scheduleTable);
final category = row.readTable(categoryTable);
return ScheduleWithCategory(category: category, schedule: schedule);
}).watch();
}
Future<int> createSchedule(ScheduleTableCompanion data) =>
into(scheduleTable).insert(data);
Future<int> removeSchedule(int id) => (delete(scheduleTable)
..where(
(table) => table.id.equals(id),
))
.go();
int get schemaVersion => 3;
MigrationStrategy get migration {
return MigrationStrategy(
onUpgrade: (Migrator m, int from, int to) async {
if(from < 2){
await m.addColumn(categoryTable, categoryTable.randomNumber);
}
if(from < 3){
await m.addColumn(categoryTable, categoryTable.randomNumber2);
}
},
);
}
}
//데이터베이스 생성
LazyDatabase _openConnection() {
return LazyDatabase(
() async {
final dbFolder = await getApplicationDocumentsDirectory();
final file = File(p.join(dbFolder.path, 'db.sqlite'));
if (Platform.isAndroid) {
await applyWorkaroundToOpenSqlite3OnOldAndroidVersions();
}
final cachebase = await getTemporaryDirectory();
sqlite3.tempDirectory = cachebase.path;
//file에 데이터베이스를 생성한다.
return NativeDatabase.createInBackground(file);
},
);
}
(tables: [
ScheduleTable,
CategoryTable,
])
AppDatabase 클래스는 이 스케줄러 앱의 주요 데이터베이스 클래스입니다. @DriftDatabase라는 어노테이션을 통해 이 데이터베이스가 ScheduleTable과 CategoryTable 테이블을 포함하도록 지정합니다.
CategoryTable 테이블
Future<int> createCategory(CategoryTableCompanion data) =>
into(categoryTable).insert(data);
//아래 CategoryTable을 토대로 CategoryTableCompanion은 자동 생성된다.
class CategoryTable extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get color => text()();
IntColumn get randomNumber => integer().nullable()();
IntColumn get randomNumber2 => integer().withDefault(const Constant(0))();
DateTimeColumn get createdAt => dateTime().clientDefault(
() => DateTime.now().toUtc(),
)();
}
ScheduleTable 테이블
class ScheduleTable extends Table {
/// 1) 식별 가능한 ID
IntColumn get id => integer().autoIncrement()();
/// 2) 시작 시간
IntColumn get startTime => integer()();
/// 3) 종료 시간
IntColumn get endTime => integer()();
/// 4) 일정 내용
TextColumn get content => text()();
/// 5) 날짜
DateTimeColumn get date => dateTime()();
/// 6) 카테고리
IntColumn get colorId => integer().references(
CategoryTable,
#id,
)();
/// 7) 일정 생성날짜시간
DateTimeColumn get createdAt => dateTime().clientDefault(
() => DateTime.now().toUtc(),
)();
}
createCategory 함수
getCategories 함수
getScheduleById 함수
특정 id의 일정을 가져오는 함수입니다.
query = select(scheduleTable).join([...])을 통해 scheduleTable과 categoryTable을 조인합니다.
scheduleTable.colorId 컬럼을 기준으로 일정과 카테고리를 연결합니다.
class ScheduleWithCategory {
final CategoryTableData category;
final ScheduleTableData schedule;
ScheduleWithCategory({
required this.category,
required this.schedule,
});
}
이 함수는 결과로 ScheduleWithCategory 형식의 데이터를 반환합니다.
특정 일정과 연관된 카테고리를 함께 제공합니다.
updateScheduleById 함수
getSchedules 함수
streamSchedules 함수
createSchedule 함수
removeSchedule 함수
CategoryTable 추가시 Table을 최신 버전으로 업그레이드 할 때 마이그레이션을 사용합니다.
1. CategoryTable에 randomNumber Column 추가
2. CategoryTable에 randomNumber2 Column 추가
int get schemaVersion => 3;
MigrationStrategy get migration {
return MigrationStrategy(
onUpgrade: (Migrator m, int from, int to) async {
if(from < 2){ //schemaVersion 1인 경우
await m.addColumn(categoryTable, categoryTable.randomNumber);
}
if(from < 3){ ////schemaVersion 2인 경우
await m.addColumn(categoryTable, categoryTable.randomNumber2);
}
},
);
}
데이터베이스의 schemaVersion 3으로 설정되어 있습니다.
migration 속성은 MigrationStrategy를 설정하여
버전 업그레이드 시 각 마이그레이션 단계에서 추가적인 변경 사항을 적용할 수 있습니다.
from 값에 따라 특정 버전보다 낮은 경우에는
새로운 컬럼을 추가하도록 설정되어 있습니다.
(randomNumber, randomNumber2 컬럼).
LazyDatabase _openConnection() {
return LazyDatabase(
() async {
final dbFolder = await getApplicationDocumentsDirectory();
final file = File(p.join(dbFolder.path, 'db.sqlite'));
if (Platform.isAndroid) {
await applyWorkaroundToOpenSqlite3OnOldAndroidVersions();
}
final cachebase = await getTemporaryDirectory();
sqlite3.tempDirectory = cachebase.path;
return NativeDatabase.createInBackground(file);
},
);
이 함수는 LazyDatabase를 생성하며 데이터베이스 연결을 초기화합니다.
getApplicationDocumentsDirectory로 데이터베이스 파일이 저장될 폴더 경로를 얻은 후,
db.sqlite라는 파일명을 붙여 경로를 지정합니다.
Platform.isAndroid를 사용
안드로이드 기기에서 특정 SQLite 버전과의 호환성을 위해 applyWorkaroundToOpenSqlite3OnOldAndroidVersions 함수를 호출하여
구형 안드로이드 기기를 위한 해결 방법을 적용합니다.
임시 캐시 디렉터리를 설정하여 sqlite3.tempDirectory에 지정한 후, NativeDatabase.createInBackground를 통해
LazyDatabase를 생성하고 백그라운드에서 데이터베이스를 엽니다.
DB Table 변경시에는 어플을 삭제하고 재설치합니다.