SQLite Database 및 Content Provider 만들기

11 분 소요

여기서 다룰 내용은 Sunshine의 7-1부터 7-3, 9-1 부터 9-3 까지다. 이 내용은 동영상 강의가 없다. (아마 유료강의에만 포함되어 있는듯..) 하지만 파일만 있기에 파일 TODO를 따라가며 작성해 본다. Content Provider자체를 만들기 위한 작업과정이다.

database 만들기

SQLite이용한 Database만드는 내용이다. 먼저 Table에 사용될 다양한 Constant값을 담고있는 Contract Class를 만들어야 한다. Sunshine에서는 com.example.android.sunshine.data package밑에(즉 app밑에 data폴더를 만들고) WeatherContract라는 class를 만들고 아래와 같이 BaseColumn을 implement하는 WeatherEntry class를 작성했다.

package com.example.android.sunshine.data;

import android.provider.BaseColumns;

public class WeatherContract {

    public static final class WeatherEntry implements BaseColumns {
    	//Table 이름 
        public static final String TABLE_NAME = "weather";
        //Comlumn이름들 
        public static final String COLUMN_DATE = "date";

        public static final String COLUMN_WEATHER_ID = "weather_id";

        public static final String COLUMN_MIN_TEMP = "min";
        public static final String COLUMN_MAX_TEMP = "max";

        public static final String COLUMN_HUMIDITY = "humidity";
        public static final String COLUMN_PRESSURE = "pressure";

        public static final String COLUMN_WIND_SPEED = "wind";
        public static final String COLUMN_DEGREES = "degrees";

    }
}

각 column에 들어갈 데이터에 대한 설명은 S7-1 solution파일을 살펴보자. 여기서는 기능적인 측면만..

다음으로는 실제 테이블을 만들어 보자. 쌩으로 만드는게 아니고 SQLiteOpenHelper class를 상속받아 이용한다.

public class WeatherDbHelper extends SQLiteOpenHelper {

    public static final String DATABASE_NAME = "weather.db";

    private static final int DATABASE_VERSION = 1;

    public WeatherDbHelper(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }

    @Override
    public void onCreate(SQLiteDatabase sqLiteDatabase) {
    	//테이블 생성 문구 
        final String SQL_CREATE_WEATHER_TABLE =

                "CREATE TABLE " + WeatherEntry.TABLE_NAME + " (" +
                //_ID는 BaseColumn 인스턴스것임 
                WeatherEntry._ID               + " INTEGER PRIMARY KEY AUTOINCREMENT, " +

                WeatherEntry.COLUMN_DATE       + " INTEGER, "                 +

                WeatherEntry.COLUMN_WEATHER_ID + " INTEGER, "                 +

                WeatherEntry.COLUMN_MIN_TEMP   + " REAL, "                    +
                WeatherEntry.COLUMN_MAX_TEMP   + " REAL, "                    +

                WeatherEntry.COLUMN_HUMIDITY   + " REAL, "                    +
                WeatherEntry.COLUMN_PRESSURE   + " REAL, "                    +

                WeatherEntry.COLUMN_WIND_SPEED + " REAL, "                    +
                WeatherEntry.COLUMN_DEGREES    + " REAL" + ");";

        //테이블 생성 수행 
        sqLiteDatabase.execSQL(SQL_CREATE_WEATHER_TABLE);
    }

    //선샤인에서는 온라인 데이터를 갖고올것이기 때문에 이 메쏘드는 비워두면 된다. 
    @Override
    public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion) {

    }
}

잘못된 값 입력되는 것 방지하기

위 column들에 빈 값이 들어가는 것을 막기 위해 CREATE문의 각 Column생성시 NOT NULL을 붙여주도록 하자.

 				...
 				WeatherEntry._ID               + " INTEGER PRIMARY KEY AUTOINCREMENT, " +

                WeatherEntry.COLUMN_DATE       + " INTEGER NOT NULL, "                 +

                WeatherEntry.COLUMN_WEATHER_ID + " INTEGER NOT NULL, "                 +
                ...

db schema가 바뀌었으므로 버전을 하나 업그레이드 해 주고

	...
    private static final int DATABASE_VERSION = 2;
    ...

onUpgrade메쏘드 안에 db리셋 가능을 추가하자.

    @Override
    public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion) {
    	//기존 테이블을 지우고 
    	sqLiteDatabase.execSQL("DROP TABLE IF EXISTS " + WeatherEntry.TABLE_NAME);
    	//새 테이블 생성
    	onCreate(sqLiteDatabase);
    }

날짜 컬럼에 UNIQUE추가

마지막으로 Date Column에 Unique및 conflict 시 replace 조건을 추가하자. 참고:클릭

		...
		"UNIQUE (" + WeatherEntry.COLUMN_DATE + ") ON CONFLICT REPLACE);";
		...

버전도 3으로 업!

	...
    private static final int DATABASE_VERSION = 3;
    ...

Content Provider 만들기

Data package 밑에 ContentProvider클래스를 상속받는 WeatherProvider 클래스를 만들자.

public class WeatherProvider extends ContentProvider {
    // 아까 만들어 둔  DbHelper를 선언한다.
    WeatherDbHelper mOpenHelper;

    @Override
    public boolean onCreate() {
        //onCreate()안에서 WeatherDbhelper를 객체화한다. 
        mOpenHelper = new WeatherDbHelper(getContext());
        //셋업수행의 성공을 나타내기 위해 true를 리턴 
        return true;
    }
}

UriMatcher

다음으로는 UriMatcher에서 사용될 상수를 선언한다.

    ...
    //이 상수는 UriMatcher클래스가 Uri를 찾을 때 사용됨으로서 정규식을 사용하는 등의 과정을 피하게 해준다. 정규식을 왜 안쓰는가? 라고 묻는다면 있는 기능을 안 쓰는것이 더 말이 안된다고 한다..
    public static final int CODE_WEATHER = 100;
    public static final int CODE_WEATHER_WITH_DATE = 101;
    ..

UriMatcher를 반환하는 buildUriMatecher static 메쏘드를 하나 작성한다. (UriMatcher란? 참조)

public static UriMatcher buildUriMatcher() {

        //UriMatcher에 들어가는 uri는 그에 해당하는 반환될 상수값이 있을텐데.  만들때는 No_MATCH를 넣어주는 것은 흔한(?)일이다.
        final UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH);
        //addURI에 사용될 authority 도 가져오고..com.example.android.sunshine일 것이다. 
        final String authority = WeatherContract.CONTENT_AUTHORITY;

        // 이 URI는  content://com.example.android.sunshine/weather/이다 
        matcher.addURI(authority, WeatherContract.PATH_WEATHER, CODE_WEATHER);
        //이 URI는  content://com.example.android.sunshine/weather/1234 이다. #은 아무 숫자나 뜻한다.  

        matcher.addURI(authority, WeatherContract.PATH_WEATHER + "/#", CODE_WEATHER_WITH_DATE);

        return matcher;
    }

buildUriMatcher()를 통해 Static UriMater를 인스턴스화하자.

    // 인스턴스명 앞에 s를 붙이는 것은 static member임을 나타내며 이는 안드로이드 프로그래밍에서 흔한 것이라고 한다. 
    private static final UriMatcher sUriMatcher = buildUriMatcher();

필수 Method오버라이딩

ContentProvider를 상속받았기에 아래 메쏘드들을 오버라이딩한다. query, bulkInsert, delete, getType, insert, update, shutdown.

query 메쏘드를 오버라이딩한다. (근데 query는 contentResolver메쏘드 아니냐? 확인 필요.. )

@Override
    public Cursor query(@NonNull Uri uri, String[] projection, String selection,
                        String[] selectionArgs, String sortOrder) {

        Cursor cursor;

        // 이 메쏘드 하나로 날짜가 붙은 uri와 그렇지 않은 uri를 다 쓰려고 하기 때문에 sUriMatcher및 swith구문을 이용한다. 
        switch (sUriMatcher.match(uri)) {


            // 이 Uri의 형태는 "content://com.example.android.sunshine/weather/1472214172" 이런 식일 것이다. 이 때 는 특정 Row(날짜)의 Cursor를 반환하게 된다. 
            case CODE_WEATHER_WITH_DATE: {


                // 노말라이즈된 날짜 부분 - 즉 path, 위 예시에서 1472214172부분을 뜻함 - 을 가져오고 
                String normalizedUtcDateString = uri.getLastPathSegment();

                // selectionArgs은 array형태이므로 우리는 날짜 하나지만 string Array에 넣자 
                String[] selectionArguments = new String[]{normalizedUtcDateString};
                // 커서값에 mOpenHelper의 .getReadableDatabase()
                cursor = mOpenHelper.getReadableDatabase().query(
                        
                        WeatherContract.WeatherEntry.TABLE_NAME,

                        projection,

                        WeatherContract.WeatherEntry.COLUMN_DATE + " = ? ",
                        selectionArguments,
                        null,
                        null,
                        sortOrder);

                break;
            }


            // content://com.example.android.sunshine/weather/이렇게 생겼을 경우 

            case CODE_WEATHER: {
                cursor = mOpenHelper.getReadableDatabase().query(
                        WeatherContract.WeatherEntry.TABLE_NAME,
                        projection,
                        selection,
                        selectionArgs,
                        null,
                        null,
                        sortOrder);

                break;
            }

            default:
                throw new UnsupportedOperationException("Unknown uri: " + uri);
        }

        // setNotificationUri 를 호출한다. 
        cursor.setNotificationUri(getContext().getContentResolver(), uri);
        return cursor;
    }

다음으로 bulkInsert를 오버라이딩 하자. 기본적으로 bulkInsert는 테이블에 여러 값을 한 번에 집어넣는 메쏘드인데 Sunshine에서는 하나의 row만 집어넣는 메쏘드는 안 쓰므로 bulkInsert만 구현한다. (안드로이드에서 메쏘드 명 드래그 후 Cntl +B를 생활화하자..) 일반적인 ContentProvider에서는 한 줄씩 넣는것도 구현해야할듯. (insert)

    /**
     
     * @param uri    insertion request에 쓰일 content:// URI .
     * @param values 데이터베이스에 삽입될 column_name/value 짝의 Array 
     *               
     *
     * @return 삽입된 value 개수 
     */
    @Override
    public int bulkInsert(@NonNull Uri uri, @NonNull ContentValues[] values) {
        //먼저 getWritableDatabase로 db를 호출한다. 
        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
        //주어진 uri가 CODE_WEATHER일 경우에만 우리의 bulkInsert를 수행
        //날짜가 올바른지 검사 후 입력할 것이다. 
        switch (sUriMatcher.match(uri)) {

            case CODE_WEATHER:
                // beginTransaction을 열고 
                db.beginTransaction();
                // 반환할 값을 선언한다. 
                int rowsInserted = 0;
                try {
                    //전체 value array에 대해서
                    for (ContentValues value : values) {
                        // 각 Key/value pair에서, date를 "data"키값을 이용, Long형식으로 얻어낸 후 검사한다. 
                        long weatherDate =
                                value.getAsLong(WeatherContract.WeatherEntry.COLUMN_DATE);
                        // SunShineDateUtils. 의 보조 메쏘드 이용. 
                        if (!SunshineDateUtils.isDateNormalized(weatherDate)) {
                            throw new IllegalArgumentException("Date must be normalized to insert");
                        }
                        // 한 줄씩 insert한다! insert의 return값은 int 이다. 
                        long _id = db.insert(WeatherContract.WeatherEntry.TABLE_NAME, null, value);
                        // insert실패 시 -1반환. 
                        if (_id != -1) {
                            rowsInserted++;
                        }
                    }
                    // setTransactionSuccessful 과 endTransaction사이에는 어떠한 작업도 하지 말라고 한다. (javadoc)
                    db.setTransactionSuccessful();
                } finally {
                    db.endTransaction();
                }

                //한 줄이라도 들어가면 notifyChange를 콜한다. 
                if (rowsInserted > 0) {
                    getContext().getContentResolver().notifyChange(uri, null);
                }
                // 입력된 row 수 반환 
                return rowsInserted;
            // uri가 매치하지 않을 경우는 일반적인 bulkInsert하면 된다.
            default:
                return super.bulkInsert(uri, values);
        }
    }



마지막으로 delete메쏘드를 오버라이딩한다.

    /**
     * 주어진 URI와 추가적 argument를 이용해 데이터를 지웁니다.  
     *
     * @param uri           쿼리할 전체 URI
     * @param selection     지울 때 옵셔널하게 들어가는 
     * @param selectionArgs 
     * @return 지워진 row 개수 
     */
    @Override
    public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) {

        /* delete메쏘드를 사용하는 사람은 몇개 줄이 지워졋는지 알고 싶어할 것 */
        int numRowsDeleted;

        /*
         * 우리가 selection인자에 null을 넣으면 전체 테이블이 지워지지만 얼마나 지워졌는지 알수 없다. 이에 SQLiteDatabase 문서에 따르면 1을 집어넣으면 return값이 나온다고 한다..(?)
         */
        if (null == selection) selection = "1";

        switch (sUriMatcher.match(uri)) {

            case CODE_WEATHER:
                numRowsDeleted = mOpenHelper.getWritableDatabase().delete(
                        WeatherContract.WeatherEntry.TABLE_NAME,
                        selection,
                        selectionArgs);

                break;

            default:
                throw new UnsupportedOperationException("Unknown uri: " + uri);
        }

        /* 한 줄이라도 지우면 notify한다.  */
        if (numRowsDeleted != 0) {
            getContext().getContentResolver().notifyChange(uri, null);
        }


        return numRowsDeleted;
    }

댓글남기기