Flutter - 로컬 데이터베이스 ①: sqflite(생성, 삽입, 업데이트, 삭제, 쿼리)

3 분 소요


목차

1: Flutter - 로컬 데이터베이스 ①: sqflite(생성, 삽입, 업데이트, 삭제, 쿼리) <현재>

  • sqflite를 사용하여 Specs(id, type, category, method, contents, money, dateTime)라는 로컬 데이터베이스를 생성
  • 로컬 데이터베이스에 접근하는 SpecProvider 클래스를 생성하고, DB에 읽고 쓰는 함수들을 정의

2: Flutter - 로컬 데이터베이스 ②: Future(데이터베이스 화면에 띄우기, FutureBuilder)

  • DB에 접근하는 것은 async한 작업
  • 이를 Future 키워드를 사용해 다루고, FutureBuilder로 화면에 값을 보여줌

3: Flutter - 로컬 데이터베이스 ③: Singleton, factory

  • DB 인스턴스를 Singleton으로 생성

4: Flutter - 로컬 데이터베이스 ④: 이미지 파일을 데이터베이스에 저장하고 읽기

  • 이미지 파일을 데이터베이스에 저장하고 읽기


플러터에서 로컬 데이터베이스를 써 보자
sqflite와 path를 써서 그냥 SQL처럼 쓸 수 있다.

보기 편하게 DBHelper.dart 파일을 만들어서 그 안에 메소드들을 모았다

사용 준비

dependencies:
  flutter:
    sdk: flutter
  sqflite:
  path:

pubspec.yaml의 dependencies에 sqflitepath를 추가해준다.

import 'dart:async';
import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart';

dart 파일에 해당 패키지를 임포트하면 끝
async도 DB는 비동기로 접근하기 때문에 필요하다


모델 만들기

class Spec {
  int? id;
  int type;
  String? category;
  int? method;
  String? contents;
  int money;
  String? dateTime;

  Spec({this.id, required this.type, this.category, this.method, this.contents, required this.money, this.dateTime});

  Map<String, dynamic> toMap(){
    return {
      'id': id,
      'type': type,
      'category': category,
      'method': method,
      'contents': contents,
      'money': money,
      'dateTime': dateTime,
    };
  }

나는 지출/수입 내역을 저장하는 Spec을 모델로 만들었다.

id는 실제 사용하는 Spec의 내용은 아니고, update하거나 delete할 때 접근하기 위해 부여 받은 고유한 수로, 즉 primary key다.
DB에 새 Spec을 insert할 때마다 새 id를 부여받을 수 있다. 아래에 기술

필수 항목으로 Null 값이 되어선 안 되는 것이 있다면 생성자에 required를 넣어서 표시해줄 수 있다. DB 테이블 만들 때 Not Null 항목들

toMap()은 Spec을 map 타입으로 변환해준다. DB에 실제로 insert할 땐 map 타입이어야 하기 때문이다.


참고: DateTime 타입은 불가능

https://pub.dev/packages/sqflite

DateTime is not a supported SQLite type. Personally I store them as int (millisSinceEpoch) or string (iso8601)

DateTime은 SQLite 타입에 없기 때문에 String 등으로 변환해서 넣자


데이터베이스 만들기

class SpecProvider {
  late Database _database;

  Future<Database?> get database async {
    _database = await initDB();
    return _database;
  }

  initDB() async {
    String path = join(await getDatabasesPath(), 'Specs.db');
    return await openDatabase(
        path,
        version: 1,
        onCreate: (db, version) async {
          await db.execute('''
            CREATE TABLE Specs(
              id INTEGER PRIMARY KEY AUTOINCREMENT,
              type INTEGER NOT NULL,
              category TEXT,
              method INTEGER, 
              contents TEXT,
              money INT NOT NULL,
              dateTime TEXT
            )
          ''');
        },
        onUpgrade: (db, oldVersion, newVersion){}
     );
  }
}

SpecProvider 클래스를 만들었다.
DB에 접근하는 거는 시간이 오래 걸릴 수 있기 때문에 비동기로 동작한다.

initDB()로 DB를 가져온다. getDatabasesPath()로 안드로이드, IOS등 각 기기에 저장된 DB의 위치를 가져올 수 있다.
openDatabase()로 DB를 여는데, 만약 첫 사용자라서 DB가 비어 있는 경우, 새 DB를 만들어 준다.
execute()에 쿼리문으로 테이블을 CREATE해 주면 된다.
id는 Primary Key로 AUTOINCREMENT 속성을 주었는데, 위에서 말했듯이 DB에 새 Spec을 insert할 때마다 새 id를 부여한다.


데이터베이스 접근하기

이제 DB에 직접 넣고, 업데이트하고, 삭제할 수 있어야 한다.
SpecProvider 클래스 안에 함수들을 만들어 준다.

DB 전체 들고 오기(db.query())

  Future<List<Spec>> getDB() async {
    final db = await database;
    final List<Map<String, dynamic>> maps = await db!.query(tableName);
    if( maps.isEmpty ) return [];
    List<Spec> list = List.generate(maps.length, (index) {
      return Spec(
        id: maps[index]["id"],
        type: maps[index]["type"],
        category: maps[index]["category"],
        method: maps[index]["method"],
        contents: maps[index]["contents"],
        money: maps[index]["money"],
        dateTime: maps[index]["dateTime"],
      );
    });
    return list;
  }

db.query(tableName)으로 List<Map<String, dynamic>> maps에 DB를 다 들고 올 수 있다.
그럼 이제 map들 List를 원래 모델인 Spec들 List로 변환해서 리턴하면 끝이다.
List.generate()로 간단하게 바꿀 수 있다.

db!.query(
        tableName,
        columns: columnsToSelect,
        where: whereString,
        whereArgs: whereArguments);

이런 식으로 sql의 WHERE 절도 쓸 수 있다.


DB에 쿼리 날려서 들고 오기(db.rawQuery())

db.query()도 쿼리를 날리는 거긴 하지만, 그것보다는 그냥 sql문이 개인적으로 더 편해서 나는 이 쪽이 좋은 것 같다

  Future<List<Spec>> getQuery(String query) async {
    final db = await database;
    final List<Map<String, dynamic>> maps = await db!.rawQuery(query);
    if( maps.isEmpty ) return [];
    List<Spec> list = List.generate(maps.length, (index) {
      return Spec(
        id: maps[index]["id"],
        type: maps[index]["type"],
        category: maps[index]["category"],
        method: maps[index]["method"],
        contents: maps[index]["contents"],
        money: maps[index]["money"],
        dateTime: maps[index]["dateTime"],
      );
    });
    return list;
  }

rawQuery()를 쓰면 sql문을 쓸 수 있다.
나머지는 위랑 똑같음


DB insert

  Future<void> insert(Spec spec) async {
    final db = await database;
    spec.id = await db?.insert(tableName, spec.toMap());
  }


DB update

  Future<void> update(Spec spec) async {
    final db = await database;
    await db?.update(
      tableName,
      spec.toMap(),
      where: "id = ?",
      whereArgs: [spec.id],
    );
  }

primary key인 id로 접근해서 해당 Spec을 업데이트해 줬다.


DB delete

  Future<void> delete(Spec spec) async {
    final db = await database;
    await db?.delete(
      tableName,
      where: "id = ?",
      whereArgs: [spec.id],
    );
  }

primary key인 id로 접근해서 해당 Spec을 삭제한다.



굿
처음 Null check 문제로 좀 애먹었는데 지금 보니 간단하고 좋다

댓글남기기