ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
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; } } ~~~ 完成这一章所有的工作了,执行应用程式,试试看新增、修改和删除记事资料的功能。因为记事资料都保存在数据库,完成测试以后,关闭应用程式再重新启动,记事资料还是会显示在画面。