Showing posts with label Android Sqlite. Show all posts
Showing posts with label Android Sqlite. Show all posts

Saturday, October 10, 2015

[Android Databass] SQLiteOpenHelper, ContentProvider, OrmLite, AndroidAnnotations @OrmLite - 2

Android에서 Sqlite를 사용하는 것에 있어서 두번째 포스팅입니다. 기본적인 OrmLite Library사용과 AndoridAnnotations를 적용하여 OrmLitef를 사용하는 방법에 대하여 포스팅하도록 하겠습니다.

3. OrmLite

ORM 이라는 의미는 Object Realational Mapping의 약자입니다. OrmLite는 많은 표준 Library의 복잡성과 오버헤드를 피하면서 SqlDatabase를 접근 할 수 있게 하는 경량 Library입니다. 이것은 다양한 JDBC를 지원할뿐만아니라, Sqlite와 함께사용하는 Android OS Database API의 native call 또한 지원한다.(Homepage참고) Orm 에 Java 표준 기술은 Java Persistence API로 JPA가 요즘 많이 사용되고 있다고 한다. Spring과 같은 웹 프로젝트에서 이런 Java Library를 사용하는 것과 같이 Android 계열에서 Sqlite 사용시 Orm 을 사용할 경우 더 편하게 사용할 수있으니 해당 Library는 앞으로 봤을때 사용성이 높은 Library라고 생각합니다.

Github Repository 에 가면 아래의 작성한 소스코드 전체를 확인할 수 있습니다. OrmLite에 대한 상세한 설명은 더 파악한 후에 포스팅하도록 하겠습니다. 이 포스팅에서는 사용법 위주로 설명하도록 하겠습니다.

가장 먼저 gradle에 ormlite에 대한 dependency를 추가해주어야 합니다.

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:23.0.1'
    ...

    compile 'com.j256.ormlite:ormlite-android:4.46'
    ...
}

저는 OrmLiteDBHelper라고 class 이름을 설정하고 해당 class는 OrmLiteSqliteOpenHelper를 상속받아야 합니다. 또한 상속받게 되면 아래와 같이 생성자와 onCreate, onUpgrade에 대해서 implement를 반드시 해줘야 한다. 그리고 SQLiteOpenHelper 에서 해줬던것 처럼 데이터베이스 이름과 버전을 설정해줘야한다. 저는 이 부분에 대해서 Constant Class를 따로 두고 받아오도록 구현하였습니다.

package com.juranoaa.sqlite.common;

public class Constant {
    public static final class SQLite {
        public static final Integer ORM_DB_VERSION = 1;
        public static final String ORM_DB_NAME = "ORMTest.db";
    }
}
package com.juranoaa.sqlite.ormlite;

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;

import com.j256.ormlite.android.apptools.OrmLiteSqliteOpenHelper;
import com.j256.ormlite.support.ConnectionSource;
import com.juranoaa.sqlite.common.Constant;

public class OrmLiteDBHelper extends OrmLiteSqliteOpenHelper{

    public OrmLiteDBHelper(Context context) {
        super(context, Constant.SQLite.ORM_DB_NAME, null, Constant.SQLite.ORM_DB_VERSION);
    }

    @Override
    public void onCreate(SQLiteDatabase database, ConnectionSource connectionSource) {

    }

    @Override
    public void onUpgrade(SQLiteDatabase database, ConnectionSource connectionSource, int oldVersion, int newVersion) {

    }
}

그리고 onCreate와 onUpgrade method에 아래와 같이 구현해야 합니다. onCreate에는 Database생성, onUpgrade에는 Database 삭제후 생성과 같은 함수 내용을 작성해야 합니다. 이 부분을 아래와 같이 작성해줍니다. 이때 사용되는 것은 " TableUtils " 에 정의 되어있고 데이터베이스를 생성해주는 쿼리문을 작성해줍니다.

package com.juranoaa.sqlite.ormlite;

public class OrmLiteDBHelper extends OrmLiteSqliteOpenHelper{

//...

@Override
public void onCreate(SQLiteDatabase database, ConnectionSource connectionSource) {
    try {
        TableUtils.createTable(connectionSource, House.class);
    } catch (SQLException e) {
        Log.e(TAG, e.getMessage(), e);
        throw new RuntimeException(e);
    }
}
// ...

}

여기서 House.class 와 같은 경우는 아래와 같이 정의가 필요합니다. 이렇게 작성해주게 되면 자동으로 CREATE 문을 생성하여 Sqlite Database에 입력해줍니다. 자세한 내용은 Database를 참고 하시기 바랍니다.

package com.juranoaa.sqlite.ormlite;

public class House {

    // auto increment
    @DatabaseField(generatedId = true)
    Integer id;

    @DatabaseField
    String name;

    public House() {
        // must exist
    }
}

그리고 두가지 method를 정의하여 사용할 수 있습니다. getDao(class)의 경우에는 SQLExcpetion이 throw 되는 method이고 getRuntimeExceptionDao(class)은 Exception 에 대해서 RuntimeException으로 처리하여 사용하는 method가 제공됩니다. 이에 대한 구현은 아래와 같이 표현됩니다.

package com.juranoaa.sqlite.ormlite;
// ...

public class OrmLiteDBHelper extends OrmLiteSqliteOpenHelper{
    // ...    
    private RuntimeExceptionDao<House, Integer> houseRuntimeExceptionDao = null;
    // ...
    
    public RuntimeExceptionDao<House, Integer> getHouseDataDao() {
        if (houseRuntimeExceptionDao == null) {
            houseRuntimeExceptionDao = getRuntimeExceptionDao(House.class);
        }
        return houseRuntimeExceptionDao;
    }
    // ...
}

이렇게 구성하면 OrmLite를 사용할 수 있게됩니다. 또한 해당 Library는 apt를 통해 compile 시점에 작동하는 library가 아닌 runtime시에 작동하는 library입니다. 그리고 Activity에서 아래 코드와 같이 사용하면 사용할 수 있습니다.


@EActivity(R.layout.main)
public class AASQLiteActivity extends OrmLiteBaseActivity<OrmLiteDBHelper> {

    @ViewById
    TextView textView;

    @AfterViews
    void afterViews(){
        RuntimeExceptionDao<House, Integer> houseDao = getHelper().getHouseDataDao();
        List<House> houseList = houseDao.queryForAll();
        textView.setText(houseList.toString());

        for(House house : houseList){
            House updateHouse = new House(house.getId(), house.getName()+"_update");
            houseDao.update(updateHouse);
        }
        List<House> updateHouseList = houseDao.queryForAll();
        textView.setText(textView.getText()+updateHouseList);
    }
}

Activity는 OrmLiteBaseActivity를 상속받고 Generic 부분은 앞에서 작성한 클래스 이름이 작성되게 됩니다. (OrmLiteDBHelper) 그리고 getHeleper()를 통하여 OrmLiteDBHelper에서 작성한 getHouseDataDao method를 호출하여 사용하면 된다.

해당 Dao에 대해서 create, update, delete, queryForAll 과 같은 method을 이용하여 SQLite 데이터 베이스에 CRUD 작업을 할 수 있습니다. 이렇게 사용할 경우 상속받을 Activity에 type과 method를 작성을 해야하는데 이런 불편함을 조금 더 개선한게 AndroidAnnotations에 @OrmLite입니다.

4. AndroidAnnotations @OrmLite @EProvider

가장먼저 AndroidAnnotations의 @OrmLite를 사용하기 위해서 Gradle에 추가정보를 입력해야 합니다. 해당 부분은 4.0-SNAPSHOT 기준입니다. 현재 Release 버전인 3.3.2를 사용한다면 기존 apt만 사용해도 사용할 수 있습니다.

def AAVersion = "3.3.2"
dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:23.0.1'
    apt "org.androidannotations:androidannotations:$AAVersion"
    compile "org.androidannotations:androidannotations-api:$AAVersion"
    compile 'com.j256.ormlite:ormlite-android:4.46'
}
def AAVersion = "4.0-SNAPSHOT"
dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:23.0.1'
    apt "org.androidannotations:androidannotations:$AAVersion"
    compile "org.androidannotations:androidannotations-api:$AAVersion"

    compile 'com.j256.ormlite:ormlite-android:4.46'
    apt "org.androidannotations:ormlite:$AAVersion"
    compile "org.androidannotations:ormlite-api:$AAVersion"
}

@OrmLite 사용에 있어서 기존에 앞에 사용하던 Class는 그대로 유지를 하여 사용하면 됩니다. 그리고 객체를 반환했을때 사용할 수 있도록 하는 객체하나가 필요합니다.

package com.juranoaa.sqlite.ormlite;

import com.j256.ormlite.dao.Dao;
import com.j256.ormlite.dao.RuntimeExceptionDao;

public class HouseRuntimeExceptionDao extends RuntimeExceptionDao<House,Integer>{

    public HouseRuntimeExceptionDao(Dao<House, Integer> dao) {
        super(dao);
    }
}

위의 객체를 정의하고 Activity에서 아래와 같이 정의하고 사용하면 @OrmLite Annotation을 사용할 수 있습니다.

@EActivity(R.layout.main)
public class AASQLiteActivity extends Activity{
    ...
    @OrmLiteDao(helper = OrmLiteDBHelper.class)
    HouseRuntimeExceptionDao houseDao;
    ...
}

위의 코드는 Github Repository에서 전체 코드를 확인할 수 있습니다.

이상으로 Android 에서 Database를 접근하는 방법에 대해서 두개의 포스팅에서 검토해보았습니다. ContentProvider의 경우 Android Docs를 보게되면 Application간에 데이터를 공유해야 되는 경우에만 필요하다고 작성되어있습니다. 따라서 CRUD에 대한 쿼리 작성이 불편한 점으로 인해 하나의 Application에서만 사용하는 DB를 ContentProvider로 쓰는것은 올바르지 않다고 생각합니다. 그래서 제 생각에는 OrmLite를 이용하면 조금 더 이런 코드에 대한 리팩토링을 할 수있다고 생각합니다. 이상 끝.

Friday, October 9, 2015

[Android Databass] SQLiteOpenHelper, ContentProvider, OrmLite, AndroidAnnotations @OrmLite - 1

이 블로그는 4개의 Documentation 을 참고하여 작성되었습니다. [ SQLiteOpenHelper Docs, ContentProvider Docs, OrmLite Docs, AndroidAnnotations @EProvider ]

팀원 중 한명이 AndroidAnnotations 를 기반으로한 주소록 어플을 빠르게 만들고 있는데, 그러던 중 SQLiteOpenHelper 클래스를 사용하여 쓰는 것을 볼 수 있었다. 초기 개발시에는 해당 클래스를 기반으로 개발하고 ContentProvider를 쓰고 @EProvider를 써본다고 해서.. 갑자기 3가지의 차이점이 궁금하게 되었다. 그래서 Android Docs에 가서 해당 내용에 대해서 분석해보았다.

1. SQLiteOpenHelper

먼저 SQLiteOpenHelper를 설명하자면 클래스 이름 그대로 Android 에서 내장 되어 사용되는 SQLite를 사용하는데 도와주는 Helper역할을 하는 클래스이다. Database Creation, Version Management, 즉, 데이터베이스 생성과 버전관리를 담당하는 부분이다. 해당 Class를 상속받는 클래스를 생성하면 아래의 코드와 같이 onCreate(SQLiteDatbase)onUpgrade(SQLiteDabase, int int) 2개의 method 구현이 반드시 이뤄져야 하며 생성자 또한 존재해야한다.

package com.juranoaa.sqlite;

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;

public class BaseSQLiteOpenHelper extends SQLiteOpenHelper{

    public BaseSQLiteOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) {
        super(context, name, factory, version);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {

    }

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

    }
}

1.1. Constructor(Context, String, SQLiteDabase.CursorFactory, int) 생성자

해당 생성자의 경우에 중요한 부분이 있습니다. 역할은 당연히 database에 대한 create, open, manage하기 위한 부부이지만 해당 되는 부분은 getWritableDatabase(), getReadableDatabase() method를 부르기 전까지 데이터베이스 자체는 생성되지 않는 다는 점입니다. 앞으로 나올 부분에서도 해당 부분에 대한 언급을 계속하게 되기때문에 "데이터베이스 자체가 생성되지 않는다"라는 것은 반드시 숙지하고 넘어가시기 바랍니다.

1.2. onCreate(SQLiteDatabase), onUpgrede(SQLiteDatabae)

method 명과 같이 데이터 베이스를 생성하거나, 데이터베이스를 업그레이드 하는 역할을 하는 method입니다. 만약 Sqlite로 id, title, content를 가진 게사판을 구현한다고 하면 다음과 같이 작성됩니다.

public class Constant {

    public static final class SQLite {
        public static final Integer DB_VERSION = 1;
        public static final String DB_NAME = "AATest.db";

        public static final String TABLE_NAME = "board";
        public static final String DB_CREATE =  "CREATE TABLE " + TABLE_NAME + " ("+
                                                " id INTEGER PRIMARY KEY AUTOINCREMENT "+
                                                " title VARCHAR NOT NULL"+
                                                " content VARCHAR NOT NULL"+
                                                ")";
        public static final String DB_DROP =    "DROP TABLE IF EXIST " + TABLE_NAME ;
    }
}
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;

public class BaseSQLiteOpenHelper extends SQLiteOpenHelper{

    public BaseSQLiteOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) {
        super(context, name, factory, version);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(Constant.SQLite.DB_CREATE);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        db.execSQL(Constant.SQLite.DB_DROP);
        onCreate(db);
    }
}

1.3. getReadableDatabase(), getWritableDatabase()

두 개의 method는 특징이 있습니다. SQLiteOpenHelper의 경우 onCreate, onOpen, onUpgrade method 들이 생성자 시점에 불리는 것이 아니라 해당 method들이 호출될때 불리게 됩니다. 이렇게 구현이 되는 이유는 생성 또는 업데이트 과정에서 시간이 올래걸리게 된다면 Application이 호출하는데 늦게 활성화가 되기 때문입니다. 따라서 해당 method가 Application main thread에서 불리게 하는것은 되도록 지양해야합니다.

2. ContentProvider

ContentProvider는 AndroidApplication에서 application에서 제공하는 가장 중요한 부분 중 하나 입니다.Data는 Encapsulation(은닉화)되어있으며, ContentResolver를 이용하여 인터페이스간 통신을 제공해줍니다. 따라서 ContentProvider는 Application간에 데이터를 주고 받고자할 때 필요한 부분입니다. 만약에 Application간에 Data share가 발생하지 않는다면 굳이 contentProvider를 사용하는 것이아닌 SQLiteDatabase를 직접적으로 사용하면 됩니다.

(이 부분이 Docs에 쓰여져있긴 하지만 굳이 Database에 Directly 접근할 필요가 있을까? 라는 의문이 드네요).

그리고 아래와 같이 ContentProvider를 사용하게 되면 6개의 method가 반드시 작성되어 있어야만합니다.

public class BaseContentProvider extends ContentProvider{
    @Override
    public boolean onCreate() {
        return false;
    }

    @Nullable
    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
        return null;
    }

    @Nullable
    @Override
    public String getType(Uri uri) {
        return null;
    }

    @Nullable
    @Override
    public Uri insert(Uri uri, ContentValues values) {
        return null;
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        return 0;
    }

    @Override
    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
        return 0;
    }
}

2.1. Constructor() 생성자

저는 이 생성자가 가장 중요한 부분이라고 생각합니다. ContentProvider는 Application 실행시점에 application main thread를 사용하여 실행합니다. 따라서 아까 위에서 말했듯이 SQLiteOpenHelper를 통해 구현한 클래스에 대한 실행이 생성자 부분에서 실행되도록 하면 안됩니다. 해당 부분이 실행되게 되면 application이 늦게 시작이 될 수 있기 때문에 이부분이 가장 중요한 부분이라고 생각합니다.

2.2. onCreate()

Docs에 존재하는 이 method에 대한 설명 또한 시간이 오래걸리는 작업, Application 시작시 느려지는 것은 수행하지 않도록 해야 한다고 권장하고 있습니다. 따라서 SQLiteOpenHelper를 이용해서 Database의 생성부분을 관리하도록 짜는 것이 중요합니다. 또한 onCreate() method 내부에서 getReadableDatabase()getWriteabaleDatabase() method를 부른다면 Database 생성을 main thread에서 수행하는것과 마찬가지가 되기 때문에 이부분을 가장 중요시하게 다뤄야 합니다.

ContentProvider를 작성하면 다음과 같다.

Constant.class

public class Constant {

    public static final class SQLite {
        public static final Integer DB_VERSION = 1;
        public static final String DB_NAME = "AATest.db";
    }
}

Board.class

public class Board {

    private Integer id;

    private String title;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public Board() {}

    public Board(Integer id, String title) {
        this.id = id;
        this.title = title;
    }

    @Override
    public String toString() {
        return "Board{" +
                "id=" + id +
                ", title='" + title + '\'' +
                '}';
    }
}

BoardProvider.class

public class BoardProvider extends ContentProvider {
    private static final String TAG = BoardProvider.class.getSimpleName();

    public static final String TABLE_NAME_BOARD = "dbBoard";
    private static final String AUTHORITY = "com.juranoaa.sqlite";

    public static final Uri CONTENT_URI_BOARD = Uri.parse("content://" + AUTHORITY + "/" + TABLE_NAME_BOARD);

    public static final String COL_ID = "_id";
    public static final String COL_TITLE = "title";

    /** create db string */
    public static final String DATABASE_CREATE_BOARD =
            "CREATE TABLE " + TABLE_NAME_BOARD +
            " (" + COL_ID + " INTEGER PRIMARY KEY,"
            + COL_TITLE + " TEXT );";

    private static BoardDataBaseHelper mDbHelperBoard;
    private SQLiteDatabase mDbBoard;

    @Override
    public boolean onCreate() {
        mDbHelperBoard = new BoardDataBaseHelper(getContext());
        return true;
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection,
                        String[] selectionArgs, String sortOrder) {
        mDbBoard = mDbHelperBoard.getReadableDatabase();
        Cursor cursor = mDbBoard.query(TABLE_NAME_BOARD, projection, selection,
                selectionArgs, null, null, sortOrder);
        return cursor;
    }

    @Override
    public String getType(Uri uri) {
        return null;
    }

    @Override
    public Uri insert(Uri uri, ContentValues values) {
        mDbBoard = mDbHelperBoard.getWritableDatabase();
        mDbBoard.insertOrThrow(TABLE_NAME_BOARD, null, values);
        return null;
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        mDbBoard = mDbHelperBoard.getWritableDatabase();
        int count = mDbBoard.delete(TABLE_NAME_BOARD, selection, selectionArgs);
        return count;
    }

    @Override
    public int update(Uri uri, ContentValues values, String selection,
                      String[] selectionArgs) {
        mDbBoard = mDbHelperBoard.getWritableDatabase();
        int count = mDbBoard.update(TABLE_NAME_BOARD, values, selection, selectionArgs);
        return count;
    }

    private static class BoardDataBaseHelper extends SQLiteOpenHelper {

        private static final String TAG = BoardDataBaseHelper.class.getSimpleName();

        public BoardDataBaseHelper(Context context){
            super(context, Constant.SQLite.DB_NAME, null, Constant.SQLite.DB_VERSION);
        }

        @Override
        public void onCreate(SQLiteDatabase db) {
            db.execSQL(DATABASE_CREATE_BOARD);

        }

        @Override
        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
            Log.w(TAG, "invoked! " + oldVersion + " -> " + newVersion + ", it'll destroy old data");
            db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME_BOARD);
            onCreate(db);

        }
    }
}

BoardDao.class

public class BoardDao {

    public static final String COLS_BOARD_ARR[] = {
            BoardProvider.COL_ID,
            BoardProvider.COL_TITLE
    };

    private BoardDao() {} 

    /**
     * Insert new Board to DB.
     */
    public static void insertNewBoard(Context ctx, Board board) {
        ContentValues Values = new ContentValues();
        ContentResolver cr = ctx.getContentResolver();

        Values.put(BoardProvider.COL_TITLE, board.getTitle());

        cr.insert(BoardProvider.CONTENT_URI_BOARD, Values);
    }

    public static List getAllBoards(Context ctx) {
        List boards = new ArrayList();

        ContentResolver cr = ctx.getContentResolver();
        Cursor c = cr.query(BoardProvider.CONTENT_URI_BOARD,
                COLS_BOARD_ARR, null, null, null);

        if (c != null && c.getCount() > 0) {
            c.moveToFirst();
            while (c.isAfterLast() == false) {
                boards.add(new Board(
                        c.getInt(c.getColumnIndex(BoardProvider.COL_ID)),
                        c.getString(c.getColumnIndex(BoardProvider.COL_TITLE))));
                //LogUtil.w("map.put(" + uid + "," + persona_name+ ")");
                c.moveToNext();
            }
        }
        c.close();

        return boards;
    }

    public static Board getBoardById(Context ctx, int id) {
        Board board = null;

        ContentResolver cr = ctx.getContentResolver();
        Cursor c = cr.query(BoardProvider.CONTENT_URI_BOARD,
                COLS_BOARD_ARR, BoardProvider.COL_ID + "=?",
                new String[]{String.valueOf(id)}, null);
        if (c != null && c.getCount() > 0) {
            c.moveToFirst();
            while (c.isAfterLast() == false) {
                board = new Board(
                        c.getInt(c.getColumnIndex(BoardProvider.COL_ID)),
                        c.getString(c.getColumnIndex(BoardProvider.COL_TITLE)));
                c.moveToNext();
            }
        }
        c.close();

        return board;
    }
}

AndroidManifest.xml




    

        
        
    


Github 링크는 다음과 같습니다. https://github.com/JuranoSaurus/AndroidAnnotationsSampleProject/tree/sqlite.1.0

-------------------------------------------

다음 포스팅에서 OrmLite와 AndroidAnntation EProvide를 이용한 DB 구성에 대한 포스팅을 하도록 하겠습니다. 끝...

참고 논문 및 사이트

1. [Android/안드로이드] Content Provider ( 콘텐트 프로바이더 ) 에 대한 모든 것. http://aroundck.tistory.com/236