第三堂(3)Android 内建的 SQLite 数据库

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, "一只非常可爱的小狗狗!", "她的名字叫“大热狗”,又叫
作“奶嘴”,是一只非常可爱
的小狗。", "", 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

接下来需要处理新增与修改的部份,同样在“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;
    }       
}

完成这一章所有的工作了,执行应用程式,试试看新增、修改和删除记事资料的功能。因为记事资料都保存在数据库,完成测试以后,关闭应用程式再重新启动,记事资料还是会显示在画面。

文章导航