[안드로이드] Database(데이터베이스) SQLite / SQLiteOpenHelper
데이터베이스는 데이터가 저장되어 있는 공간이라고 생각할 수 있다. 이 데이터베이스는 우리가 어떤 시스템을 만들거나 앱을 만들 때 데이터를 저장해놓기 위해서 필수적으로 필요한 수단인 만큼 활용성이 크고 많이 사용된다. 데이터베이스에도 여러 종류가 있는데 현재 가장 많이 사용되는 것은 관계형 데이터베이스로 오라클, MySQL, 마리아디비, 몽고디비 등 여러 종류가 있다. 오늘은 안드로이드 내에 작은 임베디드 데이터베이스로 개발된 경량급 관계형 데이터베이스인 SQLite를 활용하는 방법을 알아보도록 하겠다.
SQLite는 서버에서 사용되는 큰 데이터베이스들과 같이 표준SQL을 지원한다. 이것은 기존에 웹이나 PC에서 사용하던 업무용 앱의 데이터 관리 기능을 그대로 사용할 수 있다는 것이다.
데이터베이스를 사용하는 방법은 먼저 데이터베이스를 만들고, 그 안에 테이블을 만들고, 테이블 안에 레코드(데이터)를 추가해주고, 조회를 할 수 있다.
위에 문장 그대로 순서대로 하나하나씩 하면 만들 수 있다. 좀 더 나아가면 테이블 간의 관계, 조인 등 더 활용되는 것이 있는데 그건 데이터베이스에 대해 따로 공부를 해야할 필요가 있다.
기본적으로 데이터를 다루는데 있어서 SELECT: 조회, INSERT: 입력, DELETE: 삭제, UPDATE: 수정(업데이트) 정도만 알고 있으면 된다.
주요 메소드
- openOrCreateDatabase(데이터베이스 이름, 사용모드, CursorFactory): 데이터베이스 생성
- rawQuery(): 데이터 조회를 할 때 Cursor 객체를 반환 받기 위해 사용
- Cursor 객체: 테이블에 들어있는 각각의 레코드를 순서대로 접근할 수 있도록 제공
- moveToNext(): 레코드를 순차적으로 넘겨줌
이 정도면 이해했다면 아래 코드는 바로 이해될 것 이다.
코드
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:text="데이터베이스 생성"
android:id="@+id/button1"/>
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/edittext1"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:text="테이블 생성"
android:id="@+id/button2"/>
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/edittext2"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:text="레코드(데이터) 추가"
android:id="@+id/button3"/>
<EditText
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:id="@+id/edittext3_1"/>
<EditText
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:id="@+id/edittext3_2"/>
<EditText
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:id="@+id/edittext3_3"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:text="데이터 조회"
android:id="@+id/button4"/>
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/edittext4"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:text="레코드 수정"
android:id="@+id/button5"/>
<EditText
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:id="@+id/edittext5_1"/>
<EditText
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:id="@+id/edittext5_2"/>
<EditText
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:id="@+id/edittext5_3"/>
</LinearLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/textview1"
android:textSize="20dp"/>
</LinearLayout>
MainActivity.java
public class MainActivity extends AppCompatActivity {
Button b1,b2,b3,b4,b5;
EditText et1,et2,et3_1,et3_2,et3_3,et4,et5_1,et5_2,et5_3;
TextView tv1;
SQLiteDatabase sqLiteDatabase;
String table_name;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
b1 = findViewById(R.id.button1);
b2 = findViewById(R.id.button2);
b3 = findViewById(R.id.button3);
b4 = findViewById(R.id.button4);
b5 = findViewById(R.id.button5);
et1 = findViewById(R.id.edittext1);
et2 = findViewById(R.id.edittext2);
et3_1 = findViewById(R.id.edittext3_1);
et3_2 = findViewById(R.id.edittext3_2);
et3_3 = findViewById(R.id.edittext3_3);
et4 = findViewById(R.id.edittext4);
et5_1 = findViewById(R.id.edittext5_1);
et5_2 = findViewById(R.id.edittext5_2);
et5_3 = findViewById(R.id.edittext5_3);
tv1 = findViewById(R.id.textview1);
b1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
try {
createDatabase(et1.getText().toString()); //데이터베이스 생성/열기
}catch (Exception e){
e.printStackTrace();
}
}
});
b2.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
try {
table_name = et2.getText().toString();
createTable(et2.getText().toString()); //테이블 생성
}catch (Exception e){
e.printStackTrace();
}
}
});
b3.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
try{
//데이터 입력
insertData(table_name, et3_1.getText().toString(), Integer.parseInt(et3_2.getText().toString()),et3_3.getText().toString());
}catch (Exception e){
e.printStackTrace();
}
}
});
b4.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
try{
//데이터 조회
selectData(table_name, et4.getText().toString());
}catch (Exception e){
e.printStackTrace();
}
}
});
b5.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
try{
//데이터 업데이트
updateData(table_name, et5_1.getText().toString(), Integer.parseInt(et5_2.getText().toString()),et5_3.getText().toString());
}catch (Exception e){
e.printStackTrace();
}
}
});
}
private void createDatabase(String database_name){
if(sqLiteDatabase == null) {
printLn("데이터베이스 생성 / " + database_name);
sqLiteDatabase = openOrCreateDatabase(database_name, MODE_PRIVATE, null);
}
else{
printLn("데이터베이스 이미 생성됨");
}
}
private void createTable(String table_name){
if(sqLiteDatabase != null) {
printLn("테이블 생성 / " + table_name);
sqLiteDatabase.execSQL("create table " + table_name + "(id integer primary key autoincrement, name text, age integer, major text);");
}
}
private void insertData(String table_name, String name, int age, String major){
if(sqLiteDatabase != null){
printLn("데이터 입력 완료");
String query = "insert into "+table_name + "(name, age, major) VALUES(?, ?, ?);";
Object[] params = new Object[]{name, age, major};
sqLiteDatabase.execSQL(query, params);
}
}
private void selectData(String table_name, String id){
if(sqLiteDatabase != null){
printLn("데이터 조회");
String query = "select * from " + table_name + ";"; //조회할 쿼리문
//rawQuery()메소드로 데이터 정보를 가져오고 Cursor 객체를 반환
Cursor cursor = sqLiteDatabase.rawQuery(query,null);
for (int i=0;i<cursor.getCount();i++) { //Cursor 객체에 있는 데이터 수만큼 반복
cursor.moveToNext(); //다음 커서로 넘기기
//cursor의 컬럼인덱스는 0부터 시작함, 현재 0은 id로 출력하지 않음
//컬럼인덱스는 테이블 속성들의 순서넘버라고 보면 된다.
String name = cursor.getString(1);
int age = cursor.getInt(2);
String major = cursor.getString(3);
printLn(name + " / " + age + " / " + major);
}
cursor.close();
}
}
private void updateData(String table_name, String name, int age, String major){
if(sqLiteDatabase != null){
printLn("데이터 수정 완료");
String query = "update " + table_name + " set age=?, major=? where name=?;";
Object[] params = new Object[]{age, major, name};
sqLiteDatabase.execSQL(query, params);
}
}
public void printLn(String text){
tv1.append(text + "\n");
}
}
selectData 메소드에서 조회하는데 있어서 select * from 테이블네임 where name(속성명)="Amy" 이런식으로 특정 데이터를 뽑아낼 수 있다.
* 은 이 테이블의 전체 데이터를 뽑아달라는 의미이고, where 이후에 조건을 설정해주는 것이다.
결과
Device File Explorer에 들어가서 /data/data/패키지명/databases 로 들어가면 데이터베이스가 만들어진 것을 볼 수 있다.(디폴트)
이렇게 데이터베이스를 만들고 조회, 업데이트도 해보았다. 그런데 테이블의 속성을 추가 or 삭제하고 싶거나 여러모로 테이블에 변형을 주고 싶을 경우 삭제하고 다시 만들면 제일 간단하지만, 앱이 서비스 중이면 사용자들의 데이터가 사라질 수 있다. 그래서 데이터베이스 버전을 관리할 수 있는 SQLiteOpenHelper를 사용해 볼 것이다.
주요 메소드
1. SQLiteOpenHelper를 상속하면 오버라이드 하는 메소드
- onCreate(): 데이터베이스가 처음 생성될 때 호출 (필수 정의)
- onUpgrade(): 버전이 바뀌어서 업그레이드 되었을 경우 호출 (필수 정의)
- onOpen(): 버전과 상관없이 실행 (필수 정의 아님)
2. 그 외
- getReadableDatabase(): SQLiteDatabase 객체를 반환하는데 보통 select 할 때 사용
- getWritableDatabase(): 마찬가지로 SQLiteDatabase 객체 반환, 데이터베이스에 무언가 적용시킬 때(insert, delete, update) 사용
위에 1번에서 언급한 것과 같이 SQLiteOpenHelper를 상속하는 클래스를 하나 만들고 그 안에 1번 메소드들을 오버라이드 해준다.
그리고 getWriableDatabases()나 getReadableDatabase()는 DB파일을 만들 때나 데이터를 읽어볼 때나 쓸 때 무언가 일을 할 때 호출하고 SQLiteDatabase 객체를 반환한다.
SQLiteDatabase은 DB가 만들어지고 업그레이드가 필요할 때 콜백 메소드가 호출된다는 점인데 여러 가지 상태에 따른 콜백 메소드를 다시 정의하면 상태에 맞는 처리가 가능하다.
코드
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:text="DB, Table 생성"
android:id="@+id/button1"/>
<EditText
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:id="@+id/edittext1_1"
android:hint="DB명"/>
<EditText
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:id="@+id/edittext1_2"
android:hint="테이블명"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:text="데이터 입력"
android:id="@+id/button2"/>
<EditText
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:id="@+id/edittext2_1"
android:hint="이름"/>
<EditText
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:id="@+id/edittext2_2"
android:hint="나이"/>
<EditText
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:id="@+id/edittext2_3"
android:hint="전공"/>
<EditText
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:id="@+id/edittext2_4"
android:enabled="false"
android:hint="순위"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:text="데이터 수정"
android:id="@+id/button3"/>
<EditText
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:id="@+id/edittext3_1"
android:hint="이름"/>
<EditText
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:id="@+id/edittext3_2"
android:hint="나이"/>
<EditText
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:id="@+id/edittext3_3"
android:hint="전공"/>
<EditText
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:id="@+id/edittext3_4"
android:enabled="false"
android:hint="순위"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:text="데이터 삭제"
android:id="@+id/button5"/>
<EditText
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:id="@+id/edittext5"
android:hint="이름"/>
</LinearLayout>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="데이터 조회"
android:id="@+id/button4"/>
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/textview1"
android:textSize="20dp"/>
</LinearLayout>
</ScrollView>
</LinearLayout>
MainActivity.java
public class MainActivity extends AppCompatActivity {
Button b1,b2,b3,b4,b5;
TextView tv1;
EditText et1_1, et1_2, et2_1, et2_2, et2_3, et2_4, et3_1, et3_2, et3_3, et3_4, et5;
Helper helper;
SQLiteDatabase sqLiteDatabase;
String database_name, table_name;
boolean up = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
b1 = findViewById(R.id.button1);
b2 = findViewById(R.id.button2);
b3 = findViewById(R.id.button3);
b4 = findViewById(R.id.button4);
b5 = findViewById(R.id.button5);
tv1 = findViewById(R.id.textview1);
et1_1 = findViewById(R.id.edittext1_1);
et1_2 = findViewById(R.id.edittext1_2);
et2_1 = findViewById(R.id.edittext2_1);
et2_2 = findViewById(R.id.edittext2_2);
et2_3 = findViewById(R.id.edittext2_3);
et2_4 = findViewById(R.id.edittext2_4);
et3_1 = findViewById(R.id.edittext3_1);
et3_2 = findViewById(R.id.edittext3_2);
et3_3 = findViewById(R.id.edittext3_3);
et3_4 = findViewById(R.id.edittext3_4);
et5 = findViewById(R.id.edittext5);
//DB 생성 버튼
b1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
table_name = et1_2.getText().toString();
database_name = et1_1.getText().toString();
helper = new Helper(getApplicationContext(), database_name, null, 1); //생성자 맞춰 helper 객체 변수 생성
sqLiteDatabase = helper.getWritableDatabase(); //DB 생성 호출 -> 버전 업그레이드가 아니라면 onCreate()가 실행될 것 이다.
}
});
//데이터 입력 버튼
b2.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
try {
if(up == false) { //업그레이드 전
String name = et2_1.getText().toString();
String age = et2_2.getText().toString();
String major = et2_3.getText().toString();
insertData(table_name, name, Integer.parseInt(age), major, -1); //입력 메소드 호출
}
else if(up == true){ //업그레이드 후
String name = et2_1.getText().toString();
String age = et2_2.getText().toString();
String major = et2_3.getText().toString();
String rank = et2_4.getText().toString();
insertData(table_name, name, Integer.parseInt(age), major, Integer.parseInt(rank)); //입력 메소드 호출
}
}catch (Exception e){
printLn("입력 오류");
}
}
});
//데이터 수정 버튼
b3.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
try {
if(up == false) {
String name = et3_1.getText().toString();
String age = et3_2.getText().toString();
String major = et3_3.getText().toString();
updateData(table_name, name, Integer.parseInt(age),major, -1); //수정 메소드 호출
}
else if(up == true){
String name = et3_1.getText().toString();
String age = et3_2.getText().toString();
String major = et3_3.getText().toString();
String rank = et3_4.getText().toString();
updateData(table_name, name, Integer.parseInt(age),major, Integer.parseInt(rank)); //수정 메소드 호출
}
}catch (Exception e){
printLn("입력 오류");
}
}
});
//데이터 삭제 버튼
b5.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String name = et5.getText().toString();
deleteData(table_name, name); //삭제 메소드 호출
}
});
//데이터 조회 버튼
b4.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
try{
selectData(table_name); //조회 메소드 호출
}catch (Exception e){
e.printStackTrace();
}
}
});
}
private void insertData(String table_name, String name, int age, String major, int rank){
printLn("데이터 입력 완료");
//입력 쿼리
if(up == false) {
String query = "insert into " + table_name + "(name, age, major) VALUES(?, ?, ?);";
Object[] params = new Object[]{name, age, major};
sqLiteDatabase = helper.getWritableDatabase();
sqLiteDatabase.execSQL(query, params);
}
else if(up == true){
String query = "insert into " + table_name + "(name, age, major, rank) VALUES(?, ?, ?, ?);";
Object[] params = new Object[]{name, age, major, rank};
sqLiteDatabase = helper.getWritableDatabase(); //객체 반환해서 지정해주고
sqLiteDatabase.execSQL(query, params); //실행
}
}
private void updateData(String table_name, String name, int age, String major, int rank){
printLn("데이터 수정 완료");
//업데이트 쿼리
if(up == false) {
String query = "update " + table_name + " set age=?, major=? where name=?;";
Object[] params = new Object[]{age, major, name};
sqLiteDatabase = helper.getWritableDatabase();
sqLiteDatabase.execSQL(query, params);
}
else if(up == true){
String query = "update " + table_name + " set age=?, major=?, rank=? where name=?;";
Object[] params = new Object[]{age, major, rank, name};
sqLiteDatabase = helper.getWritableDatabase();
sqLiteDatabase.execSQL(query, params);
}
}
private void deleteData(String table_name, String name){
printLn("데이터 삭제 완료");
//삭제 쿼리
String query = "delete from " + table_name + " where name=?;";
Object[] params = new Object[]{name};
sqLiteDatabase = helper.getWritableDatabase();
sqLiteDatabase.execSQL(query, params);
}
private void selectData(String table_name){
printLn("데이터 조회");
sqLiteDatabase = helper.getReadableDatabase();
//조회 쿼리
String query = "select * from " + table_name + ";";
Cursor cursor = sqLiteDatabase.rawQuery(query,null);
if(up == false) {
for (int i = 0; i < cursor.getCount(); i++) {
cursor.moveToNext();
String name = cursor.getString(1);
int age = cursor.getInt(2);
String major = cursor.getString(3);
printLn(name + " / " + age + " / " + major);
}
}
else if(up == true){
for (int i = 0; i < cursor.getCount(); i++) {
cursor.moveToNext();
String name = cursor.getString(1);
int age = cursor.getInt(2);
String major = cursor.getString(3);
int rank = cursor.getInt(4);
printLn(name + " / " + age + " / " + major + " / " + rank);
}
}
cursor.close();
}
private class Helper extends SQLiteOpenHelper{
public Helper(@Nullable Context context, @Nullable String name, @Nullable SQLiteDatabase.CursorFactory factory, int version) {
super(context, name, factory, version);
}
//DB를 최초 생성할 때 호출
@Override
public void onCreate(SQLiteDatabase db) {
printLn("onCreate()");
//데이터베이스 최초 생성 쿼리
String query = "create table " + table_name +"(id integer primary key autoincrement, name text, age integer, major text)";
db.execSQL(query);
printLn("데이터베이스 및 테이블 생성됨");
}
//업그레이드 되었을 경우 호출
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
printLn("onUpgrade()");
printLn(database_name + " / " + table_name + "이전버전: "+oldVersion + " 새로운버전: "+newVersion);
String query = "alter table " + table_name + " add column rank integer null;"; //rank라는 칼럼(속성) 하나를 추가해줬다.
db.execSQL(query);
et2_4.setEnabled(true);
et3_4.setEnabled(true);
up = true;
printLn("새로운 버전으로 업그레이드 완료");
}
@Override
public void onOpen(SQLiteDatabase db) {
super.onOpen(db);
printLn("onOpen()");
}
}
private void printLn(String text){
tv1.append(text + "\n");
}
}
결과
version을 1로 두고 최초 DB를 생성하였다.
TextView에 찍힌 것을 잘 보면 순서가 어떻게 됐는지 알 수 있다.
버전 업데이트 전이라 순위 입력은 불가능하다.
업그레이드가 된 후 모습이다. alter문으로 컬럼을 하나 추가해줬기 때문에 순위 입력도 가능해졌다.
부족한 점, 피드백 환영합니다.
감사합니다.
참고
정재곤, 『Do it! 안드로이드 앱 프로그래밍 - 개정 6판』, 이지스퍼블리싱(주)