Android系统内建“SQLite”数据库,它是一个开放的小型数据库,它跟一般商用的大型数据库有类似的架构与用法,例如MySQL数据库。应用程式可以建立自己需要的数据库,在数据库中使用Android API执行资料的管理和查询的工作。储存资料的数量是根据装置的储存空间决定的,所以如果空间足够的话,应用程式可以储存比较大量的资料,在需要的时候随时可以执行数据库的管理和查询的工作。
一般商用的大型数据库,可以提供快速存取与储存非常大量的资料,也包含网络通讯和复杂的存取权限管理,不过它们都会使用一种共通的语言“SQL”,不同的数据库产品都可以使用SQL这种数据库语言,执行资料的管理和查询的工作。SQLite数据库虽然是一个小型数据库,不过它跟一般大型数据库的架构与用法也差不多,同样可以使用SQL执行需要的工作,Android另外提供许多数据库的API,让开发人员使用API执行数据库的工作。
这一章会从了解应用程式数据库的需求开始,介绍如何建立数据库与表格,在应用程式运作的过程中,如何执行数据库的新增、修改、删除与查询的工作。
## 11-1 设计数据库表格
在数据库的技术中,一个数据库(Database)表示应用程式储存与管理资料的单位,应用程式可能需要储存很多不同的资料,例如一个购物网站的数据库,就需要储存与管理会员、商品和订单资料。每一种在数据库中的资料称为表格(Table),例如会员表格可以储存所有的会员资料。
SQLite 数据库的架构也跟一般数据库的概念类似,所以应用程式需要先建立好需要的数据库与表格后,才可以执行储存与管理资料的工作。建立表格是在Android应用程式中,唯一需要使用SQL执行的工作。其它执行数据库管理与查询的工作,Android都提供执行各种功能的API,使用这些API就不需要了解太多SQL这种数据库语言。
建立数据库表格使用SQL的“CREATE TABLE”指令,这个指令需要指定表格的名称,还有这个表格用来储存每一笔资料的字段(Column)。这些需要的表格字段可以对应到主要类别中的字段变量,不过SQLite数据库的资料型态只有下面这几种,使用它们来决定表格字段可以储存的资料型态:
* INTEGER – 整数,对应Java 的byte、short、int 和long。
* REAL – 小数,对应Java 的float 和double。
* TEXT – 字串,对应Java 的String。
在设计表格字段的时候,需要设定字段名称和型态,表格字段的名称建议就使用主要类别中的字段变量名称。表格字段的型态依照字段变量的型态,把它们转换为SQLite提供的资料型态。通常在表格字段中还会加入“NOT NULL”的指令,表示这个表格字段不允许空值,可以避免资料发生问题。
表格的名称可以使用主要类别的类别名称,一个SQLite表格建议一定要包含一个可以自动为资料编号的字段,字段名称固定为“_id”,型态为“INTEGER”,后面加上“PRIMARY KEY AUTOINCREMENT”的设定,就可以让SQLite自动为每一笔资料编号以后储存在这个字段。
## 11-2 建立SQLiteOpenHelper类别
Android 提供许多方便与简单的数据库API,可以简化应用程式处理数据库的工作。这些API都在“android.database.sqlite”套件,它们可以用来执行数据库的管理和查询的工作。在这个套件中的“SQLiteOpenHelper”类别,可以在应用程式中执行建立数据库与表格的工作,应用程式第一次在装置执行的时候,由它负责建立应用程式需要的数据库与表格,后续执行的时候开启已经建立好的数据库让应用程式使用。还有应用程式在运作一段时间以后,如果增加或修改功能,数据库的表格也增加或修改了,它也可以为应用程式执行数据库的修改工作,让新的应用程式可以正常的运作。
接下来设计建立数据库与表格的类别,在“net.macdidi.myandroidtutorial”套件按鼠标右键,选择“New -> Java CLass”,在Name输入“MyDBHelper”后选择“OK”。参考下列的内容先完成部份的程式码:
~~~
package net.macdidi.myandroidtutorial;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteDatabase.CursorFactory;
import android.database.sqlite.SQLiteOpenHelper;
public class MyDBHelper extends SQLiteOpenHelper {
// 数据库名称
public static final String DATABASE_NAME = "mydata.db";
// 数据库版本,资料结构改变的时候要更改这个数字,通常是加一
public static final int VERSION = 1;
// 数据库物件,固定的字段变量
private static SQLiteDatabase database;
// 建构子,在一般的应用都不需要修改
public MyDBHelper(Context context, String name, CursorFactory factory,
int version) {
super(context, name, factory, version);
}
// 需要数据库的元件呼叫这个方法,这个方法在一般的应用都不需要修改
public static SQLiteDatabase getDatabase(Context context) {
if (database == null || !database.isOpen()) {
database = new MyDBHelper(context, DATABASE_NAME,
null, VERSION).getWritableDatabase();
}
return database;
}
@Override
public void onCreate(SQLiteDatabase db) {
// 建立应用程式需要的表格
// 待会再回来完成它
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// 删除原有的表格
// 待会再回来完成它
// 呼叫onCreate建立新版的表格
onCreate(db);
}
}
~~~
## 11-3 数据库功能类别
在Android应用程式中使用数据库功能通常会有一种状况,就是Activity或其它元件的程式码,会因为加入处理数据库的工作,程式码变得又多、又复杂。一般程式设计的概念,一个元件中的程式码如果很多的话,在撰写或修改的时候,都会比较容易出错。所以这里说明的作法,会采用在一般应用程式中执行数据库工作的设计方式,把执行数据库工作的部份写在一个独立的Java类别中。
接下来设计应用程式需要的数据库功能类别,提供应用程式与数据库相关功能。在“net.macdidi.myandroidtutorial”套件按鼠标右键,选择“New -> Java CLass”,在Name输入“ItemDAO”后选择“OK”。参考下列的内容先完成部份的程式码:
~~~
package net.macdidi.myandroidtutorial;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
// 资料功能类别
public class ItemDAO {
// 表格名称
public static final String TABLE_NAME = "item";
// 编号表格字段名称,固定不变
public static final String KEY_ID = "_id";
// 其它表格字段名称
public static final String DATETIME_COLUMN = "datetime";
public static final String COLOR_COLUMN = "color";
public static final String TITLE_COLUMN = "title";
public static final String CONTENT_COLUMN = "content";
public static final String FILENAME_COLUMN = "filename";
public static final String LATITUDE_COLUMN = "latitude";
public static final String LONGITUDE_COLUMN = "longitude";
public static final String LASTMODIFY_COLUMN = "lastmodify";
// 使用上面宣告的变量建立表格的SQL指令
public static final String CREATE_TABLE =
"CREATE TABLE " + TABLE_NAME + " (" +
KEY_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
DATETIME_COLUMN + " INTEGER NOT NULL, " +
COLOR_COLUMN + " INTEGER NOT NULL, " +
TITLE_COLUMN + " TEXT NOT NULL, " +
CONTENT_COLUMN + " TEXT NOT NULL, " +
FILENAME_COLUMN + " TEXT, " +
LATITUDE_COLUMN + " REAL, " +
LONGITUDE_COLUMN + " REAL, " +
LASTMODIFY_COLUMN + " INTEGER)";
// 数据库物件
private SQLiteDatabase db;
// 建构子,一般的应用都不需要修改
public ItemDAO(Context context) {
db = MyDBHelper.getDatabase(context);
}
// 关闭数据库,一般的应用都不需要修改
public void close() {
db.close();
}
// 新增参数指定的物件
public Item insert(Item item) {
// 建立准备新增资料的ContentValues物件
ContentValues cv = new ContentValues();
// 加入ContentValues物件包装的新增资料
// 第一个参数是字段名称, 第二个参数是字段的资料
cv.put(DATETIME_COLUMN, item.getDatetime());
cv.put(COLOR_COLUMN, item.getColor().parseColor());
cv.put(TITLE_COLUMN, item.getTitle());
cv.put(CONTENT_COLUMN, item.getContent());
cv.put(FILENAME_COLUMN, item.getFileName());
cv.put(LATITUDE_COLUMN, item.getLatitude());
cv.put(LONGITUDE_COLUMN, item.getLongitude());
cv.put(LASTMODIFY_COLUMN, item.getLastModify());
// 新增一笔资料并取得编号
// 第一个参数是表格名称
// 第二个参数是没有指定字段值的默认值
// 第三个参数是包装新增资料的ContentValues物件
long id = db.insert(TABLE_NAME, null, cv);
// 设定编号
item.setId(id);
// 回传结果
return item;
}
// 修改参数指定的物件
public boolean update(Item item) {
// 建立准备修改资料的ContentValues物件
ContentValues cv = new ContentValues();
// 加入ContentValues物件包装的修改资料
// 第一个参数是字段名称, 第二个参数是字段的资料
cv.put(DATETIME_COLUMN, item.getDatetime());
cv.put(COLOR_COLUMN, item.getColor().parseColor());
cv.put(TITLE_COLUMN, item.getTitle());
cv.put(CONTENT_COLUMN, item.getContent());
cv.put(FILENAME_COLUMN, item.getFileName());
cv.put(LATITUDE_COLUMN, item.getLatitude());
cv.put(LONGITUDE_COLUMN, item.getLongitude());
cv.put(LASTMODIFY_COLUMN, item.getLastModify());
// 设定修改资料的条件为编号
// 格式为“字段名称=资料”
String where = KEY_ID + "=" + item.getId();
// 执行修改资料并回传修改的资料数量是否成功
return db.update(TABLE_NAME, cv, where, null) > 0;
}
// 删除参数指定编号的资料
public boolean delete(long id){
// 设定条件为编号,格式为“字段名称=资料”
String where = KEY_ID + "=" + id;
// 删除指定编号资料并回传删除是否成功
return db.delete(TABLE_NAME, where , null) > 0;
}
// 读取所有记事资料
public List getAll() {
List result = new ArrayList<>();
Cursor cursor = db.query(
TABLE_NAME, null, null, null, null, null, null, null);
while (cursor.moveToNext()) {
result.add(getRecord(cursor));
}
cursor.close();
return result;
}
// 取得指定编号的资料物件
public Item get(long id) {
// 准备回传结果用的物件
Item item = null;
// 使用编号为查询条件
String where = KEY_ID + "=" + id;
// 执行查询
Cursor result = db.query(
TABLE_NAME, null, where, null, null, null, null, null);
// 如果有查询结果
if (result.moveToFirst()) {
// 读取包装一笔资料的物件
item = getRecord(result);
}
// 关闭Cursor物件
result.close();
// 回传结果
return item;
}
// 把Cursor目前的资料包装为物件
public Item getRecord(Cursor cursor) {
// 准备回传结果用的物件
Item result = new Item();
result.setId(cursor.getLong(0));
result.setDatetime(cursor.getLong(1));
result.setColor(ItemActivity.getColors(cursor.getInt(2)));
result.setTitle(cursor.getString(3));
result.setContent(cursor.getString(4));
result.setFileName(cursor.getString(5));
result.setLatitude(cursor.getDouble(6));
result.setLongitude(cursor.getDouble(7));
result.setLastModify(cursor.getLong(8));
// 回传结果
return result;
}
// 取得资料数量
public int getCount() {
int result = 0;
Cursor cursor = db.rawQuery("SELECT COUNT(*) FROM " + TABLE_NAME, null);
if (cursor.moveToNext()) {
result = cursor.getInt(0);
}
return result;
}
// 建立范例资料
public void sample() {
Item item = new Item(0, new Date().getTime(), Colors.RED, "关于Android Tutorial的事情.", "Hello content", "", 0, 0, 0);
Item item2 = new Item(0, new Date().getTime(), Colors.BLUE, "一只非常可爱的小狗狗!", "她的名字叫“大热狗”,又叫\n作“奶嘴”,是一只非常可爱\n的小狗。", "", 25.04719, 121.516981, 0);
Item item3 = new Item(0, new Date().getTime(), Colors.GREEN, "一首非常好听的音乐!", "Hello content", "", 0, 0, 0);
Item item4 = new Item(0, new Date().getTime(), Colors.ORANGE, "储存在数据库的资料", "Hello content", "", 0, 0, 0);
insert(item);
insert(item2);
insert(item3);
insert(item4);
}
}
~~~
完成数据库功能类别以后,里面也宣告了一些SQLiteOpenHelper类别会使用到的资料,开启“MyDBHelper”类别,完成之前还没有完成的工作:
~~~
@Override
public void onCreate(SQLiteDatabase db) {
// 建立应用程式需要的表格
db.execSQL(ItemDAO.CREATE_TABLE);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// 删除原有的表格
db.execSQL("DROP TABLE IF EXISTS " + ItemDAO.TABLE_NAME);
// 呼叫onCreate建立新版的表格
onCreate(db);
}
~~~
## 11-4 使用数据库中的记事资料
完成与数据库相关的类别以后,其它的部份就简单多了,Activity元件也可以保持比较简洁的程式架构。开启在“net.macdidi.myandroidtutorial”套件下的“MainActivity”类别,修改原来自己建立资料的作法,改由数据库提供记事资料并显示在画面。由于所有执行数据库工作的程式码都写在“ItemDAO”类别,所以要宣告一个ItemDAO的字段变量,“onCreate”方法也要执行相关的修改:
~~~
// 宣告数据库功能类别字段变量
private ItemDAO itemDAO;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
processViews();
processControllers();
// 建立数据库物件
itemDAO = new ItemDAO(getApplicationContext());
// 如果数据库是空的,就建立一些范例资料
// 这是为了方便测试用的,完成应用程式以后可以拿掉
if (itemDAO.getCount() == 0) {
itemDAO.sample();
}
// 取得所有记事资料
items = itemDAO.getAll();
itemAdapter = new ItemAdapter(this, R.layout.single_item, items);
item_list.setAdapter(itemAdapter);
}
~~~
完成这个部份的修改以后,执行应用程式,如果画面上显示像这样的画面,数据库的部份应该就没有问题了。
[![AndroidTutorial5_03_03_01](http://www.codedata.com.tw/wp-content/uploads/2015/02/AndroidTutorial5_03_03_01-190x300.png)](http://www.codedata.com.tw/wp-content/uploads/2015/02/AndroidTutorial5_03_03_01.png)
接下来需要处理新增与修改的部份,同样在“MainActivity”类别,找到“onActivityResult”方法,参考下列的内容修改程式码:
~~~
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode == Activity.RESULT_OK) {
Item item = (Item) data.getExtras().getSerializable(
"net.macdidi.myandroidtutorial.Item");
if (requestCode == 0) {
// 新增记事资料到数据库
item = itemDAO.insert(item);
items.add(item);
itemAdapter.notifyDataSetChanged();
}
else if (requestCode == 1) {
int position = data.getIntExtra("position", -1);
if (position != -1) {
// 修改数据库中的记事资料
itemDAO.update(item);
items.set(position, item);
itemAdapter.notifyDataSetChanged();
}
}
}
}
~~~
最后是删除记事资料的部份,同样在“MainActivity”类别,找到“clickMenuItem”方法,参考下列的内容修改程式码:
~~~
public void clickMenuItem(MenuItem item) {
int itemId = item.getItemId();
switch (itemId) {
...
case R.id.delete_item:
if (selectedCount == 0) {
break;
}
AlertDialog.Builder d = new AlertDialog.Builder(this);
String message = getString(R.string.delete_item);
d.setTitle(R.string.delete)
.setMessage(String.format(message, selectedCount));
d.setPositiveButton(android.R.string.yes,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// 取得最后一个元素的编号
int index = itemAdapter.getCount() - 1;
while (index > -1) {
Item item = itemAdapter.get(index);
if (item.isSelected()) {
itemAdapter.remove(item);
// 删除数据库中的记事资料
itemDAO.delete(item.getId());
}
index--;
}
itemAdapter.notifyDataSetChanged();
}
});
d.setNegativeButton(android.R.string.no, null);
d.show();
break;
case R.id.googleplus_item:
break;
case R.id.facebook_item:
break;
}
}
~~~
完成这一章所有的工作了,执行应用程式,试试看新增、修改和删除记事资料的功能。因为记事资料都保存在数据库,完成测试以后,关闭应用程式再重新启动,记事资料还是会显示在画面。
- 第一堂
- 第一堂(1)西游记里的那只猴子
- 第一堂(2)准备 Android Studio 开发环境
- 第一堂(3)开始设计 Android 应用程式
- 第一堂(4)开发 Android 应用程式的准备工作
- 第二堂
- 第二堂(1)规划与建立应用程式需要的资源
- 第二堂(2)设计应用程式使用者界面
- 第二堂(3)应用程式与使用者的互动
- 第二堂(4)建立与使用 Activity 元件
- 第三堂
- 第三堂(1)为ListView元件建立自定画面
- 第三堂(2)储存与读取应用程式资讯
- 第三堂(3)Android 内建的 SQLite 数据库
- 第四堂
- 第四堂(1)使用照相机与麦克风
- 第四堂(2)设计地图应用程式 - Google Maps Android API v2
- 第四堂(3)读取装置目前的位置 - Google Services Location
- 第五堂
- 第五堂(1)建立广播接收元件 - BroadcastReceiver
- 第五堂(2)系统通知服务 - Notification
- 第五堂(3)设计小工具元件 - AppWidget
- 第六堂
- 第六堂(1)Material Design - Theme与Transition
- 第六堂(2)Material Design - RecylerView
- 第六堂(3)Material Design - Shared Element与自定动画效果