안드로이드 데이터베이스(DB) 프로그래밍 2. [SQLiteDatabase] (Android Database 2)

2017. 4. 10. 17:35


1. 관계형 데이터베이스(Relational Database)

지난 글 [안드로이드 데이터베이스(DB) 다루기 1 - 관계형 데이터베이스]에서 관계형 데이터베이스(Relational Database)의 기본 개념과 용어, 그리고 데이터베이스를 다루기 위해 필수적으로 알아야 하는 SQL(Structured Query Language) 등에 대해 살펴보았습니다. 안드로이드에서 데이터베이스를 사용하는 구체적인 방법을 설명하기에 앞서, 데이터베이스 프로그래밍을 하기 위해서 미리 알아두면 좋을, 아니, 최소한 한번쯤은 살펴봐야 할 기초적인 데이터베이스 이론에 대해 설명하였죠.


안드로이드에서 기본적으로 제공되는 데이터베이스인 SQLite가 관계형 데이터베이스 구조를 따르고, 또한 표준으로 정의된 SQL 기능의 대부분을 지원하기 때문에, 안드로이드에서 SQLite를 사용하기 위해 데이터베이스 이론을 공부한다는 것은 상당한 도움이 되는 과정이라 할 수 있습니다. 만약, 데이터베이스를 처음 접하거나, 그 개념 또는 용어가 머리 속에 정리되어 있지 않다면, [안드로이드 데이터베이스(DB) 다루기 1 - 관계형 데이터베이스]에서 정리한 내용을 읽은 다음, 이 글의 내용을 살펴보시길 권해드립니다.


자, 이제 관계형 데이터베이스에 대한 내용에 이은 두 번째 주제로 넘어가서, 안드로이드에서 제공되는 SQLite 관련 클래스에 대한 구조 및 API 함수들의 사용법에 대해 설명할 차례가 되었습니다. 대부분의 내용이 데이터베이스 처리를 위한 SQL 문과 그 SQL을 실행하는 함수에 대한 설명, 그리고 함수의 실행 결과를 확인하는 방법들로 구성될텐데요, 여기서 설명하는 내용 정도만 이해하고 있어도 실질적인 구현 과정에서 큰 어려움없이 SQLite 를 사용할 수 있을거라 생각합니다.


그럼 지금부터 SQLite를 다루는 클래스에 대한 구조와 API 함수들의 사용법에 대해 설명하도록 하겠습니다.

2. 안드로이드에서 SQLite 사용하기.

앞서 설명했듯이 안드로이드에서 기본적으로 제공되는 데이터베이스는 SQLite입니다. SQLite는 비교적 작은 규모의 안드로이드 앱에서 사용하기 적합한 데이터베이스로써, SQLite에서 제공하는 몇 가지 API 함수를 호출하는 것만으로 데이터베이스 기능을 사용할 수 있는 특징이 있습니다.


안드로이드의 SQLite 관련 클래스 및 API 함수는 "android.database.sqlite" 패키지에 들어 있으며, 그 중 가장 중요한 클래스는 SQLiteDatabase 클래스입니다.


아래 그림은 앞으로 설명할 SQLite 데이터베이스를 사용하는 과정을 요약한 것입니다.

SQLiteDatabase 사용하기


2.1 SQLiteDatabase 클래스

SQLite 데이터베이스에 데이터를 추가(INSERT)하거나, 삭제(DELETE), 수정(UPDATE) 또는 조회(SELECT)를 하기 위해서는 SQLiteDatabase 클래스를 사용해야 합니다. 그리고 데이터가 저장될 테이블 생성 및 삭제, 수정 등의 기능 또한 SQLiteDatabase 클래스에서 제공되죠. (https://developer.android.com/reference/android/database/sqlite/SQLiteDatabase.html)


즉, SQLiteDatabase 클래스는 하나의 SQLite 데이터베이스를 다루기 위한 핵심 역할을 수행하는 클래스입니다. 그러므로 SQLite 데이터베이스 작업을 수행하기 전 반드시 SQLiteDatabase 클래스 객체의 참조를 획득해야 합니다.

SQLiteDatabase 클래스


일단 객체에 대한 참조를 획득하고 나면, SQLiteDatabase 클래스에 정의된 함수를 통해 데이터베이스 기능을 사용할 수 있습니다.


SQLiteDatabase 객체의 참조를 획득하는 것은 SQLite 데이터베이스 파일을 열거나, 새로운 파일을 생성함으로써 획득할 수 있습니다.

2.2 SQLite 데이터베이스 열기. (SQLiteDatabase 객체 참조 획득)

SQLite 데이터베이스를 사용하기 위해서는 가장 먼저 데이터베이스 파일을 열거나 생성해야 합니다. 이는 SQLiteDatabase 클래스에 정의된 몇 가지 static 함수를 통해 수행될 수 있습니다.

리턴 타입 메소드 이름
SQLiteDatabase openDatabase(String path, SQLiteDatabase.CursorFactory factory, int flags)
SQLiteDatabase openDatabase(String path, SQLiteDatabase.CursorFactory factory, int flags, DatabaseErrorHandler errorHandler)
SQLiteDatabase openOrCreateDatabase(File file, SQLiteDatabase.CursorFactory factory)
SQLiteDatabase openOrCreateDatabase(String path, SQLiteDatabase.CursorFactory factory, DatabaseErrorHandler errorHandler)
SQLiteDatabase openOrCreateDatabase(String path, SQLiteDatabase.CursorFactory factory)

표에 나와 있는 static 함수의 이름을 통해 알 수 있듯이, 데이터베이스를 여는 과정에서 만날 수 있는 상황은 크게 두 가지로 나뉠 수 있습니다. 이미 데이터베이스 파일이 존재하는 경우(openDatabase() 함수 사용)와 데이터베이스 파일이 없을 수도 있는 경우(openOrCreateDatabase() 함수 사용)가 바로 그것인데요. 주로 openOrCreateDatabase() 함수를 사용하여, 데이터베이스 파일 열기를 시도한 다음 만약 파일이 존재하지 않는다면 새로운 데이터베이스 파일을 생성하는 방법을 사용합니다.


아래의 코드는 openOrCreateDatabase() 함수를 사용하여 "sample.db" 파일을 여는 예제입니다. 이 때 만약 "sample.db" 파일이 존재하지 않는다면, 새로 만들게 됩니다.

[STEP-1] SQLiteDatabase - 데이터베이스 열기. (openOrCreateDatabase() 함수 호출.)
    SQLiteDatabase sqliteDB = null ;

    try {
        sqliteDB = SQLiteDatabase.openOrCreateDatabase("sample.db", null) ;
    } catch (SQLiteException e) {
        e.printStackTrace() ;
    }

2.3 SQLite 데이터베이스에 테이블 생성하기. (CREATE TABLE)

SQLite 데이터베이스 파일을 열어 SQLiteDatabase 객체의 참조를 확보했다면, 이제 객체의 참조를 통해 데이터베이스에 데이터를 추가하거나, 삭제 또는 조회 등의 작업을 수행할 수 있습니다. 하지만 데이터베이스 파일을 열었다고 해서 무턱대고 데이터를 추가할 순 없죠. 앞서 관계형 데이터베이스에 대해 설명할 때, 데이터베이스에 데이터를 저장하기 위해 데이터를 구조화하는 과정에 대해 간단히 언급하였습니다. 이 때 데이터 구조화의 결과로써, 데이터의 속성과 그 값의 관계를 나타내는 테이블(Table)이 만들어지는 것을 설명했습니다.


즉, SQLite 데이터베이스 파일을 열었다면, 다음 해야 할 일은 데이터베이스 내에 테이블(Table)을 생성하는 것입니다. SQLite 데이터베이스에 테이블을 만드는 방법은 테이블 생성을 위한 SQL 문자열을 SQLiteDatabase 클래스의 execSQL() 함수를 통해 전달하는 것입니다.


아래 예제 코드는 정수형(INTEGER) 데이터를 저장하기 위한 "NO" 필드와 문자열(TEXT) 데이터를 저장하기 위한 "NAME" 필드를 가지는 "ORDER_T"라는 테이블을 생성하는 코드입니다.

[STEP-2] SQLiteDatabase - 테이블 만들기. ("CREATE TABLE ... " 문을 execSQL()로 실행.)
    String sqlCreateTbl = "CREATE TABLE ORDER_T (NO INTEGER, NAME TEXT)" ;

    sqliteDB.execSQL(sqlCreateTbl) ;

그런데 "CREATE TABLE" SQL 문은 데이터베이스 파일이 생성되고나서 최초에 한번만 실행할 수 있습니다. 만약 생성하고자 하는 테이블과 같은 이름의 테이블이 이미 존재하는 상황에서 "CREATE TABLE" 명령을 실행하면, 다음과 같이 예외 상황이 발생합니다.

Caused by: android.database.sqlite.SQLiteException: table ORDER_T already exists (code 1): , while compiling: CREATE TABLE ORDER_T (NO INTEGER, NAME TEXT)

이런 경우, 테이블 중복 생성으로 인한 예외 상황(Exception)이 발생하는 것을 막기 위해서는 테이블이 존재하지 않는 경우에만 테이블을 새로 만들도록 만들어야 합니다. 이를 위해서는 "CREATE TABLE" 문에 "IF NOT EXISTS" 옵션을 추가하여 실행하면 됩니다. 예외 상황이 발생하는 문제를 해결한 코드는 아래와 같습니다.

[STEP-2.1] SQLiteDatabase - 테이블이 없는 경우 새로 만들기. ("CREATE TABLE IF NOT EXISTS ... " 문을 execSQL()로 실행.)
    String sqlCreateTbl = "CREATE TABLE IF NOT EXISTS ORDER_T (NO INTEGER, NAME TEXT)" ;

    sqliteDB.execSQL(sqlCreateTbl) ;

2.4 테이블에 데이터 추가, 수정, 삭제하기.

2.4.1 테이블에 데이터 추가하기. (INSERT)

SQLite 데이터베이스를 열고 테이블을 생성했다면, 이제 생성된 테이블에 데이터를 추가할 수 있습니다. 테이블에 데이터를 추가할 때는 "INSERT" 문을 사용합니다.


아래 예제 코드는 앞서 만든 "ORDER_T" 테이블의 "NO", "NAME" 필드에 각각 1과 "ppotta" 값을 추가하는 코드입니다. 앞서 테이블 생성 때와 마찬가지로 SQLiteDatabase 클래스의 execSQL() 함수를 사용합니다.

[STEP-3] SQLiteDatabase - 데이터 추가. ("INSERT INTO ... " 문을 execSQL()로 실행.)
    String sqlInsert = "INSERT INTO ORDER_T (NO, NAME) VALUES (1, 'ppotta')" ;

    sqliteDB.execSQL(sqlInsert) ;

"INSERT" 문을 실행하면 테이블에 새로운 데이터를 추가합니다. 이 때 새로 추가되는 값은 테이블 내 동일한 값을 가진 로우(Row)의 존재 여부와 관계없이, 새로운 로우(Row)로 추가됩니다.


그런데 어떤 상황에서는 새로운 값이 추가되는 것 대신, 기존에 저장된 로우(Row)의 값을 수정하고 싶을 때도 있을 것입니다. 물론 뒤에서 살펴볼 "UPDATE" 명령을 통해 데이터를 수정하는 것이 일반적이긴 하지만, "INSERT" 명령을 실행할 때 미리 조건에 맞는 데이터가 있으면, 새로운 로우(Row)를 추가하지 않고 이미 들어있던 로우(Row)의 값을 수정하도록 만들 수 있습니다. 이를 위해 "INSERT OR REPLACE" 문을 사용합니다.

[STEP-3.1] SQLiteDatabase - 데이터 추가. 이미 존재하면 수정. ("INSERT OR REPLACE INTO ... " 문을 execSQL()로 실행.)
    String sqlInsert = "INSERT OR REPLACE INTO ORDER_T (NO, NAME) VALUES (1, 'ppotta')" ;

    sqliteDB.execSQL(sqlInsert) ;
2.4.2 테이블 데이터 수정하기. (UPDATE)

테이블에 데이터가 추가되어 있다면, "UPDATE" 문을 사용하여 데이터의 내용을 수정할 수 있습니다.


아래 예제 코드는 ORDER_T 테이블의 "NO"와 "NAME" 필드 값을 각각 2, "ppotta2"로 수정하는 코드입니다.

[STEP-4] SQLiteDatabase - 데이터 수정. ("UPDATE ... " 문을 execSQL()로 실행.)
    String sqlUpdate = "UPDATE ORDER_T SET NO=2, NAME='ppotta2'" ;

    sqliteDB.execSQL(sqlUpdate) ;

그런데 위의 코드를 수행하면 테이블 내의 모든 행의 값이 수정됩니다. 이는 "UPDATE" 문이 실행될 데이터에 대한 조건이 지정되지 않았기 때문입니다. 만약 모든 로우(Row)가 아닌 특정 로우(Row)의 값만 수정하고자 한다면, 아래와 같이 "UPDATE" 문에 "WHERE"를 사용하여 조건을 추가하면 됩니다.


아래는 "WHERE"를 사용하여 "NO" 필드 값이 1인 로우(Row)에 대해서만 값을 수정하도록 만드는 코드입니다.

[STEP-4.1] SQLiteDatabase - 조건에 해당하는 데이터 수정. ("UPDATE ... WHERE ... " 문을 execSQL()로 실행.)
    String sqlUpdate = "UPDATE ORDER_T SET NO=2, NAME='ppotta2' WHERE NO=1" ;

    sqliteDB.execSQL(sqlUpdate) ;
2.4.3 테이블 데이터 삭제하기. (DELETE)

테이블에 저장되어 있는 데이터를 삭제하려면 "DELETE" 문을 사용합니다.


아래 예제 코드는 "ORDER_T" 테이블의 모든 데이터를 삭제하는 코드입니다.

[STEP-5] SQLiteDatabase - 데이터 삭제. ("DELETE ... " 문을 execSQL()로 실행.)
    String sqlDelete = "DELETE FROM ORDER_T" ;

    sqliteDB.execSQL(sqlDelete) ;

만약 "ORDER_T" 테이블의 데이터 중에서 특정 로우(Row)만 삭제하길 원한다면 "WHERE"를 추가하여 조건을 지정할 수 있습니다.


아래 코드는 "NO"의 값이 2인 모든 데이터를 삭제하는 예제입니다.

[STEP-5.1] SQLiteDatabase - 조건에 맞는 데이터 삭제. ("DELETE ... WHERE ... " 문을 execSQL()로 실행.)
    String sqlDelete = "DELETE FROM ORDER_T WHERE NO=2" ;

    sqliteDB.execSQL(sqlDelete) ;


2.5 테이블 데이터 조회하기.

테이블에 저장된 데이터를 조회하려면 "SELECT" 문을 사용합니다. 하지만 앞에서 살펴 본 데이터 추가, 수정, 삭제를 위한 SQL 문장을 실행할 때와는 다르게 추가적으로 알아두어야 할 요소가 두 가지 있습니다. 바로 쿼리(Query)와 커서(Cursor) 입니다.

2.5.1 쿼리(Query)

쿼리(Query)라는 단어를 우리 말로 표현할 때 주로 "질의"라는 용어를 사용합니다. "질의"라는 단어의 사전적 의미는, "의심나거나 모르는 점을 묻는 것"을 말합니다. 즉, 자신이 모르는 사실을 알기 위해, 누군가에게 질문하여 정보를 요청하는 것이 바로 "쿼리(Query)"라는 단어의 의미인 것이죠.


이제 관점을 데이터베이스로 옮겨보도록 하겠습니다. 데이터베이스에 저장된 데이터는, 그 정보를 획득하기 전까지는 사용자가 모르는(가지고 있지 않은) 정보입니다. 그래서 만약 그 정보를 얻기 위해서 데이터베이스 시스템에 정보를 요청한다면, 우리는 그것을 "데이터베이스에 쿼리(Query)"한다고 말할 수 있는 것입니다.


정리하자면, 쿼리(Query)란, 원하는 데이터를 얻기 위해 데이터베이스에 정보를 요청(Request)하는 것을 말하며, SQLite 데이터베이스에서 그 요청(Request)은 "SELECT" 문을 사용하여 작성할 수 있습니다.


그런데 앞서 살펴본 데이터 추가(INSERT), 수정(UPDATE), 삭제(DELETE)는 데이터베이스로 전달되는 단-방향 명령인데 반해, 조회(SELECT)를 위한 쿼리(Query)는 데이터베이스로부터 결과 데이터 전달이 필요한 양-방향 명령입니다. 그러므로 아무런 값도 리턴하지 않는 execSQL() 함수 대신, "SELECT" 문의 조건에 따라 선택된 레코드 집합(RecordSet)을 리턴하는 query() 함수 또는 rawQuery() 함수를 사용해야 합니다.


리턴 타입 메소드
Cursor query(boolean distinct, String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy, String limit)
Cursor query(String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy, String limit)
Cursor query(boolean distinct, String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy, String limit, CancellationSignal cancellationSignal)
Cursor query(String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy)
Cursor queryWithFactory(SQLiteDatabase.CursorFactory cursorFactory, boolean distinct, String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy, String limit, CancellationSignal cancellationSignal)
Cursor queryWithFactory(SQLiteDatabase.CursorFactory cursorFactory, boolean distinct, String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy, String limit)
Cursor rawQuery(String sql, String[] selectionArgs, CancellationSignal cancellationSignal)
Cursor rawQuery(String sql, String[] selectionArgs)
Cursor rawQueryWithFactory(SQLiteDatabase.CursorFactory cursorFactory, String sql, String[] selectionArgs, String editTable, CancellationSignal cancellationSignal)
Cursor rawQueryWithFactory(SQLiteDatabase.CursorFactory cursorFactory, String sql, String[] selectionArgs, String editTable)

많은 종류의 쿼리(Query) 함수가 제공되긴 하지만 많은 경우에 "SELECT" 문 전체를 한번에 전달할 수 있는 rawQuery() 함수가 주로 사용됩니다.


또한 모든 쿼리(Query) 함수가 리턴하는 레코드 집합(RecordSet)은 Cursor 인터페이스 타입으로 전달됩니다.


참고로 레코드 집합(RecordSet)이라는 용어는 쿼리(Query) 결과에 포함된 레코드(Record)의 묶음(Set)을 말하는 것입니다. 보통 데이터를 쿼리(Query)한 결과는 하나 또는 하나 이상의 레코드(Record)를 포함하고 있는데, 이렇게 리턴된 결과 레코드들의 집합을 레코드 집합(RecordSet)이라고 지칭합니다.


또한 레코드 집합(RecordSet)을 다른 용어로 로우 집합(RowSet)이라고 부르기도 합니다. 이전 글에서 관계형 데이터베이스 용어에 대해 설명할 때 "로우(Row)=레코드(Record)"라는 것을 설명하였으므로 그 연관성을 쉽게 이해할 수 있으리라 생각합니다.

2.5.2 커서(Cursor)

일반적인 컴퓨팅 환경에서 커서(Cursor)란, 화면에 표시된 내용에서 사용자가 내용 입력 또는 확인을 위해, 사용자가 현재 주시하고 있는 위치에 대한 표시를 말합니다. 키보드 커서 또는 마우스 커서 등이 대표적이죠.
그리고 데이터베이스에서도 그 의미는 크게 다르지 않습니다. 데이터베이스의 쿼리 결과로 리턴된 데이터에서 현재 그 내용을 확인하고 있는 위치를 나타내는 정보를, 커서(Cursor)라는 용어를 사용하여 지칭합니다.


데이터베이스에 저장된 데이터를 쿼리하면 그 결과 데이터는, 한 개의 레코드만 가지거나, 또는 여러 개의 레코드가 포함된 레코드 집합(RecordSet)입니다. 이 때 레코드 집합(RecordSet)에 들어 있는 개별 레코드에 접근하여 그 값을 확인할 수 있는 기능을 제공해주는 것이 바로 커서(Cursor)가 하는 역할인 것입니다. 그래서 SQLiteDatabase가 제공하는 모든 쿼리(Query) 관련 함수들은 Cursor 타입을 리턴하도록 되어 있습니다.


안드로이드에서 데이터베이스 커서(Cursor) 기능은 Cursor(android.database.Cursor) 인터페이스에 정의되어 있습니다. (https://developer.android.com/reference/android/database/Cursor.html)

Cursor 인터페이스


Cursor 인터페이스에는 많은 수의 함수가 정의되어 있습니다. 하지만 대부분의 경우, 몇 개의 함수 사용만으로 쿼리 결과로 리턴된 데이터를 처리할 수 있습니다.

아래 예제 코드는 Cursor 인터페이스를 사용하여 쿼리 결과 데이터를 탐색하는 과정을 나타낸 것입니다.

    Cursor cursor = sqliteDB.rawQuery("SELECT ...", null) ;

    while (cursor.moveToNext()) {
        // 첫 번째 컬럼(Column)이 INTEGER 타입인 경우.
        int val = cursor.getInt(0) ;

        // 두 번째 컬럼(Column)의 타입이 TEXT 인 경우.
        String str = cursor.getText(1) ;

        // 세 번째 컬럼(Column)이 REAL 타입으로 선언된 경우.
        float real = cursor.getFloat(2) ;

        // 코드 계속...
    }
2.5.3 데이터베이스 데이터 조회하기. (SELECT)

앞서 설명한 쿼리(Query) 함수 및 커서(Cursor)를 사용하여 테이블에 저장된 모든 데이터를 조회하는 코드는 아래와 같습니다.

[STEP-6] SQLiteDatabase - 데이터 조회. ("SELECT ... " 문을 rawQuery()로 실행.)
    String sqlSelect = "SELECT * FROM ORDER_T" ;
    Cursor cursor = null ;

    cursor = sqliteDB.rawQuery(sqlSelect) ;
    while (cursor.moveToNext()) {
        // INTEGER로 선언된 첫 번째 "NO" 컬럼 값 가져오기.
        int no = cursor.getInt(0) ;

        // TEXT로 선언된 두 번째 "NAME" 컬럼 값 가져오기.
        String name = cursor.getText(1) ;

        // TODO : use no, name.
    }

만약 "SELECT" 문에 조건을 추가하여 특정 데이터만 조회하고자 한다면, 다른 SQL 문과 마찬가지로 "WHERE"를 추가하여 조건을 기술할 수 있습니다.

[STEP-6.1] SQLiteDatabase - 조건을 추가하여 데이터 조회. ("SELECT ... WHERE ..." 문을 rawQuery()로 실행.)
    // NAME 컬럼 값이 'ppotta'인 모든 데이터 조회.
    String sqlSelect = "SELECT * FROM ORDER_T WHERE NAME='ppotta'" ;

    Cursor cursor = sqliteDB.rawQuery(sqlSelect) ;
    while (cursor.moveToNext()) {
        // INTEGER로 선언된 첫 번째 "NO" 컬럼 값 가져오기.
        int no = cursor.getInt(0) ;

        // TEXT로 선언된 두 번째 "NAME" 컬럼 값 가져오기.
        String name = cursor.getText(1) ;

        // TODO : use no, name.
    }

2.6 SQLite 데이터베이스 테이블 삭제하기.

SQLite 데이터베이스에 만들어져 있는 테이블을 앱 실행 중 삭제하는 경우는 매우 드뭅니다. 하지만 앱의 기능이 변경되고 데이터베이스에 저장되는 데이터 구조가 복잡해지면, 기존에 만들어놓은 테이블만으로는 복잡해진 데이터를 처리하는 게 힘들어질 수 있습니다. 이 때 개발자가 선택할 수 있는 방법 중 하나는, 앱이 업그레이드된 후 처음 실행될 때 새로운 테이블을 만들어 기존 테이블의 데이터를 옮긴 다음, 기존 테이블은 삭제하는 것입니다.


테이블을 삭제하기 위해서는 "DROP TABLE" 문을 사용합니다.

[STEP-7] SQLiteDatabase - 테이블 삭제. ("DROP TABLE ... " 문을 execSQL()로 실행.)
    String sqlDropTbl = "DROP TABLE ORDER_T" ;

    sqliteDB.execSQL(sqlDropTbl) ;

"DROP TABLE" 문을 사용하여 테이블을 삭제할 때 주의해야 할 점은, 삭제하기 전 저장되어있던 데이터 또한 삭제와 동시에 모두 지워진다는 것과, 삭제된 데이터와 테이블은 다시 복구가 불가능하다는 것입니다. 그러므로 삭제하고자 하는 테이블에 중요한 데이터가 저장되어 있다면, 반드시 백업을 해둔 다음 테이블을 삭제해야 합니다.

3. 참고.

.END.


ANDROID 프로그래밍/DB