ANDROID/Android 앱 프로그래밍
[Android] 데이터베이스(Database)와 내용 제공자(Content Provider)
주 녕
2021. 7. 6. 11:55
반응형
모든 내용은 Do it! 안드로이드 앱 프로그래밍을 바탕으로 정리한 것입니다.
내용 제공자(Content Provider)
: 앱에서 관리하는 데이터를 다른 앱에서 접근하도록 돕는 것
- 앱 구성요소이므로 시스템에서 관리하며 Manifest 파일에 등록해야 사용 가능함
- 내용 제공자가 필요한 이유? 앱의 보안
- 각 앱은 자신의 프로세스와 권한 안에서만 데이터에 접근할 수 있음
- A 앱과 B 앱은 각각 독립된 프로세스를 가지며, A는 A의 데이터를 B는 B의 데이터만 사용해야 함
- 가끔 서로 다른 앱의 데이터에 접근해야 할 때 내용 제공자를 사용하는 것
⭐ 내용 제공자의 동작은 CRUD 동작을 기준으로 함
CRUD : Create(생성), Read(조회), Update(수정), Delete(삭제)
→ 내용제공자는 insert(), query(), update(), delete() 메서드를 지원함
⭐ 내용 제공자를 사용하면 다른 앱에게 데이터 접근 통로를 열어줄 수 있음
👆 반드시 허용된 통로로만 접근해야 함!
→ 해당 통로로 접근하기 위해서 콘텐트 리졸버(ContentResolver) 객체가 필요함
[ 예제 ]
DatabaseHelper.java
- SQLiteOpenHelper 클래스 상속
- 생성자에서 person.db 파일(데이터 베이스)를 생성함
- onCreate() 메서드 안에서 execSQL() 메서드를 이용하여 person 테이블을 생성함
public class DatabaseHelper extends SQLiteOpenHelper {
private static final String DATABASE_NAME = "person.db";
private static final int DATABASE_VERSION = 1;
public static final String TABLE_NAME = "person";
public static final String PERSON_ID = "_id";
public static final String PERSON_NAME = "name";
public static final String PERSON_AGE = "age";
public static final String PERSON_MOBILE = "mobile";
public static final String[] ALL_COLUMNS = {PERSON_ID, PERSON_NAME, PERSON_AGE, PERSON_MOBILE};
// 테이블 생성 쿼리문
private static final String CREATE_TABLE =
"CREATE TABLE " + TABLE_NAME + " (" +
PERSON_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
PERSON_NAME + " TEXT, " +
PERSON_AGE + " INTEGER, " +
PERSON_MOBILE + " TEXT" +
");";
public DatabaseHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase sqLiteDatabase) {
sqLiteDatabase.execSQL(CREATE_TABLE);
}
@Override
public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVer, int newVer) {
sqLiteDatabase.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
onCreate(sqLiteDatabase);
}
}
PersonProvider.java
public class PersonProvider extends ContentProvider {
private static final String AUTHORITY = "com.june.providertest";
private static final String BASE_PATH = "person";
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/" + BASE_PATH);
private static final int PERSONS = 1;
private static final int PERSON_ID = 2;
private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
static {
uriMatcher.addURI(AUTHORITY, BASE_PATH, PERSONS);
uriMatcher.addURI(AUTHORITY, BASE_PATH + "/#", PERSON_ID);
}
private SQLiteDatabase database;
@Override
public boolean onCreate() {
// 헬퍼 클래스로 데이터베이스 생성
DatabaseHelper helper = new DatabaseHelper(getContext());
database = helper.getWritableDatabase();
return true;
}
- 내용 제공자를 만들기 위해 고유한 값을 가진 content URI가 필요함
- 예제에서는 앱의 패키지명과 person 테이블명을 합쳐 만들었음 (content://com.june.providertest/person/1)
- content:// - 내용 제공자에 의해 제어되는 데이터라는 의미
- Authority - 패키지명으로 지정한 부분을 가리키며 특정 내용 제공자를 구분하는 고유값
- BasePath - 테이블명으로 지정한 부분 가리키며 요청할 데이터의 자료형을 결정 (여기에선 테이블 명)
- ID - 맨 뒤에 1로 지정한 부분을 가리키며 요청할 데이터 레코드를 지정
- UriMatcher 객체 : URI를 매칭하는 역할
- addURI() 메서드 : UriMatcher 객체에 URI를 추가함
- match() 메서드를 호출하여 UriMatcher에 추가된 URI 중 실행 가능한 것이 있는지 확인
- ContentResolver 객체 : 내용 제공자에 접근하는 역할
- 액티비티에서 getContentResolver() 메서드를 호출하면 ConentResolver 객체를 반환함
- 이 객체에 insert(), query(), update(), delete() 등의 메서드가 정의되어 있음
- notifyChange() 메서드 : 레코드의 변경이 일어났음을 알려주는 역할
@Nullable
@Override
public String getType(@NonNull Uri uri) {
switch (uriMatcher.match(uri)) {
case PERSONS:
return "vnd.android.cursor.dir/persons";
default:
throw new IllegalArgumentException("알 수 없는 URI " + uri);
}
}
getType() 메서드 : MIME 타입이 무엇인지 알고 싶을 때 사용
- Uri 객체가 전달되며 결과 값으로 MIME 타입이 반환됨
- MIME 타입을 알 수 없는 경우 null을 반환함
@Nullable
@Override
public Uri insert(@NonNull Uri uri, @Nullable ContentValues contentValues) {
// 삽입된 새로운 레코드의 id값 반환
long id = database.insert(DatabaseHelper.TABLE_NAME, null, contentValues);
if (id > 0) {
Uri _uri = ContentUris.withAppendedId(CONTENT_URI, id); // 새로운 레코드의 uri
getContext().getContentResolver().notifyChange(_uri, null);
return _uri;
}
throw new SQLException("추가 실패 -> URI : " + uri);
}
insert() 메서드 : 내용 제공자를 이용하여 데이터를 추가할 때 사용
- 첫 번째 파라미터(uri)
- 두 번째 파라미터(contentValues) : 저장한 칼럼명과 값들이 들어간 ContentValues 객체
- 결과 값 : 새로 추가된 Uri 정보 반환
@Nullable
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] strings, @Nullable String s, @Nullable String[] strings1, @Nullable String s1) {
Cursor cursor;
switch (uriMatcher.match(uri)) {
case PERSONS:
cursor = database.query(DatabaseHelper.TABLE_NAME, DatabaseHelper.ALL_COLUMNS, s, null, null, null,
DatabaseHelper.PERSON_NAME + " ASC");
break;
default:
throw new IllegalArgumentException("알 수 없는 URI " + uri);
}
cursor.setNotificationUri(getContext().getContentResolver(), uri);
return cursor;
}
query() 메서드 : 내용 제공자를 이용하여 값을 조회하고 싶을 때 사용
- 첫 번째 파라미터(uri)
- 두 번째 파라미터(String[] strings) : 어떤 칼럼들을 조회할 것인지 지정 (null인 경우 모든 칼럼 조회)
- 세 번째 파라미터(String s) : SQL의 where 절 조건 (null인 경우 where 절이 없는 것과 동일)
- 네 번째 파라미터(String[] strings1) : 세 번째 파라미터에 값이 있을 경우 그 안에 들어갈 조건 값을 대체하기 위해 사용
- 다섯 번째 파라미터(String s1) : 정렬 칼럼을 지정 (null인 경우 정렬X)
@Override
public int update(@NonNull Uri uri, @Nullable ContentValues contentValues, @Nullable String s, @Nullable String[] strings) {
int count = 0;
switch (uriMatcher.match(uri)) {
case PERSONS:
count = database.update(DatabaseHelper.TABLE_NAME, contentValues, s, strings);
break;
default:
throw new IllegalArgumentException("알 수 없는 URI " + uri);
}
getContext().getContentResolver().notifyChange(uri, null);
return count;
}
}
update() 메서드 : 내용 제공자를 이용해 값을 수정하고 싶을 때 사용
- 첫 번째 파라미터(uri)
- 두 번째 파라미터(contentValues) : 저장할 칼럼명과 값들이 들어간 ContentValues 객체 (null이면 XXX)
- 세 번째 파라미터(String s) : SQL의 where 절 조건 (null인 경우 where 절이 없는 것과 동일)
- 네 번째 파라미터(String[] selectionsArgs) : 세 번째 파라미터에 값이 있을 경우 그 안에 들어갈 조건 값을 대체하기 위해 사용
- 결과 값 : 영향을 받은 레코드의 개수
@Override
public int delete(@NonNull Uri uri, @Nullable String s, @Nullable String[] strings) {
int count = 0;
switch (uriMatcher.match(uri)) {
case PERSONS:
count = database.delete(DatabaseHelper.TABLE_NAME, s, strings);
break;
default:
throw new IllegalArgumentException("알 수 없는 URI " + uri);
}
getContext().getContentResolver().notifyChange(uri, null);
return count;
}
delete() 메서드 : 내용 제공자를 이용해 값을 삭제하고 싶을 때 사용
- 첫 번째 파라미터(uri)
- 두 번째 파라미터(String s) : SQL의 where 절 조건 (null인 경우 where 절이 없는 것과 동일)
- 세 번째 파라미터(String[] selectionsArgs) : 두 번째 파라미터에 값이 있을 경우 그 안에 들어갈 조건 값을 대체하기 위해 사용
- 결과 값 : 영향을 받은 레코드의 개수
MainActivity.java
public class MainActivity extends AppCompatActivity {
TextView textView;
String uriString = "content://com.june.providertest/person";
Uri uri = new Uri.Builder().build().parse(uriString);
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
textView = findViewById(R.id.textview_result);
Button InsertBtn = findViewById(R.id.btn_insert);
InsertBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
insertPerson(uri);
}
});
Button QueryBtn = findViewById(R.id.btn_query);
QueryBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
queryPerson(uri);
}
});
Button UpdateBtn = findViewById(R.id.btn_update);
UpdateBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
updatePerson(uri);
}
});
Button DeleteBtn = findViewById(R.id.btn_delete);
DeleteBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
deletePerson(uri);
}
});
}
public void printText(String text) {
textView.append(text+"\n");
}
- 사용할 Uri 문자열은 Uri 클래스의 parse() 메소드를 이용하여 Uri 객체로 변환하여 사용
- 변환한 Uri 객체를 매개변수로 하는 생성, 조회, 수정, 삭제 버튼의 기능 메소드를 만듦
public void insertPerson(Uri uri) {
printText("insertPerson 호출");
Cursor cursor = getContentResolver().query(uri, null, null, null, null, null);
String[] columns = cursor.getColumnNames();
printText("columns count -> " + columns.length);
for (int i = 0; i < columns.length; i++) {
printText("#" + i + " : " + columns[i]);
}
ContentValues values = new ContentValues();
values.put("name", "person"+ Integer.toString(cursor.getCount()+1));
values.put("age", cursor.getCount()+20);
values.put("mobile", "010-0000-0000");
uri = getContentResolver().insert(uri, values); // 삽입 후 새로운 URI 반환
printText("insert 결과 -> " + uri.toString());
}
- ContentResolver 객체의 query() 메소드를 호출하여 Uri 객체를 파라미터로 정의하고 Cursor 객체를 반환 받음
- Cursor 객체의 getColumnNames() 메소드를 이용하여 컬럼들의 이름을 조회함
- ContentValues 객체로 새로 생성할 레코드를 만들어줌 (Cursor를 이용하여 현재 레코드의 개수에 1과 20을 더해 새로운 레코드를 생성함)
- ContentResolver 객체의 insert() 메소드를 이용하여 레코드를 추가하고 새로운 Uri 객체를 반환받음
public void queryPerson(Uri uri) {
try {
String[] columns = new String[] {"name", "age", "mobile"};
Cursor cursor = getContentResolver().query(uri, columns, null, null, "name ASC");
printText("query 결과 : " + cursor.getCount());
int index = 0;
while(cursor.moveToNext()) {
String name = cursor.getString(cursor.getColumnIndex(columns[0]));
int age = cursor.getInt(cursor.getColumnIndex(columns[1]));
String mobile = cursor.getString(cursor.getColumnIndex(columns[2]));
printText("#" + index + " -> " + name + ", " + age + ", " + mobile);
index += 1;
}
} catch (Exception e) {
e.printStackTrace();
}
}
- Cursor 객체의 getColumnIndex() 메서드를 이용하여 조회할 칼럼의 이름을 매개변수로 전달하고 지정 칼럼의 값을 반환받음
- while문에서 cursor.moveToNext() 메서드를 이용하여 다음 레코드로 커서를 넘겨줌
public void updatePerson(Uri uri) {
String selection = "mobile = ?";
String[] selectionArgs = new String[] {"010-0000-0000"};
ContentValues updateValue = new ContentValues();
updateValue.put("mobile", "010-1000-2000");
int count = getContentResolver().update(uri, updateValue, selection, selectionArgs);
printText("update 결과 : " + count);
}
- ContentResolver의 update() 메서드를 이용하여 호출하면서 Uri 객체, ContentValues 객체, where 조건, where 조건의 ? 기호를 대체할 값을 차례로 넣음
public void deletePerson(Uri uri) {
String selection = "age >= 20";
int count = getContentResolver().delete(uri, selection, null);
printText("delete 결과 : " + count);
}
}
- ContentResolver의 delete() 메서드를 사용해서 원하는 데이터를 삭제할 수 있음
- 해당 예제에서는 age 컬럼 값이 20 이상인 경우 모두 삭제해 주었고, 삭제된 레코드의 개수를 표시해주었음
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.june.providertest">
<permission android:name="com.june.providertest.READ_DATABASE" android:protectionLevel="normal"/>
<permission android:name="com.june.providertest.WRITE_DATABASE" android:protectionLevel="normal"/>
<application
...>
<provider
android:authorities="com.june.providertest"
android:name=".PersonProvider"
android:exported="true"
android:readPermission="com.june.provider.READ_DATABASE"
android:writePermission="com.june.provider.WRITE_DATABASE"/>
...
</application>
</manifest>
- <permission> : 권한을 새로 정의할 때 사용하는 태그
- .READ_DATABASE, .WRITE_DATABASE
- protectionaLevel 속성
- <provider>
- authorities 속성 : 내용 제공자를 정의할 때 설정한 authorities 값과 동일하게 설정
- name 속성 : 내용 제공자 클래스로 설정한 클래스 명으로 설정
- readPermission 속성 : .READ_DATABASE 권한으로 설정
- writePermission 속성 : .WRITE_DATABASE 권한으로 설정
*DatabaseHelper 클래스와 MIME TYPE에 대한 자세한 내용은 아래 포스팅 참고!
반응형