본문

160118P(월)

Android Chapter 26 - CP


SQLite

초경량급 관계형 데이터베이스

Android는 운영체제 차원으로 SQLite 라이브러리를 포함하고 있다.

안정적이고 용량이 작아서 소규모의 DB에 적합하다.


데이터를 저장하는 장소가 단순한 파일

복수사용자 지원 불가

용량이 작고 C언어로 작성되어 속도가 빠르다.


SQLite의 데이터타입

사용할 수 있는 자료형 5가지 

NULL : 널값 

INTEGER : 1,2,3,4,6,8bytes의 정수값(자동증가 시킬 경우 INTEGER PRIMARY KEY AUTOINCREMENT 로 표기)

REAL : 8bytes의 부동소수점값 

TEXT : UTF-8, UTF-16BE, UTF-16LE인코딩의 문자열 

BLOB : 입력된 그대로 저장, 바이너리 파일 등 


SQLite는 다른 데이터베이스에 비해 매우 적은 자료형만을 제공한다. 그러면 다른 형태는 어떻게 만들면 될까? 


Boolean타입: INTEGER로 만들어서 0과 1로 구별한다. 

Date, Time타입 : 

TEXT : YYYY-MM-DD HH:MM:SS.SSS 형태로 저장 

REAL : 율리우스력을 기준으로 하는 정보를 저장 

INTEGER : UTC타입으로 저장    


결국 SQLite에 숫자, 문자, 바이너리를 넣을 수 있으니 자유롭게 넣고 개발자가 알아서 정해서 사용하면 된다는 것이 요지다. 


상당히 일리있는 형태로 SQLite는 대용량의 데이터를 처리하기에는 적합하지 않기에 대형프로그램보다는 지금 가장 많이 사용되고 있는 안드로이드 어플 개발 등 작은 프로그램 개발에 사용된다. 즉 다수의 개발자가 아닌 소수 혹은 혼자서 개발하는 경우가 많을 것이기에 좀 더 가볍고 자유롭게 사용할 수 있게 하는 편이 더 낫다고 본다. 


http://redroid.tistory.com/1

http://www.tutorialspoint.com/sqlite/sqlite_data_types.htm


SQLiteOpenHelper

DB생성 및 오픈하는 처리 담당

추상메소드이므로 DB 구조에 맞게 메소드를 정의하고 적절한 스크립트를 작성


SQLiteOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version);


context

DB생성 컨텍스트이되 보통 메인 액티비티이다.


name, version

DB 파일의 이름과 버전

추후 DB를 생성, 업데이트할 때 사용된다.


factory

커스텀 커서를 사용할 때 지정

표준 커서 사용시에는 null


콜백메소드 호출

onCreate

DB가 처음 만들어질 때 호출

테이블을 만들고 초기 레코드를 삽입

CREATE TABLE 문을 실행하여 테이블 생성


onUpgrade

DB를 업그레이드 할 때 호출

버전이 더 높으면 기존 테이블을 삭제하고 새로 만들거나 ALTER TABLE로 스키마를 수정


onOpen

DB를 열 때 호출


관련 메소드

리턴타입은 SQLiteDatabase


getReadableDatabase

읽기위해서 DB를 연다

SQLiteOpenHelper가 알아서 DB가 없으면 onCreate가 호출되며 버전이 바뀌었으면 onUpgrade가 호출된다.


getWritableDatabase

읽고 쓰기위해서 DB를 연다.

권한이 없다거나 디스크가 가득차면 실패


close

DB를 닫는다.


public class EnglishWord extends Activity {

WordDBHelper mHelper;

EditText mText;

public void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.englishword);


mHelper = new WordDBHelper(this);

mText = (EditText)findViewById(R.id.edittext);

}

public void mOnClick(View v) {

SQLiteDatabase db;

ContentValues row;


switch (v.getId()) {

case R.id.insert:

db = mHelper.getWritableDatabase();

// insert 메서드로 삽입

row = new ContentValues();

row.put("eng", "boy");

row.put("han", "머스마");

db.insert("dic", null, row);

// SQL 명령으로 삽입

db.execSQL("INSERT INTO dic VALUES (null, 'girl', '가시나');");

mHelper.close();

mText.setText("Insert Success");

break;


case R.id.delete:

db = mHelper.getWritableDatabase();

// delete 메서드로 삭제

db.delete("dic", null, null);

// SQL 명령으로 삭제

//db.execSQL("DELETE FROM dic;");

mHelper.close();

mText.setText("Delete Success");

break;


case R.id.update:

db = mHelper.getWritableDatabase();

// update 메서드로 갱신

row = new ContentValues();

row.put("han", "소년");

db.update("dic", row, "eng = 'boy'", null);

// SQL 명령으로 갱신

//db.execSQL("UPDATE dic SET han = '소년' WHERE eng = 'boy';");

mHelper.close();

mText.setText("Update Success");

break;


case R.id.select:

db = mHelper.getReadableDatabase();

Cursor cursor;

// query 메서드로 읽기

//cursor = db.query("dic", new String[] {"eng", "han"}, null, 

// null, null, null, null);

// SQL 명령으로 읽기

cursor = db.rawQuery("SELECT eng, han FROM dic", null);

String Result = "";

while (cursor.moveToNext()) {

String eng = cursor.getString(0);

String han = cursor.getString(1);

Result += (eng + " = " + han + "\n");

}


if (Result.length() == 0) {

mText.setText("Empyt Set");

} else {

mText.setText(Result);

}

cursor.close();

mHelper.close();

break;

}

}

}


class WordDBHelper extends SQLiteOpenHelper {

public WordDBHelper(Context context) {

super(context, "EngWord.db", null, 1);

}


public void onCreate(SQLiteDatabase db) {

db.execSQL("CREATE TABLE dic (_id INTEGER PRIMARY KEY AUTOINCREMENT, " + " eng TEXT, han TEXT);");

}


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

// 실제로는 ALTER TABLE 문으로 테이블의 구조만 변경하는것이 원칙이다.  

db.execSQL("DROP TABLE IF EXISTS dic");

onCreate(db);

}

}


Helper를 사용하지 않고 직접 DB생성

필요할 때 즉석에서 만들어 쓸 수 있다.

통상 임시적인 정보 저장용으로 사용되며 다 사용하고 난 후 deleteDatabase 메소드로 삭제

버전이 바뀐 경우, DB가 있는경우, 없는경우를 직접 판단해야하므로 Helper가 편하다.


SQLiteDatabase Context.openOrCreateDatabase(String name, int mode, SQLiteDatabase.CursorFactory factory)

boolean deleteDatabase(String name)


쿼리 실행

1. getWritableDatabase 메소드로 DB 오픈

2. Helper가 판단하여 DB 객체 리턴

3. 레코드 관리

ContentValues 타입

빈객체를 만든 후 put으로 필드와 값의 쌍을 저장


void put(String key, Integer value)

void put(String key, String value)

void put(String key, Boolean value)


long SQLiteDatabase.insert(String table, String nullColumnHack, ContentValues values)

void execSQL("INSERT INTO DB_이름 VALUES (null, 'girl', '가시나');");


int delete(String table, String whereClause, String[] whereArgs)

int update(String table, ContentValues values, String whereClause, String[] whereArgs)


Cursor query(boolean distinct, String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy, String limit)


SELECT는 결과셋을 리턴하므로 execSQL이 아니라 rawQuery메소드로 실행한다.


Android는 쿼리 메소드와 SQL을 둘다 제공한다.

솔직히 SQL이 더 사용하기 편하고 어차피 쿼리 메소드도 SQL명령으로 변환하여 쿼리를 수행하지만, 그럼에도 불구하고 사용하는 이유는 편의성보다 CP에서 정보를 읽을 때 DB 액세스 방식과 똑같은 형식의 메소드를 제공함으로써 임의의 정보를 엑세스 하는 방식을 통일하기 위함이다. 따라서 통일된 정보를 위해서 쿼리 메소드를 사용하는것이 권장된다.


커서 메소드

쿼리 결과가 굉장히 많을 수 있으므로 결과셋 자체가 리턴되는것이 아니라 커서로 리턴



SQLite는 타입 점검을 느슨하게 한다.

따라서 반드시 컬럼의 타입과 같은 타입으로 읽을 필요는 없다.

ex) 정수 컬럼을 getString으로 읽을 수 있다. 반대도 가능


필드번호를 구할때

getColumnIndex로 구하며 레코드를 읽을 때마다 컬럼번호를 일일이 조사하면 느려진다.

그러므로 루프 밖에서 미리 구해놓아야 함


커서 바인딩

결과셋이 굉장히 많을 때는 쿼리로 전체를 다 읽어 화면에 출력하면 느리다.

이럴때는 커서를 어댑터에 바인딩해 놓고 어댑터뷰로 출력한다.


SimpleCursorAdapter(Context context, int layout, Cursor C, String[] from, int[] to)


from

커서의 열 이름 배열


to

각 열이 출력될 위젯의 ID 배열


public class ProductList extends Activity {

ProductDBHelper mHelper;


@SuppressWarnings("deprecation")

public void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.productlist);

mHelper = new ProductDBHelper(this);

Cursor cursor;

SQLiteDatabase db = mHelper.getWritableDatabase();


cursor = db.rawQuery("SELECT * FROM product", null);

startManagingCursor(cursor);


SimpleCursorAdapter Adapter = null;

Adapter = new SimpleCursorAdapter(this, 

android.R.layout.simple_list_item_2,

cursor, new String[] { "name", "price" }, 

new int[] { android.R.id.text1, android.R.id.text2});

ListView list = (ListView)findViewById(R.id.list);

list.setAdapter(Adapter);

}

}


class ProductDBHelper extends SQLiteOpenHelper {

public ProductDBHelper(Context context) {

super(context, "Product.db", null, 1);

}


public void onCreate(SQLiteDatabase db) {

db.execSQL("CREATE TABLE product ( _id INTEGER PRIMARY KEY AUTOINCREMENT, " +

"name TEXT, price INTEGER);");

db.execSQL("INSERT INTO product VALUES (null, '오징어 땅콩', 900);");

db.execSQL("INSERT INTO product VALUES (null, '농심 포테이토 칩', 2000);");

db.execSQL("INSERT INTO product VALUES (null, '로보트 태권 V', 1000);");

db.execSQL("INSERT INTO product VALUES (null, '꼬마 자동차 붕붕', 1500);");

db.execSQL("INSERT INTO product VALUES (null, '윈도우즈 API 정복', 32000);");

db.execSQL("INSERT INTO product VALUES (null, '롯데 인벤스 아파트', 190000000);");

db.execSQL("INSERT INTO product VALUES (null, '88 라이트', 1900);");

db.execSQL("INSERT INTO product VALUES (null, '프라이드 1.6 CVVT 골드', 8900000);");

db.execSQL("INSERT INTO product VALUES (null, '캐리비안 베이 입장권', 25000);");

}


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

db.execSQL("DROP TABLE IF EXISTS product");

onCreate(db);

}

}


startManagingCursor

액티비티의 생명주기에 맞춰 커서를 자동으로 관리하도록 요청

커서는 액티비티와 운명을 같이 한다.


CP(Content Provider)

응용 프로그램끼리 정보를 공유하는 유일한 방법

Android는 보안정책상 응용프로그램이 만든 데이터는 혼자서만 액세스 가능하다.

이 데이터를 외부로 공개하려면 CP를 제공해야 한다.


URI(Uniform Resource Identifier)

정보의 고유한 명칭

웹상의 주소를 나타내는 URI보다 더 상위의 개념

누가 어떤 정보를 제공하는지, 어떤 정보를 원하는지에 대한 정보


문자열 형태이고 국제표준(RFC 2396)을 따른다.

content://authority/path/id


content://

이 문자열이 URI임을 나타낸다.


authority

정보 제공자의 명칭

중복되면 안되므로 패키지명을 사용할 것을 권장


path

정보의 종류를 지정하는 가상의 경로


id

어떤 정보를 원하는가 기술

전체 정보를 원할때는 생략


단수 URI

뒤에 id가 붙는다.

id까지 있다.


복수 URI

path까지만 있다.

id가 없음


URI 객체 생성

static Uri parse(String uriString)


예외처리를 하지 않으므로 굉장히 위험하다.

반드시 충분한 주의를 기울이자


path정보 조사

List<String> getPathSegments()


0번째 요소가 path

1번째 요소가 id

/는 제외된다.


UriMatcher 유틸리티 클래스

일일이 추출하는것은 빙시같고, 속도도 느리며 단수 복수를 구분하기도 까다롭다.

문자열 안의 요구 정보를 분석하여 정수 코드로 변환


void addURI(String authority, String path, int code)

authority와 path 쌍을 정수코드와 대응시켜 맵(map)을 등록한다.


int match(Uri uri)

uri를 분석하여 등록된 정수코드를 리턴

없으면 -1


보통 switch case문으로 분기하여 사용하므로 편하다


자료공유

데이터의 MIME 타입조사

String getType(Uri uri)


단수 : vnd.회사명.cursor.item/타입

복수 : vnd.회사명.cursor.dir/타입


public class EWProvider extends ContentProvider {

static final Uri CONTENT_URI = Uri.parse("content://andexam.ver4_1.EnglishWord/word");

 static final int ALLWORD = 1;

 static final int ONEWORD = 2;

static final UriMatcher Matcher;

static {

Matcher = new UriMatcher(UriMatcher.NO_MATCH);

Matcher.addURI("andexam.ver4_1.EnglishWord", "word", ALLWORD);

Matcher.addURI("andexam.ver4_1.EnglishWord", "word/*", ONEWORD);

}

SQLiteDatabase mDB;


public boolean onCreate() {

WordDBHelper helper = new WordDBHelper(getContext());

mDB = helper.getWritableDatabase();

   return true;

}


public String getType(Uri uri) {

if (Matcher.match(uri) == ALLWORD) {

return "vnd.EnglishWord.ver4_1.andexam.cursor.item/word";

}

if (Matcher.match(uri) == ONEWORD) {

return "vnd.EnglishWord.ver4_1.andexam.cursor.dir/word";

}

return null;

}


public Cursor query(Uri uri, String[] projection, String selection,

String[] selectionArgs, String sortOrder) {

String sql;

// 전체에 대한 쿼리 명령

sql = "SELECT eng, han FROM dic";

// 단어 선택 where절 추가

if (Matcher.match(uri) == ONEWORD) {

sql += " where eng = '" + uri.getPathSegments().get(1) + "'";

}


Cursor cursor = mDB.rawQuery(sql, null);

return cursor;

}


public Uri insert(Uri uri, ContentValues values) {

long row = mDB.insert("dic", null, values);

if (row > 0) {

Uri notiuri = ContentUris.withAppendedId(CONTENT_URI, row);

getContext().getContentResolver().notifyChange(notiuri, null);

return notiuri;

}

return null;

}


public int delete(Uri uri, String selection, String[] selectionArgs) {

int count = 0;

//*

switch (Matcher.match(uri)) {

case ALLWORD:

count = mDB.delete("dic", selection, selectionArgs);

break;

case ONEWORD:

String where;

where = "eng = '" + uri.getPathSegments().get(1) + "'";

if (TextUtils.isEmpty(selection) == false) {

where += " AND" + selection;

}

count = mDB.delete("dic", where, selectionArgs);

break;

}

getContext().getContentResolver().notifyChange(uri, null);

return count;

//*/

/*

String sql;

// 전체에 대한 쿼리 명령

sql = "DELETE FROM dic";

// 단어 선택 where절 추가

if (Matcher.match(uri) == ONEWORD) {

sql += " where eng = '" + uri.getPathSegments().get(1) + "'";

}

mDB.execSQL(sql);

return 1;

//*/

}


public int update(Uri uri, ContentValues values, String selection,

String[] selectionArgs) {

int count = 0;

switch (Matcher.match(uri)) {

case ALLWORD:

count = mDB.update("dic", values, selection, selectionArgs);

break;

case ONEWORD:

String where;

where = "eng = '" + uri.getPathSegments().get(1) + "'";

if (TextUtils.isEmpty(selection) == false) {

where += " AND " + selection;

}

count = mDB.update("dic", values, where, selectionArgs);

break;

}

getContext().getContentResolver().notifyChange(uri, null);

return count;

}

}


CP의 메니페스트 등록

application 태크 아래, activity와 같은수준에 선언

<provider android:name=".CP이름"

android:authorities="패키지명"

android:exported="true" />


4.1 이하의 버전은 별도의 선언없이 외부공개 되었으나, 4.1 이후에는 보안을 이유로 디폴트로 공개가 아니다.

따라서 exported로 명시적 공개를 한다.


CP사용

public class CallWordCP extends Activity {

static final String WORDURI = "content://andexam.ver4_1.EnglishWord/word";

EditText mText;

public void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.callwordcp);


mText = (EditText)findViewById(R.id.edittext);

}

public void mOnClick(View v) {

ContentResolver cr = getContentResolver();

switch (v.getId()) {

// 전부 읽기

case R.id.readall:

Cursor cursor = cr.query(Uri.parse(WORDURI), null, null, null, null);

String Result = "";

while (cursor.moveToNext()) {

String eng = cursor.getString(0);

String han = cursor.getString(1);

Result += (eng + " = " + han + "\n");

}


if (Result.length() == 0) {

mText.setText("Empyt Set");

} else {

mText.setText(Result);

}

cursor.close();

break;

// 하나만 읽기

case R.id.readone:

Cursor cursor2 = cr.query(Uri.parse(WORDURI + "/boy"), 

null, null, null, null);


String Result2 = "";

if (cursor2.moveToFirst()) {

String eng = cursor2.getString(0);

String han = cursor2.getString(1);

Result2 += (eng + " = " + han + "\n");

}

if (Result2.length() == 0) {

mText.setText("Empyt Set");

} else {

mText.setText(Result2);

}

cursor2.close();

break;

// 삽입

case R.id.insert:

ContentValues row = new ContentValues();

row.put("eng", "school");

row.put("han", "학교");


cr.insert(Uri.parse(WORDURI), row);

mText.setText("Insert Success");

break;

// 삭제

case R.id.delete:

cr.delete(Uri.parse(WORDURI), null, null);

mText.setText("Delete Success");

break;

// 수정

case R.id.update:

ContentValues row2 = new ContentValues();

row2.put("han", "핵교");

cr.update(Uri.parse(WORDURI + "/school"), row2, null, null);

mText.setText("Update Success");

break;

}

}

 }


ContentResolver 
콘텐츠를 읽기 위해서 ContentResolver 객체를 구한다.
쿼리를 통해 CP와 간접적으로 통신하는 표준 인터페이스
응용 프로그램마다 ContentResolver를 하나씩 가지고 있다.

ContentResolver ContextWrapper.getContentResolver()


1. 시스템은 URI로부터 콘텐츠 제공자를 찾아 CP를 로드, CP 메소드 호출

2. URI에 기록된 authority와 각 프로그램의 매니페스트의 authority를 비교하고 검색

3. 정보제공쪽과 받는 쪽이 미리 URI를 약속해두어야 한다.

4. CP의 기동은 시스템이 알아서 한다.

5. Resolver가 구해지면 CP의 데이터를 자신의 것처럼 관리 가능하다.


CP 제공 응용 프로그램이 실행중이 아니면 시스템이 메모리로 올린다.


Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)


Uri insert(Uri url, ContentValues values)

int bulkInsert(Uri url, ContentValues[] values)


Uri delete(Uri url, String where, String[] selectionArgs)

Uri update(Uri url, ContentValues values, String where, String[] selectionArgs)


보안

CP는 필요할 경우 퍼미션을 요구할 수 있으며, 특정 응용 프로그램만 데이터에 접근할 수 있도록 차단 가능하다.

퍼미션이 없는 응용 프로그램은 설치 단계에서 거부된다.



'Mobile > Android' 카테고리의 다른 글

160321A(월)  (0) 2016.03.21
160319A(토)  (0) 2016.03.19
160302P(수)  (0) 2016.03.03
160210P(수)  (0) 2016.02.11
160119P(화)  (0) 2016.01.19

공유

댓글