안드로이드 실전 개발 데이터베이스편 파트2 입니다. 전편에 이어 바로 시작하도록 하겠습니다.

전체 강좌 목차

[강좌A01] Moteodev Studio를 이용한 안드로이드 개발 환경 구축 가이드
[강좌A02] 안드로이드 개발 참고 서적 소개
[강좌A03] Android 실전 개발 - 아이디어 / 기획 / Wireframe
[강좌A04] 안드로이드 실전 개발 - 아이콘 제작
[강좌A05] 안드로이드 실전 개발 - 레이아웃 및 리소스 : Part1
[강좌A06] 안드로이드 실전 개발 - 레이아웃 및 리소스 : Part2
[강좌A07] 안드로이드 실전 개발 - 리소스 해킹
[강좌A08] 안드로이드 실전 개발 - SQLite

[강좌A09] 안드로이드  실전 개발 - 데이터베이스 : Part1
[강좌A10] 안드로이드  실전 개발 - 데이터베이스 : Part2

다음으로 살펴볼 클래스는 General DatabaseHelper Class 입니다. 이 클래서는 SQLiteOpenHelper를 상속받아서 Database 생성 및 업그레이드, 연결 등의 작업을 담당하고 있으며, 외부 파일에서 SQLiteDatabase instance를 직접 핸들링 하지 않게 하기 위해서 database C/R/U/D 작업을 Wrapping 하고 있습니다. 또한 여러 Activity에서 DB Connection 관련 문제를 해결하기 위해서 Singleton으로 만들었습니다.

//DatabaseHelper.java


package com.overoid.hangul2english.data;

 

import com.overoid.hangul2english.Constants;

import com.overoid.hangul2english.data.H2eDatabaseCreator;

 

import android.content.ContentValues;

import android.content.Context;

import android.database.Cursor;

import android.database.SQLException;

import android.database.sqlite.SQLiteDatabase;

import android.database.sqlite.SQLiteDatabase.CursorFactory;

import android.database.sqlite.SQLiteException;

import android.database.sqlite.SQLiteOpenHelper;

import android.util.Log;

 

public class DatabaseHelper extends SQLiteOpenHelper {

 

    private static final String CLASSNAME = DatabaseHelper.class.getSimpleName();

    private static final String KEY_COLUMN = "_id";

   

    private static DatabaseHelper mInstance;

    private static SQLiteDatabase db; 

   

    /***

     * 생성자

     *

     * @param context   : app context

     * @param name      : database name

     * @param factory   : cursor Factory

     * @param version   : DB version

     */

    private DatabaseHelper(Context context, String name, CursorFactory factory, int version) { 

        super(context, name, factory, version); 

        Log.v(Constants.LOG_TAG,  DatabaseHelper.CLASSNAME + "Create or Open database : "+name);

    }

   

    /***

     * 생성자

     *

     * @param context   : app context

     */

    private DatabaseHelper(final Context context) {

        super(context, DatabaseCreator.DB_NAME , null, DatabaseCreator.DB_VERSION);

        Log.v(Constants.LOG_TAG,  DatabaseHelper.CLASSNAME + "Create or Open database : "+ DatabaseCreator.DB_NAME);

    }

   

    /***

     * Initialize method

     *

     * @param context       : application context

     */

    private static void initialize(Context context) { 

        if(mInstance == null) { 

 

            Log.i(Constants.LOG_TAG, DatabaseHelper.CLASSNAME + "Try to create instance of database (" + DatabaseCreator.DB_NAME + ")");

            mInstance = new DatabaseHelper(context);

           

            try {              

                Log.i(Constants.LOG_TAG, "Creating or opening the database ( " + DatabaseCreator.DB_NAME + " ).");               

                db = mInstance.getWritableDatabase();          

            } catch (SQLiteException se) {               

                Log.e(Constants.LOG_TAG, "Cound not create and/or open the database ( " + DatabaseCreator.DB_NAME + " ) that will be used for reading and writing.", se);      

            }

            Log.i(Constants.LOG_TAG,  DatabaseHelper.CLASSNAME + "instance of database (" + DatabaseCreator.DB_NAME + ") created !");

        } 

    }

   

    /***

     * Static method for getting singleton instance

     *

     * @param context       : application context

     * @return              : singleton instance

     */

    public static final DatabaseHelper getInstance(Context context) { 

        initialize(context); 

        return mInstance; 

    } 

   

    /***

     * Method to close database & instance null

     */

    public void close() {       

        if(mInstance != null) {           

            Log.i(Constants.LOG_TAG, DatabaseHelper.CLASSNAME + "Closing the database [ " + DatabaseCreator.DB_NAME + " ].");           

            db.close();           

            mInstance = null;       

        }   

    }

   

    /***

     * Method for select table

     * db.query wrapper 

     * @param table     : table name

     * @param columns   : column name array

     * @return          : cursor

     */

    public Cursor get(String table, String[] columns){       

        return db.query(table, columns, null, null, null, null, null);   

    }       

   

    /***

     * Method for select table

     * @param table     : table name

     * @param columns   : column name array

     * @param id        : record id (pk 컬러명은 "_id" 가능함)

     * @return          : cursor

     */

    public Cursor get(String table, String[] columns, long id){       

        Cursor cursor = db.query(true, table, columns, KEY_COLUMN + "=" + id, null, null, null, null, null);       

        if (cursor != null) {          

            cursor.moveToFirst();       

        }       

        return cursor;   

    }

   

    /****

     * Method for select statements

     * @param sql       : sql statements

     * @return          : cursor

     */

    public Cursor get(String sql) {

        return db.rawQuery(sql, null);

    }

   

    /***

     * Method to insert record

     * @param table     : table name

     * @param values    : ContentValues instance

     * @return          : long (rowid)

     */

    public long insert(String table, ContentValues values) {

        return db.insert(table, null, values);

    }

   

    /***

     * Method to update record

     * @param table     : table name

     * @param values    : ContentValues instance

     * @param id        : record id

     * @return          : int

     */

    public int update(String table, ContentValues values, long id) {       

        return db.update(table, values, KEY_COLUMN + "=" + id, null);   

    }

   

    /***

     * Method to update record

     * @param table         : table name

     * @param values        : ContentValues instance

     * @param whereClause   : Where Clause

     * @return              ; int

     */

    public int update(String table, ContentValues values, String whereClause) {

        return db.update(table, values, whereClause, null);

    }

   

    /***

     * Method to delete record

     * @param table         : table name

     * @param whereClause   : Where Clause

     * @return              : int

     */

    public int delete(String table, String whereClause) {

        return db.delete(table, whereClause, null);

    }

   

    /***

     * Method to delete record

     * @param table         : table name

     * @param id            : record id

     * @return              : int

     */

    public int delete(String table, long id) {

        return db.delete(table, KEY_COLUMN + "=" + id, null);

    }

   

    /***

     * Method to run sql

     * @param sql

     */

    public void exec(String sql) {

        db.execSQL(sql);

    }

   

    /****

     * logCursorInfo    : Cursor 리턴받는 Result 로깅하는 메소드

     * @param c

     */

    public void logCursorInfo(Cursor c) {

        Log.i(Constants.LOG_TAG, "*** Cursor Begin *** " + "Results:" +

                c.getCount() + " Colmns: " + c.getColumnCount());

       

        // Column Name print

        String rowHeaders = "|| ";

        for(int i=0; i<c.getColumnCount(); i++) {

            rowHeaders = rowHeaders.concat(c.getColumnName(i) + " || ");

        }

       

        Log.i(Constants.LOG_TAG, "COLUMNS " + rowHeaders);

        // Record Print

        c.moveToFirst();

        while(c.isAfterLast() == false) {

            String rowResults = "|| ";

            for(int i=0; i < c.getColumnCount(); i++) {

                rowResults = rowResults.concat(c.getString(i) + " || ");

            }

           

            Log.i(Constants.LOG_TAG, "Row " + c.getPosition() + ": " + rowResults);

           

            c.moveToNext();

        }

        Log.i(Constants.LOG_TAG, "*** Cursor End ***");

    }

   

    @Override

    /***

     * Method to create database

     * 데이터베이스 생성. 최초 한번만 실행됨.

     * @param db        :SQLiteDatabase instance

     */

    public void onCreate(SQLiteDatabase db) {

        DatabaseCreator mCreator = new H2eDatabaseCreator();

        String[] tableCreateStmt = mCreator.getCreateTablesStmt();

        String[] indexCreateStmt = mCreator.getCreateIndexStmt();

        String[] initDataDml = mCreator.getInitDataInsertStmt();

       

        try {

            if(tableCreateStmt != null && tableCreateStmt.length > 0) {

                Log.v(Constants.LOG_TAG, DatabaseHelper.CLASSNAME + " - onCreate() : Table Creation");

                for(int i = 0; i < tableCreateStmt.length; i++) {

                    db.execSQL(tableCreateStmt[i]);

                }

            }

           

            if(indexCreateStmt != null && indexCreateStmt.length > 0) {

                Log.v(Constants.LOG_TAG, DatabaseHelper.CLASSNAME + " - onCreate() : Index Creation");

                for(int i = 0; i < indexCreateStmt.length; i++) {

                    db.execSQL(indexCreateStmt[i]);

                }

            }              

           

            if(initDataDml != null && initDataDml.length > 0) {

                for(int i = 0; i < initDataDml.length; i++) {

                    Log.v(Constants.LOG_TAG, DatabaseHelper.CLASSNAME + " - onCreate() : Data Load" + initDataDml[i]);

                    db.execSQL(initDataDml[i]);

                }

                Log.v(Constants.LOG_TAG, DatabaseHelper.CLASSNAME + " - onCreate() : Init Data Load");

            }

           

        } catch(SQLException e) {

            Log.e(Constants.LOG_TAG, DatabaseHelper.CLASSNAME + " - onCreate() : Table Creation Error", e);

        }

 

    }

 

    @Override

    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {

        Log.v(Constants.LOG_TAG, DatabaseHelper.CLASSNAME + " - onUpgrade() : Table Upgrade Action");

 

    }

 

}

 


소스는 보시면 내용은 많으나 찬찬히 보시면 크게 복잡하거나 어려운 부분은 없는 것 같습니다.
SQLiteOpenHelper에서 상속을 받아 구현한 DatabaseHelper 클래스는 onCreate(), onUpgrade() 같은 SQLiteOpenHelper 콜백 메소드를 구현하고 있습니다. 데이터베이스가 Open 될 때 데이터베이스가 존재하지 않으면 onCreate() 메소드 부분이 실행됩니다. onCreate()내에서는 H2eDatabaseCreator Class에서 정의한 DDDL문을 가져와 실행합니다.
onUpgrade() 메소드는 현재 설치된 DB버전과 코드의 버전을 비교해서 서로 다르면 실행되는 메소드입니다. 저희는 초기 버전이라 별 다른 액션없이 로그만 기록했습니다.

DatabaseHelper의 private 생성자 및 getInstance() 메소드는 싱글톤 패턴의 전형적인 모습입니다.
멀티스레드 환경에서는 싱글톤을 만들 때 synchronized 키워드로 동기화를 하긴 하지만, 스마트폰 앱 환경에서는 그럴 필요가 없어서 부하를 주는 synchronized 키워드 없이 심플하게 구성하였습니다.

싱글톤 관련 메서드 하위의 메서드들은 SQLiteDatabase의 인스턴스가 직접 DB에 레코드 추가,삭제,수정,조회 등의 메서드를 코드 외부에서 SQLiteDatabase의 인스턴스를 직접 핸들링하지 않도록 캡슐화(일종의 wrapper method) 하고 있습니다.
.
logCursorInfo() 메소드는 Cursor로 리턴받은 Result를 log에 기록하는 유틸성 메소드 입니다.
우리코드에서 별 사용할 일은 없겠지만, Contents Provider에서 제공하는 기능을 이용하여 개발할 때, 디버깅시에 편리하게 사용할 수 있습니다.

이제 우리 App의 데이터를 핸들링하는 Dao 클래스를 보겠습니다.

//DataDao.java

package com.overoid.hangul2english.data;

 

import java.util.ArrayList;

import java.util.Iterator;

import java.util.List;

 

 

import com.overoid.hangul2english.Constants;

import com.overoid.hangul2english.data.H2eDatabase.DataTable;

 

import android.content.ContentValues;

import android.content.Context;

import android.database.Cursor;

import android.database.SQLException;

import android.util.Log;

 

 

 

public class DataDao {

    private static final String CLASSNAME = DataDao.class.getSimpleName();

    private DatabaseHelper db;

   

    public DataDao(Context context) {

        db = DatabaseHelper.getInstance(context);

       

    }

   

    public void close() {

        db.close();

    }

    /***

     * Inner Class - TO Objects

     * @author jinook.lee

     *

     */

    public static class DataTo {

        private int id;

        private String korText;

        private String engText;

       

        public DataTo() {}

 

        public DataTo(int id, String korText, String engText) {

            this.id = id;

            this.korText = korText;

            this.engText = engText;

        }

 

        @Override

        public String toString() {

            return "DataTo [id=" + String.valueOf(id) + ", korText=" + korText + ", engText=" + engText + "]";

        }

 

        public int getId() {

            return id;

        }

 

        public void setId(int id) {

            this.id = id;

        }

 

        public String getKorText() {

            return korText;

        }

 

        public void setKorText(String korText) {

            this.korText = korText;

        }

 

        public String getEngText() {

            return engText;

        }

 

        public void setEngText(String engText) {

            this.engText = engText;

        }

    }

 

    /****

     * insert       : table 추가하기

     * @param to    : DataTo object

     */

    public void insert(final DataTo to) {

        ContentValues values = new ContentValues();

 

        values.put(DataTable.COLUMN_KOR_TEXT, to.getKorText());

        values.put(DataTable.COLUMN_ENG_TEXT, to.getEngText());

 

        Log.v(Constants.LOG_TAG, DataDao.CLASSNAME + " insert - korText:" + to.getKorText());

        long rowId = db.insert(DataTable.TABLE_NAME, values);

        if(rowId < 0) {

            throw new SQLException("Fail At Insert");

        }

    }

  

    /****

     * update       : UI 사용될 일은 없을듯.

     * @param to    : DataTo object

     */

    public void update(final DataTo to) {

       ContentValues values = new ContentValues();

 

       values.put(DataTable.COLUMN_KOR_TEXT, to.getKorText());

       values.put(DataTable.COLUMN_ENG_TEXT, to.getEngText());

      

       Log.v(Constants.LOG_TAG, DataDao.CLASSNAME + " update - _id:" + String.valueOf(to.getId()));

       db.update(DataTable.TABLE_NAME, values, to.getId());

      

   }

   

    /****

     * delete       : record delete

     * @param id    : record id

     */

    public void delete(final int id) {

        Log.v(Constants.LOG_TAG, DataDao.CLASSNAME + " delete - _id:" + String.valueOf(id));

        db.delete(DataTable.TABLE_NAME, id);

    }

   

    /****

     * get          : select * from table

     * Cursor ArrayList 담아서 리턴한다.

     *

     * @return      : ArrayList DataTo

     */

    public List<DataTo> get() {

        Cursor c = null;

        ArrayList<DataTo> ret = null;

        

        String sql = "SELECT * FROM " + DataTable.TABLE_NAME + " ORDER BY 1";

       

        try {

            Log.v(Constants.LOG_TAG, DataDao.CLASSNAME + " get - All");

            c = db.get(sql);

           

            //db.logCursorInfo(c);

           

            ret = setBindCursor(c);

        } catch (SQLException e) {

            Log.e(Constants.LOG_TAG, DataDao.CLASSNAME + " getList ", e);

        } finally {

            if (c != null && !c.isClosed()) {

                c.close();

            }

        }

       

        return ret;

    }

   

   

    /****

     * SQLite Result Cursor 데이터를 Array List 넣고 리턴하는 메서드.

     * @param c     : cursor

     * @return      : ArrayList<DataTo>

     */

    private ArrayList<DataTo> setBindCursor(final Cursor c) {

        ArrayList<DataTo> ret = new ArrayList<DataTo>();

       

        int numRows = c.getCount();

       

        c.moveToFirst();

       

        // SQL문에서 Join 사용시 테이블명. 사용하면 컬럼명이 틀려지므로 getColumnIndex

        // Exception 낸다. 반드시 alias 사용해 컬럼명을 동일하게 맞춰야 한다.

        // 값이 null 경우 getInt() 0 반환할까? - 반환함.

       

        for(int i=0; i < numRows; i++) {

            DataTo to = new DataTo();

            to.setId(c.getInt(c.getColumnIndex(DataTable.COLUMN_ID)));

            to.setKorText(c.getString(c.getColumnIndex(DataTable.COLUMN_KOR_TEXT)));

            to.setEngText(c.getString(c.getColumnIndex(DataTable.COLUMN_ENG_TEXT)));

 

            ret.add(to);

            c.moveToNext();

        }

       

        return ret;

    }

   

    /****

     * List<DataTo> 내용을 로깅하는 메소드

     * @param to

     */

    public void logListInfo(List<DataTo> to) {

        Log.i(Constants.LOG_TAG,"*** List Begin *** " + "Results:" + to.size());

       

        Iterator<DataTo> itr = to.iterator();

        while (itr.hasNext()) {

            String msg = ((DataTo)itr.next()).toString();

            Log.i(Constants.LOG_TAG, "DATAS: " + msg);

        }

        Log.i(Constants.LOG_TAG,"*** List End ***");

    }

}

 



DataDao는 Activity에서 이용하는 클래스입니다.
테이블마다 혹은 비즈니스 단위마다 하나씩 Dao클래스를 만들어 사용하면 됩니다. 클래스명은 <테이블식별자> + “Dao”로 이루어져 있습니다.
역시 소스는 크게 어려운 코드는 없습니다.

DataDao 클래스는 내부에 중첩클래스로 Transfer Object로 사용할 클래스(Bean)를 담고 있습니다. TO 클래스는 Dao 클래스의 메소드 인자로 사용될 클래스입니다. <테이블 식별자> + “To”로 클래스명이 만들어 졌으며, 테이블의 컬럼에 해당되는 필드와 getter/setter로 이루어져 있습니다.

나머지 DataDao 클래스 소스 부분은 생성자에서 DatabaseHelper 클래스의 인스턴스를 얻어서 멤버변수에 저장하는 로직과, 실제 데이터 핸들링을 위한 get/insert/update/delete 메소드들이 존재합니다. 저희 App에서는 Cursor를 직접 핸들링 하지 않고 자바 개발자들이 많이 사용하는 ArrayList에 결과를 담아서 처리하도록 하겠습니다. 이렇게 작성하면 UI용 Adapter 작성시에도 ArrayAdapter를 사용할 수 있습니다. 실은 제가 익숙해서 사용하는 것입니다. 직접 작성하실 때는 Cursor를 직접 리턴하도록 코드를 작성하셔도 무방합니다.

setBindCursor() 메소드에서는 커서를 ArrayList로 변환하는 기능을 담당하고 있습니다.

logListInfo() 메소드는 ArrayList에 담긴 데이터를 log에 기록해주는 util method 입니다. 디버깅할 때 편리하게 사용할 수 있습니다.

Insert/update 시에는 ContentValues 객체를 사용했습니다. ContentValues 객체는 처리할 컬럼의 이름과 값의 쌍을 담는 객체입니다. 필요한 자료를 ContentValues 객체에 설정한 후 insert/update DatabaseHelper 인스턴스의 insert/update 메소드의 인자로 넘겨주면 됩니다.

위의 DataDao 및 DatabaseHelper에서는 단순한 쿼리에 대해서만 처리가 가능하도록 클래스가 구성되었습니다. 복잡한 쿼리는 SQLiteQueryBuilder Class를 이용해서 구성은 가능합니다만, 저는 그냥 Raw SQL문을 직접 실행하는 것이 쿼리 작성도 편리하고 좋은 것 같아서 SQL문을 실행할 수 있는 메소드만(get(sql), exec(sql) DatabaseHelper에 메소드로 제공하고 있습니다.

이로서 강좌에서 개발하는 Hangul2English App의 DB 부분 소스를 모두 살펴보았습니다.

이 DB 관련 클래스 사용법에 대해서 정리하자면,

H2eDatabase.java 
개발하려는 App의 테이블 구조를 static inner class로 정의(변경) 하시고 클래스명 및 파일명을 변경하시면 됩니다.

DatabaseCreator.java
인터페이스이므로 그대로 소스 복사하시면 됩니다. 패키지 구조만 수정하시면 될 것 같네요.

H2eDatabaseCreator.java
개발하려는 App의 DDL문을 상수로 정의하시고, 관련 메소드내에 포함시키시면 됩니다.

DatabaseHelper.java 
수정할 내용은 거의 없으며, onCreate() 메소드 내에서 인스턴스를 생성하는 클래스명 부분(DatabaseCreator mCreator = new H2eDatabaseCreator();) 만 자신의 DatabaseCreator클래스명으로 수정하시면 됩니다.

DataDao.java 
이 부분은 실제 안쪽 코드를 자신의 앱에 맞게 모두 수정하셔야 합니다.

처음에 제가 계획했던, Copy & Paste로 최소 노력으로 다른 App 개발에 적용하려는 목적은 대략 달성한 것 같습니다. 물론 별도의 library로 만들 수도 있지만, 그러려면 좀 더 다양한 케이스를 지원하도록 많은 코드도 추가되어야 괜챦은 library가 나올 것 같아서.. 그 부분은 차차 해보도록 하겠습니다.

이제 이 DB코드를 이용하는 UI 코드 부분을 살펴 보도록 하겠습니다.

UI코드를 살펴보기 전에 MotoDev의 Database 툴 기능을 편하게 리뷰하고 가도록 하겠습니다.
이번에 포스트 쓰면서 사용해본 기능인데, 소스 코드 Generation 기능이 상당히 맘에 들어 소개합니다.
그럼, Part 3에서 뵙겠습니다.


  1. 이유식 2010.09.05 14:52 신고

    많은걸 배우고 갑니다.

  2. 선지헌 2010.09.29 20:12 신고

    많이 배우고 있습니다. 좋은 강좌 감사합니다. 질문이 있어서 글 남기는데요 DatabaseHelper 클래스를 싱글턴으로 만드셨는데요 여러 Activity에서 DB에 접근하는 문제때문이라고 하셨는데 조금 더 자세한 설명을 부탁드리고 싶습니다. 현재 제가 ContentProvider를 이용해 DB접근을 하는 앱을 만들고 있는데 이럴경우에도 싱글턴을 써야 하는 것인지요?

    • 보고픈 2010.09.30 15:48 신고

      제가 직접 해보지는 않아서 정확히는 모르겠으나, ContentProvider로 구현할때는 크게 문제가 되지는 않을듯 싶긴 합니다. 기존 구글쪽 소스를 보더라도 큰 문제는 안되는것 같습니다.

      여러 Activity에서 db 사용할때는 다른 화면에서 db 사용할때 이전 화면에서 db를 close 하지 않으면 에러가 발생합니다.
      싱글톤을 사용하지 않더라도 db를 close() 후 새 activity에서 open()하면 문제가 되지 않습니다. 저는 왠지 그 작업이 부하가 걸릴듯 싶어 싱글톤으로 구현한 것입니다.

  3. 리칼 2010.12.10 17:48 신고

    정말 볼수록 감탄합니다.. 감사합니다..

  4. jordan retro 12 2012.03.05 17:58 신고

    총괄 은 미국 의 인기 는 패션 여성복 브랜드 나인 의 창시자 토 리 버 치 (총괄) 여사.

  5. 맹순이 2014.10.28 00:34 신고

    좋은 코드 리뷰 잘 했습니다. 한 가지 궁금한 점이 있습니다. close 부분에서 helper 인스턴스 까지 close 해버리면 문제가 생기지 않을까 해서요. 만일 멀티 스레드 환경에서 A 액티비티에서 백그라운드 작업을 하는중에 B 액티비티로 넘어간 후 A가 끝나지 않은 상황에서 B에서 close를 해버리는 상황같이요. 한참 전 글인데 보실려나 모르겠네요 ^^

+ Recent posts