Android Tutorial 第三堂(3)Android 內建的 SQLite 資料庫 by Michael | CodeData
top

Android Tutorial 第三堂(3)Android 內建的 SQLite 資料庫

分享:

專欄作者新書出版:Android App程式開發剖析 第三版(適用Android 8 Oreo與Android Studio 3)

Android Tutorial 第三堂(2)儲存與讀取應用程式資訊 << 前情

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<Item> getAll() {
        List<Item> 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

接下來需要處理新增與修改的部份,同樣在「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;
    }       
}

完成這一章所有的工作了,執行應用程式,試試看新增、修改和刪除記事資料的功能。因為記事資料都保存在資料庫,完成測試以後,關閉應用程式再重新啟動,記事資料還是會顯示在畫面。

課程相關的檔案都可以GitHub瀏覽與下載。
https://github.com/macdidi5/AndroidTutorial

後續 >> Android Tutorial 第四堂(1)使用照相機與麥克風

分享:
按讚!加入 CodeData Facebook 粉絲群

相關文章

留言

留言請先。還沒帳號註冊也可以使用FacebookGoogle+登錄留言

x8203000501/31

您好:)
setId在我的這邊是顯示"Cannot resolve method 'setId(long)' "
請問您是有在什麼地方定義嗎?
謝謝

LaurenceLiu07/07

作者您好!
我是初階的開發者,看過您的文章後真的獲益良多,謝謝!

關於SQLite的部分想向您請教,若是在一個ListView要同時讀取兩張資料表的話,Adapter的部分應該要怎麼實作呢?謝謝!!

GrassEatFlower07/09

您好
請問如果要自己額外做一個頁面,將color欄位有相同的值(即是所有相同顏色)的資料都抓出來顯示應該要怎麼實做呢?
謝謝!!

kaiyuen016309/03

您好
我參考您的範例寫了一個程式也成功轉成APK,亦可載入手機,但載入之後 SQLITE 資庫似乎無法與APK一起載入到手機
我亦參考網路上範例要我先判斷 data\dada\xxxxx\base 之下是否有sqlite資料庫,如無再利用以下指令
來copy sqlite資料庫 "InputStream is = getBaseContext().getAssets().open(DB_NAME);"
但我一直無法查到上面指另所指路徑位置
謝謝

kaiyuen016309/18

imageview scaletype matrix 與 fitxy 無法並用
如何讓開出 bitmay 先填滿畫面後並能使用 matrix 來縮放畫面

chiurc11/14

老師你好, 先多謝你一直的教導, 我從第一堂走到現在, 一直也沒多大問題, 直到這一堂, 不知為什麼, 除了sample()沒問題, 用其他 item = itemDAO.insert(item), itemDAO.update(item)和 itemDAO.delete(item.getId()) 時都會奇怪地 raise NullpointerException, 請問是否我那裡出錯了 ?

熱門論壇文章

熱門技術文章