ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [ Flutter / 플러터 ] SQFlite 문서 정리 및 예제로 마스터해보자.
    개발일지/flutter 2022. 10. 13. 12:30

    네 여러분 안녕하세요 개발하는남자 개남입니다. 

    오늘 다뤄 볼 내용은 SQFlite에 대해 정리해보려고 합니다. pub.dev에 문서가 정리되어있지만 sqflite사용에 필수적인 내용만을 추려서 간략하게 정리하면 좋을 것 같다는 생각에 이렇게 포스팅하게 되었습니다. 

     


    살펴볼 내용 

    •  sqflite란?
    •  sqflite 장점 / 단점 
    •  sqflite 사용 + 열고 / 닫고 
    •  sqflite 테이블 생성 스키마 
    •  sqflite 테이블 버전 관리 
    •  sqflite CRUD 사용
    •  sqflite transaction 관리
    •  sqflite batch 
    •  sqflite 지원되는 Type종류

     

     

    SQFlite란?

    SQFlite는 SQlite를 플러터에서도 사용할 수 있도록 지원하는 플러그인입니다.

    SQLite는 MySQL이나 PostgreSQL과 같은 관계형 데이터베이스 관리 시스템입니다. 하지만 다른 것이 있다면 서버용이 아닌 클라이언트 사이드 데이터 관리 시스템입니다. 클라이언트 사이드 시스템이기 때문에 비교적 가벼운 형태로 관리되고 있습니다. 
    <위키백과>

    지원되는 플렛폼은 Android , iOS , MacOS만 지원되고 있으며 Linux와 Window의 경우 SQflite_common_ffi 라이브러리를 통해 지원되고 있다고 합니다. Web의 경우 아쉽지만 지원되지 않습니다.

     

     

    SQFlite 장점/단점

    장점

     - 로컬 파일 기반 DB로 네트워크 사용이 필요없이 개발이 가능하기 때문에 서버 비용이나 네트워크 비용을 걱정할 필요가 없다.

     - 휘발성 데이터가 아니기 때문에 서비스 중단/종료가 되더라도 데이터 손실 없이 불러올 수 있다.

     - 트랜잭션/배치를 지원하기 때문에 데이터 무결성에 탁월하다.

     - 관계형 데이터 베이스로 통일성 있는 데이터를 관리 할 수 있다.

     - 쿼리를 지원하여 좀더 복잡한 데이터를 조회 sort 등을 할 수 있다.

    단점

     - 다른 로컬 기반의 데이터 저장 라이브러리에 비해 속도가 떨어진다.( Hive , Shared Preference ... ) 

     - Schema , rawQuery 사용시 문자 형태로 작성해야 하는 불편함이 있다. (오탈자 조심)

     

     

     

    sqflite 사용 + 열고 / 닫고 

    * sqflite는 Android / iOS에 별도의 환경설정이 필요 없이 바로 라이브러리만 Import 만 해주면 사용할 수 있습니다.

     

    플러터 cli 명령어로 sqflite 라이브러리 설치 

    $ flutter pub add sqflite

    또는 특정 버전을 pubspec.yaml 파일의 dependencies 항목에 추가하셔도 상관없습니다.

    dependencies:
      flutter:
        sdk: flutter
    
      sqflite: ^2.0.3+1

    Database 열기

    sqflite는 파일 기반의 데이터베이스이기 때문에 데이터 베이스 파일을 만들어야 합니다. 

    파일이 존재하지 않아도 괜찮습니다. 없으면 자동으로 만들어줍니다. 

    var db = await openDatabase('my_db.db');

    Database 닫기

    공식문서에 따르면 열린 Database를 닫을 필요는 없다고 합니다. 이유는 앱이 종료되는 시점에 같이 종료되기 때문이라고 합니다. 

    그럼에도 데이터 접근을 해제하려고 한다면 close함수를 통해 해제할 수 있습니다. 

    await db.close();

    존재하는 DB를 사용하고 싶을 때

    DB초기화 시점에 존재하는 DB가 있다면 해당 db파일로 초기화를 진행할 수 있습니다. 

    1. assets폴더 하위로 원하는 위치에 db파일을 위치시킨다.

    assets/examples.db
    또는
    assets/dbs/examples.db

    2. pubspec.yaml파일에 assets 사용할 수 있도록 경로 설정

    flutter:
      assets:
        - assets/example.db
        또는
        - assets/dbs/example.db

    3. database 위치시킬 경로 확인 

    var databasesPath = await getDatabasesPath();
    var path = join(databasesPath, "demo_asset_example.db");

    4. database 파일이 존재 여부 확인 후 존재하지 않는 최초 초기화 시점에서만 assets에 위치시킨 db파일을 카피한다.

    var exists = await databaseExists(path);
    
    if (!exists) {
    	try {
            await Directory(dirname(path)).create(recursive: true);
    	} catch (_) {}
        ByteData data = await rootBundle.load(join("assets", "example.db"));
        List<int> bytes = data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes);
        await File(path).writeAsBytes(bytes, flush: true);
    }

    5. database 열기 

    var db = await openDatabase(path, readOnly: true);

     

     

    sqflite 테이블 생성 스키마 

    openDatabase를 할 때에 option값으로 onCreate 함수를 사용할 수 있습니다. onCreate 함수 외에도 version, onConfigure, onUpgrade, onDowngrade를 사용할 수 있습니다. (version 관리 항목에서 살펴보겠습니다.)

     

    openDatabase시 데이터베이스가 존재하지 않으면 onCreate함수가 호출됩니다. 이때에 원하는 DB table들을 만들어주면 됩니다.

    var databasesPath = await getDatabasesPath();
    String path = join(databasesPath, 'sample.db');
    var db = await openDatabase(path,
      version: 1,
      onCreate: _onCreate);
      
    _onCreate(Database db, int version) async {
      await db.execute(
        "CREATE TABLE Sample (id INTEGER PRIMARY KEY AUTOINCREMENT, value TEXT NOT NULL)");
    }

    db.excute 함수로 String 작성한 Schema로 테이블이 생성되며, sql Schema작성법을 따르면 됩니다. (대문자 소문자 상관은 없지만 대게 Schema작성 시 대문자로 사용합니다. 단, 테이블명과 칼럼 명의 경우 원하는 방식으로 작성하셔도 됩니다. 

     

    앱이 정상적으로 실행되면 문제없이 테이블 세팅이 되었다고 생각하면 되지만 직접 눈으로 확인하고 싶을 때는 만들어진 db파일을 직접 열어보는 것입니다. 

     

     - 에뮬레이터 사용한 경우 db파일 체크 하기 @developerelen 님 블로그 참조

     

    [Flutter] sqflite database file 찾기

    sqflite 라이브러리를 사용하여 database를 CRUD 한 후, 터미널로 .db 파일을 읽어서 정말 파일에 저장이 되었는지 확인하는 방법입니다.openDatabase 메소드로 DB를 오픈할 때 사용한 주소를 로그에 찍습

    velog.io

     - 물리 디바이스를 사용한 경우 db파일 체크 하기 @developerelen 님 블로그 참조

     

    [Flutter] 앱에서 다운받은 파일 adb로 복사하기

    adb(Android Debug Bridge)는 기기와 통신할 수 있는 다목적 명령줄 도구입니다. adb 명령어는 앱의 설치 및 디버깅과 같은 다양한 기기 작업에 도움이 되며, 기기에서 다양한 명령어를 실행하는 데 사용

    velog.io

     

     

    sqflite 테이블 버전 관리 

    버전 관리가 필요한 이유는 서버를 통해 데이터를 받는 방식이 아닌 로컬 파일 기반의 데이터이기 때문에 앱의 버전에 따라 데이터 구조가 변화될 텐데 그럴 때 DB Schema도 업그래드가 필요합니다. 때로는 다운그래드도 필요할테고요, 그럴때 사용하는 것이 버전입니다. 버전에 따라 onUpgrade 또는 onDowngrade가 호출되게 됩니다.

    var db = await openDatabase(
    	path, 
        version: 1, 
        onCreate: _onCreate ,
        onUpgrade: _onUpgrade , 
        onDowngrade: _onDowngrade
    );

    현 version 숫자가 이전 버전보다 높으면 onUpgrade가 호출되고 

    현 version 숫자가 이전 버전보다 낮으면 onDowngrade가 호출됩니다.

    공식 문서에 따르면 웬만하면, 버전을 높이면서 작업하라고 합니다. 즉, downgrade는 웬만해서 사용하지 말아야 한다는 뜻으로 이해했습니다.

     

    테이블 버전 관리 테스트를 위해 시나리오를 준비했습니다. 

    1. User 정보를 저장할 table을 만들어줍니다. 유저에 정보로는 처음에는 이름과, 나이, id 이 정도만 저장합니다.
    2. 앱을 실행하여 임의의 User정보를 저장합니다. 
    3. 시간이 지났다고 가정하고, User정보에 닉네임 데이터를 추가적으로 넣는 것으로 기획이 변경되었습니다. 
    4. onUpgrade 함수를 사용하지 않고 데이터를 넣으려고 했을 때 어떤 현상이 발생되는지 확인.
    5. onUpgrade 함수를 추가했지만 version을 변경하지 않고 사용한 경우
    6. onUpgrade 함수를 통해 Schema 업데이트 후 데이터를 넣었을 때 현상 확인.

    1.

    var databasesPath = await getDatabasesPath();
    String path = join(databasesPath, 'user.db');
    var db = await openDatabase(path, version: 1, onCreate: _onCreate);
      
      
    _onCreate(Database db, int version) async {
      await db.execute('''
      	CREATE TABLE user (
        	id INTEGER PRIMARY KEY AUTOINCREMENT, 
        	name TEXT NOT NULL UNIQUE, 
            age INTEGER NOT NULL)
        '''
      );
    }

    name에 unique 옵션을 넣어 실행할 때마다 중복 저장되지 않도록 테이블을 생성했습니다. 

     

    2.

      await Future.delayed(const Duration(milliseconds: 3000));
      var id = await db.insert('user', {'name': '김성덕', 'age': 19});

    딜레이 3초 후에 사용자를 insert 해줬습니다.  정상적으로 앱이 실행이 되었고 , Unique 가 정상적으로 되는지 확인하기 위해 

    일부로 동일 데이터를 insert 해봤습니다. 다음과 같은 오류 메시지를 받았습니다.

    [ERROR:flutter/lib/ui/ui_dart_state.cc(198)] Unhandled Exception: DatabaseException(UNIQUE constraint failed: user.name (code 2067 SQLITE_CONSTRAINT_UNIQUE[2067])) sql 'INSERT INTO user (name, age) VALUES (?, ?)' args [김성덕, 19]

    3. 

    await Future.delayed(const Duration(milliseconds: 3000));
    // var id = await db.insert('user', {'name': '김성덕', 'age': 19});
    var id = await db.update(
        'user',
        {'nickname': '개발하는남자'},
        where: 'name = ?',
        whereArgs: ['김성덕'],
    );
    print(id);

    insert문을 주석처리하고 update 문으로 변경했습니다. 이때, 아직 Schema 변경 전으로 sql db에는 nickname 칼럼이 존재하지 않는 상태입니다. 

     

    4.

    no such column: nickname (code 1 SQLITE_ERROR[1]

    실행결과 no such column 오류로 인해 데이터 업데이트가 실패되었습니다. (원하는 결과를 얻었습니다.)

     

    5.

    var db = await openDatabase(
        path,
        version: 1,
        onCreate: _onCreate,
        onUpgrade: _onUpgrade,
    );
      
      
    _onUpgrade(Database db, int oldVersion, int newVersion) async {
      var batch = db.batch();
      if (oldVersion == 1) {
        batch.execute('ALTER TABLE user ADD nickname TEXT');
      }
      await batch.commit();
    }

    업그래드함수를 추가했지만 버전이 바꾸지 않았습니다 결과는 4번과 같이 오류 메시지를 받았습니다.

     

    6.

      var db = await openDatabase(
        path,
        version: 2,
        onCreate: _onCreate,
        onUpgrade: _onUpgrade,
      );

    결과는 이상 없이 업데이트됨을 확인하였습니다.

    좀 더 디테일한 version관리 예제는 링크를 통해서 확인해보세요.

     

     

    sqflite CRUD 사용

    간단 sqflite CRUD이며 별도의 ui와 model 설계 없이 사용방법만 정리하겠습니다. 

    model과 ui를 통해 crud를 활용하는 것은 제 유튜브에 [Flutter] 로컬 데이터 관리 sqflite로 손쉽게 처리 하자 영상을 참고해주세요.

     

    - Create

    // var <생성후 ID값> = await db.insert( <테이블명> , <저장시킬 데이터 (Map<String,Object> 타입)> );
    // 방식 1
    var id = await db.insert('user', {'name': '김성덕', 'age': 19});
    
    // 방식 2
    var rawId = await db.rawInsert(
          'INSERT INTO user(name, age, nickname) VALUES("개남", 22, "개발하는사람")'
    );
    
    // 방식 3
    var rawId1 = await db.rawInsert(
          'INSERT INTO user(name, age, nickname) VALUES(?,?,?)',
          ["개남1", 24, "개발하는사람2"]
    );

     

    - Read

    // var <조회된 데이터> = await db.query(<테이블명> , where : <조건식> , whereArgs : <조건식에 비교할 값(Array)>)
    // 방식 1
    var user = await db.query('user', where: 'name = ?', whereArgs: ['김성덕']);
    
    // 방식 2
    var rawUser = await db.rawQuery('SELECT * FROM user WHERE name="개남"');
    
    // 방식 3
    var rawUser1 = await db.rawQuery('SELECT * FROM user WHERE name=?', ['개남1']);

     

    - Update

    // 방식 1
    var id = await db.update(
        'user',
        {'nickname': '개발하는남자'},
        where: 'name = ?',
        whereArgs: ['김성덕'],
    );
    
    // 방식 2
    var updateID = await db.rawUpdate(
          'UPDATE user SET name = "개남개남", age = 34 WHERE name = "개남"'
    );
    
    // 방식 3
    var updateID = await db.rawUpdate(
          'UPDATE user SET name = ?, age = ? WHERE name = ?', ['개남개남', 34, '개남']
    );

     

    - Delete

    // 방식 1
    var isDeleted = await db.delete('user', where: 'name = ?', whereArgs: ['개남개남']);
    
    // 방식 2
    var isDeleted1 = await db.rawDelete('DELETE FROM user WHERE name = "김성덕"');
    
    // 방식 3
    var isDeleted2 = await db.rawDelete('DELETE FROM user WHERE name = ?', ['김성덕']);

    삭제 완료 시 return으로 1 값을 값이 없어 삭제가 안되거나 삭제될 수 없을 때 0 값을 return 합니다. 

     

     

    sqflite transaction 관리

    Transaction 은 sql 데이터 베이스뿐 아니라 모든 데이터를 관리하는 시스템에서는 transaction  관리를 해줘야 합니다. 

    Transaction 은 쉽게 말해 정합성이 깨지는 것을 방지하기 위한 방법? 기능?이라고 생각하면 됩니다. 

    예를 들어 사용자가 가입 시 포인트를 지급하는 로직이 있다고 가정한다면 사용자 가입과 포인트 등록이라는 두 가지 테이블에 저장을 시켜야 합니다. 사용자 저장 후 포인트 등록을 하려는데 포인트 등록에서 문제가 발생하여 오류가 발생했다고 가정한다면 사용자 가입이 되지 말아야 합니다(물론 이 부분은 서비스의 규칙이 어떠한지에 따라 다르겠지만 여기에서는 가정하겠습니다.)  이럴 때 필요한 것이 Transaction입니다. 

     

    시나리오

    1. 회원 가입으로 데이터 insert 
    2. 1초 정도 Delay를 주고 조금 전 저장한 데이터가 잘 들어가 있는지 확인
    3. 강제로 오류를 발생시킴 ( name 값은 unique 값이기 때문에 일부로 동일한 값을 insert 시켜 오류를 발생시킴 )
    4. 오류 후에 2. 번에서 했던 것처럼 조회하여 트렌젝션 처리가 잘 되었는지 확인.
      try {
        await db.transaction((txn) async {
          await txn.insert('user', {'name': '개남', 'age': 22, 'nickname': '개발하는남자'});
          await Future.delayed(const Duration(milliseconds: 1000));
          var data = await txn.query('user', where: 'name = ?', whereArgs: ['개남']);
          print(data); // 1번 print
          await txn.insert('user', {'name': '개남', 'age': 25, 'nickname': '개남형'});
        });
      } catch (e) {
        print(e);
      }
      var user = await db.query('user', where: 'name = ?', whereArgs: ['개남']);
      print(user); // 2번 print

    위 시나리오처럼 진행 시 1번 print에서는 저장된 '개남' 사용자를 찾을 수 있었지만  2번 print에서는 트렌젝션의 결과로 '개남' 사용자를 찾을 수 없었습니다. 정상적으로 처리됨을 확인했습니다.

     

     

     

    sqflite batch 사용

    Batch는 여러 가지 작업들을 그룹화시켜 한 번에 실행 시켜주는 기능을 말합니다. insert / update / delete 등등 한번에 실행하고 싶을때 사용할 수 있으며 commit 과정중 오류가 발생하면 성공된 데이터까지 원복 시켜버립니다. 위에서 다뤄본 transaction과도 유사하다고 볼 수 있는데 다른점이 있다면 transaction의 경우 같은 transaction 영역 안에서는 insert후 조회 및 별도의 작업등을 수행할 수 있는 반면 batch의 경우 작업목록을 정의만 할뿐 commit 전에 데이터를 조회하여 어떤 작업을 하거나 데이터를 변경하거나 할 수 없습니다. 말그대로 Queue와 같이 작업을 쌓아놓고 한번에 처리하되 순차적으로 작업되는 것을 확인하였습니다. 

      var batch = db.batch();
      batch.insert('user', {'name': '배치작업자', 'age': 31, 'nickname': 'batchwWrker'}); //1번
      batch.insert('user', {'name': '배치', 'age': 22, 'nickname': 'batch'});//2번
      batch.delete('user', where: 'name =?', whereArgs: ['배치']);//3번
      batch.update('user', {'name': '배치'}, where: 'name =?', whereArgs: ['배치작업자']);//4번
      await batch.commit();//5번
      
      var rawUser = await db.rawQuery('SELECT * FROM user WHERE name=?', ['배치']);
      print(rawUser);//6번

    batch가 순차적 실행이라는 것을 확인하기 위해 위 와 같은 시나리오를 만들어봤습니다. 

     

    시나리오

    1. '1번'과 '2번'으로 사용자 두 명을 저장시킵니다.
    2. '4번' 작업을 실행하면 배치 작업자 > 배치로 변경되기 때문에 동일 name이 발생하여 오류가 발생될 것입니다. 그 오류를 방지하고자 '3번'작업을 통해 원래 '배치'유저를 제거시켰습니다. 
    3. '5번'작업이 완료되는 시점에 오류가 발생되지 않음을 확인합니다.
    4. '6번'작업으로 조회된 '배치'사용자가 기존 '배치 작업자' user임을 확인합니다.

     

    작업 결과는 예상했던 결과를 얻을 수 있었으며 추가적으로 시나리오 2번의 오류 방지 부분을 빼고 작업할 시 update에서 오류가 발생됨을 확인하였습니다. 즉, 순차적 실행임을 알 수 있었습니다.

     

     

     

    sqflite 지원되는 Type종류

    sqflite의 경우 지원되는 종류를 알아보기 전에 Table과 Column의 이름으로 사용하면 안 되는 이름을 살펴보겠습니다. 

    [
    	"add","all","alter","and","as",
        "autoincrement","between","case",
        "check","collate","commit","constraint",
        "create","default","deferrable","delete",
        "distinct","drop","else","escape","except",
        "exists","foreign","from","group","having",
        "if","in","index","insert","intersect","into",
        "is","isnull","join","limit","not","notnull",
        "null","on","or","order","primary","references",
        "select","set","table","then","to","transaction",
        "union","unique","update","using","values","when","where"
    ]

    위의 이름으로 테이블명이나 칼럼명을 사용하게 될 경우 문제가 발생할 수 있습니다. 하여 이름을 작명하실 때에 위 사항을 유의하시기 바랍니다.

     

    Sqflite 지원되는 Type

    Integer (dart : int), Real(dart : num) , Text(dart : String ), Blob (dart : Unit8List ) 이렇게 지원이 되고 있습니다. 

    추가적으로 DateTime과 , bool 타입의 경우 형변환을 통해 저장시킬 수 있습니다.  (우회하는 방안)

    DateTime의 경우 millisSinceEpoch (int) 형태로 저장시킬 수 있고 추가적으로 String 값으로도 저장할 수 있습니다.

    bool의 경우 true를 1로 false를 0으로 저장하여 관리할 수 있습니다. 

     

    DateTime의 경우는 많은 상황에서 쿼리 질의를 사용하기 때문에 DateTime을 조건으로 쿼리 질의를 사용해 보겠습니다.

    millisSinceEpoch을 사용해 int로 저장도 하고 String으로 저장해서 둘 다 쿼리 질의가 되는지 확인해 보겠습니다.

      var batch = db.batch();
      List.generate(
          100,
          (index) => batch.insert('user', {
                'name': '배치작업자$index',
                'age': index,
                'createdAt': DateTime(2022, 10, 01, 10, 20)
                    .add(Duration(days: index))
                    .millisecondsSinceEpoch,
                'updatedAt': DateTime(2022, 10, 01, 10, 20)
                    .add(Duration(days: index))
                    .toIso8601String(),
              }));
    
      await batch.commit();

    2022년 10월 1일부터 하루에 한 명씩 가입한 데이터를 저장시켰습니다. 위 batch로 얻은 결과 데이터는 아래와 같습니다.

      var users = await db.query('user',
          where: 'createdAt > ?',
          whereArgs: [DateTime(2023, 01, 01, 0, 0).millisecondsSinceEpoch]);
      for (var user in users) {
        print(user);
      }
    
      var updateUsers = await db
          .query('user', where: 'updatedAt > ? and updatedAt < ?', whereArgs: [
        DateTime(2023, 01, 01, 0, 0).toIso8601String(),
        DateTime(2023, 01, 04, 0, 0).toIso8601String()
      ]);
      for (var user in updateUsers) {
        print(user);
      }

    실행해본 결과 아래와 같이 잘 출력되는 것을 확인할 수 있었습니다. 

     


    이것으로 플러터의 Sqflite에 대한 전반적인 기능을 예제로 살펴보면서 문서를 정리해보았습니다. 

    도움이 되셨길 바라면서 도움이 되셨다면? 좋아요 / 구독 부탁드리고 앞으로도 좋은 포스팅을 공유하도록 노력하겠습니다~! 

    감사합니다.

    댓글

Designed by Tistory.