SQLite本地數據庫的應用

說明
咱們知道savedInstanceState、文件與SharedPreference都可以保存數據,但他們都沒法知足應用持久化保存數據的需求,Android爲此提供了長期存儲地:即SQLite數據庫。html

概述

SQLite是一個輕量級的關係型數據庫,運算速度快,佔用資源少,很適合在移動設備上使用, 不只支持標準SQL語法,還遵循ACID(數據庫事務)原則,無需帳號,使用起來很是方便!java

SQLite是相似於MySQL和Postgresql的開源關係型數據庫。不一樣於其餘數據庫的是, SQLite使用單個文件存儲數據,使用SQLite庫讀取數據。android

小結下特色:web

SQlite經過文件保存數據庫,一個文件就是一個數據庫,數據庫中又包含多個表格,表格裏又有 多條記錄,每一個記錄由多個字段構成,每一個字段有對應的值,每一個值咱們能夠指定類型,也能夠不指定 類型(主鍵除外)sql

關於SQLite數據庫支持存儲的數據類型及相關的基本操做語句能夠移步到android中的數據庫操做或者SQLite在線文檔數據庫

Android標準庫包含SQLite庫以及配套的一些Java輔助類。數組

使用SQLite本地數據庫

Step 1 : 定義Schema

咱們以上一篇RecyclerView的基本用法爲例,將每個View對象中的內容存入數據庫。安全

建立數據庫前,首先要清楚存儲什麼樣的數據。 咱們要保存的是一條條Info信息
記錄,這須要定義如圖所示的infos數據表。app

這裏寫圖片描述

SQL中一個重要的概念是schema:一種DB結構的正式聲明,用於表示database的組成結構。schema是從建立DB的SQL語句中生成的。咱們會發現建立一個伴隨類(companion class)是頗有益的,這個類稱爲合約類(contract class),它用一種系統化而且自動生成文檔的方式,顯示指定了schema樣式。ide

Contract Clsss是一些常量的容器。它定義了例如URIs表名列名等。這個contract類容許在同一個包下與其餘類使用一樣的常量。 它讓咱們只須要在一個地方修改列名,而後這個列名就能夠自動傳遞給整個code

組織contract類的一個好方法是在類的根層級定義一些全局變量,而後爲每個table來建立內部類

首先,咱們來建立定義schema的Java類。建立時,新建一個databas,在包下新建類命名爲InfoDbSchema,這樣,就能夠將InfoDbSchema.java文件放入專門的database包中,實現數據庫操做相關代碼的組織和歸類。

在InfoDbSchema類中,再定義一個描述數據表的InfoTable內部類:

public class InfoDbScheme {
    public static final class InfoTable{
        public static final String NAME = "infos";
    }
}

InfoTable內部類惟一的用途就是定義描述數據表元素的String常量。首先要定義的是數據庫表名(InfoTable.NAME)

接下來定義數據表字段:

public class InfoDbScheme {
    public static final class InfoTable{
        public static final String NAME = "infos";

        public static final class Col{
            public static final String UUID = "uuid";
            public static final String TITLE = "title";
            public static final String DATE = "date";
        }
    }
}

有了這些數據表元素,就能夠在Java代碼中安全地引用了。例如, InfoTable.Cols.TITLE就是指Info記錄的title字段。此外,這種定義方式還給修改字段名稱或新增表元素帶來了方便。

step 2 : 使用SQL Helper建立初始數據庫

定 義 完 數 據 庫 schema , 就 可 以 創 建 數 據 庫 了 。 openOrCreateDatabase(…) 和databaseList()方法是Android提供的Context底層方法,能夠用來打開數據庫文件並將其轉換爲SQLiteDatabase實例。

不過,實際開發時,建議老是遵循如下步驟。

  1. 確認目標數據庫是否存在。

  2. 若是不存在,首先建立數據庫,而後建立數據庫表以及必需的初始化數據。

  3. 若是存在,打開並確認InfoDbSchema是不是最新版本。

  4. 若是是舊版本,就運行相關代碼升級到最新版本。

    使人高興的是, Android提供的SQLiteOpenHelper類能夠幫咱們處理這些。在數據庫包中建立InfoBaseHelper類(InfoBaseHelper.java):

public class InfoBaseHelper extends SQLiteOpenHelper {
private static final int VERSION = 1;
private static final String DATABASE_NAME = "infoBase.db";
public InfoBaseHelper(Context context) {
super(context, DATABASE_NAME, null, VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
}
}

有了SQLiteOpenHelper類,打開SQLiteDatabase的繁雜工做均可以交給它處理。在InfoLab中用它建立infos數據庫(InfoLab.java):

public class InfoLab {
    private static InfoLab sInfoLab;
    private Context mAppContext;
    private ArrayList<Info> mInfos;
    private SQLiteDatabase mDateBase;

    private InfoLab(Context appContext){
        mAppContext = appContext.getApplicationContext();
        mDateBase = new InfoBaseHelper(mAppContext).getWritableDatabase();
        mInfos = new ArrayList<Info>();

         /* for(int i = 0;i<100;i++){ Info info = new Info(); info.setmTtitle("Info #"+i); mInfos.add(info); }*/
    }
    ...
}

這裏調用getWritableDatabase()方法時, CrimeBaseHelper要作以下工做。

  1. 打開/data/data/com.example.sqlitetest2/databases/crimeBase.db數據庫;若是不存在,就先建立crimeBase.db數據庫文件。

  2. 若是是首次建立數據庫,就調用onCreate(SQLiteDatabase)方法,而後保存最新的版本號。

  3. 若是已建立過數據庫,首先檢查它的版本號。若是InfoOpenHelper中的版本號更高,就調用onUpgrade(SQLiteDatabase, int, int)方法升級。

最後,再作個總結: onCreate(SQLiteDatabase)方法負責建立初始數據庫; onUpgrade(SQLiteDatabase, int, int)方法負責與升級相關的工做。

我 們 在onCreate(…)方法中建立數據庫表,這須要導入InfoDbSchema類的InfoTable內部類。(InfoBaseHelper.java

public void onCreate(SQLiteDatabase db) {

        db.execSQL("create table " + InfoTable.NAME + "(" +
                " _id integer primary key autoincrement, " +
                InfoTable.Col.UUID + ", " +
                InfoTable.Col.TITLE + ", " +
                InfoTable.Col.DATE  +
                ")"
        );
    }

如今咱們就在手機本地文件中建立了一個本地數據庫,數據庫名字叫作infoBase.db,在數據庫中還建立了一個數據庫表,表的名字叫作infos,在表中咱們還建立了幾個字段,uuid、title還有date。

咱們能夠在手機目錄/data/data/[your package name]下查看(前提是手機要root),你就能夠看到下圖這樣的文件。

這裏寫圖片描述

這裏寫圖片描述

這裏寫圖片描述

這裏寫圖片描述

固然如今info表裏咱們尚未添加數據。

step 3 : 寫入數據庫

要使用SQLiteDatabase,數據庫中首先要有數據。數據庫寫入操做有:向infos表中插入新記錄以及在Info變動時更新原始記錄。

咱們修改InfoLab類,不用List來存儲數據,改用mDateBase來存儲數據,首先要刪除掉InfoLab類中的ArrayList<Info>代碼,增長一個添加數據的方法及更新數據的方法,改動完成以下:(InfoLab.java

public class InfoLab {
    private static InfoLab sInfoLab;
    private Context mAppContext;
   // private ArrayList<Info> mInfos;
    private SQLiteDatabase mDateBase;

    private InfoLab(Context appContext){
        mAppContext = appContext.getApplicationContext();
        mDateBase = new InfoBaseHelper(mAppContext).getWritableDatabase();
       // mInfos = new ArrayList<Info>();

         /* for(int i = 0;i<100;i++){ Info info = new Info(); info.setmTtitle("Info #"+i); mInfos.add(info); }*/
    }

    public static InfoLab get(Context c){
        if(sInfoLab==null){
            sInfoLab = new InfoLab(c.getApplicationContext());
        }
        return sInfoLab;
    }

    public ArrayList<Info> getInfos(){
      // return mInfos;
        return new ArrayList<>();
    }

    public Info getInfo(UUID uuid){
// for(Info i:mInfos){
// if(i.getmId().equals(uuid)){
// return i;
// }
// }
        return null;
    }

    public void addInfo(Info info){
      // mInfos.add(info);

    }

 public void updateInfo(Info info){

    }
}

在InfoListFragment.java類中增長添加數據的按鈕,修改info_list_activity.xml的代碼以下:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" tools:context=".InfoListActivity">

    <android.support.v7.widget.RecyclerView  xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/info_recycler_view" android:layout_width="match_parent" android:layout_height="wrap_content"/>

    <LinearLayout  android:id="@+id/empty_crime_list" android:layout_width="wrap_content" android:layout_height="123dp" android:orientation="vertical" android:layout_gravity="center">

        <TextView  android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:padding="16dp" android:text="沒有Info記錄能夠顯示"/>

        <Button  android:id="@+id/add_crime_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:padding="16dp" android:text="@string/new_crime"/>
    </LinearLayout>
</LinearLayout>

這裏寫圖片描述

修改InfoListActivity.java代碼以下:

public class InfoListActivity extends AppCompatActivity {
    ...
    private LinearLayout mLinearLayout;
    private Button addButton;
    ...
     protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.info_list_activity);

        mLinearLayout = (LinearLayout)this.findViewById(R.id.empty_crime_list);
        addButton = (Button)this.findViewById(R.id.add_crime_button);

        mInfoRecyclerView = (RecyclerView)this.findViewById(R.id.info_recycler_view);
        mInfoRecyclerView.setLayoutManager(new LinearLayoutManager(this));
        updateUI();
    } 
    ...
     private void updateUI() {
        InfoLab infoLab = InfoLab.get(this);
        List<Info> infos = infoLab.getInfos();

        if(mAdapter==null){
            mAdapter = new InfoAdapter(infos);
            mInfoRecyclerView.setAdapter(mAdapter);
        }
        else{
            mAdapter.notifyDataSetChanged();
        }
        if(infos.size()>0){
            mLinearLayout.setVisibility(View.GONE);
        }
        else{
            mLinearLayout.setVisibility(View.VISIBLE);
            addButton.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    Log.d("hehe","hehe");
                   Info info = new Info();
                    InfoLab.get(InfoListActivity.this).addInfo(info);
                    Intent intent = new Intent(InfoListActivity.this,InfoDetailActivity.class);
                    intent.putExtra(EXTRA_INFO_ID,info.getmId());
                    startActivity(intent);
                }
            });
        }
           mInfoRecyclerView.addItemDecoration(new DividerItemDecoration(this,DividerItemDecoration.VERTICAL_LIST));
    }   

}

如今咱們點擊按鈕,就會加載InfoDetailActivity.java頁面。

接下來開始往數據庫中寫入數據:

使用 ContentValues

負責處理數據庫寫入和更新操做的輔助類是ContentValues。它是個鍵值存儲類,相似於Java的HashMap和前面用過的Bundle。不一樣的是, ContentValues只能用於處理SQLite數據。

step 4 : 建立ContentValues( InfoLab.java )

public class InfoLab {
    ...
     public static ContentValues getContentValues(Info info){
        ContentValues values = new ContentValues();
        values.put(InfoTable.Col.UUID,info.getmId().toString());
        values.put(InfoTable.Col.TITLE,info.getmTtitle());
        values.put(InfoTable.Col.DATE,info.getmDate().toString());
        return values;
    }
}

step 5 : 插入和更新記錄( InfoLab.java )

public void addInfo(Info info){
      // mInfos.add(info);
        ContentValues values = getContentValues(info);
        mDateBase.insert(InfoTable.NAME,null,values);
    }

insert(String, String, ContentValues)方法有兩個重要的參數,還有一個不多用到。
傳入的第一個參數是數據庫表名,最後一個是要寫入的數據。
第二個參數稱爲nullColumnHack。它有什麼用途呢?
別急,舉個例子你就明白了。假設你想調用insert(…)方法,但傳入了ContentValues
空值。這時, SQLite不幹了, insert(…)方法調用只能以失敗了結。
然而,若是能以uuid值做爲nullColumnHack傳入的話, SQLite就能夠忽略ContentValues空值,並且還會自動傳入一個帶uuid且值爲null的ContentValues。結果, insert(…)方法得以成功調用並插入了一條新記錄

public void updateInfo(Info info){
        String uuidString = info.getmId().toString();
        ContentValues values = getContentValues(info);
        mDateBase.update(InfoTable.NAME, values,
                InfoTable.Col.UUID + " = ?",
                new String[] { uuidString });

    }

update(String, ContentValues, String, String[])更新方法相似於insert(…)方法,向其傳入要更新的數據表名和爲表記錄準備的ContentValues。然而,與insert(…)方法不一樣的是,你要肯定該更新哪些記錄。具體的作法是:建立where子句(第三個參數) ,而後指定where子句中的參數值(String[]數組參數)。
問題來了,爲何不直接在where子句中放入uuidString呢?這可比使用?而後傳入String[]簡單多了!
事實上,不少時候, String自己會包含SQL代碼。若是將它直接放入query語句中,這些代碼
可能會改變query語句的含義,甚至會修改數據庫資料。這實際就是SQL腳本注入, 危害至關嚴重。
使用?的話,就不用關心String包含什麼,代碼執行的效果確定就是咱們想要的。

step 6 : Info數據刷新( InfoDetailActivity.java )

public class InfoDetailActivity extends AppCompatActivity {
    ...
    public void onCreate(Bundle savedInstanceState) {
    ...
    }

    @Override
    protected void onResume() {
        super.onResume();
        InfoLab.get(this).updateInfo(mInfo);
    }
}

這樣,點擊按鈕,你就能夠往裏面插入數據了,由於尚未完成會致使閃退,可是數據庫中已經成功的添加了一條數據,打開數據庫目錄能夠看到:

這裏寫圖片描述

step 7 : 讀取數據庫

讀取SQLite數據庫中數據須要用到query(…)方法。這個方法有好幾個重載版本。咱們要用的版本以下:

public Cursor query(
String table,
String[] columns,
String where,
String[] whereArgs,
String groupBy,
String having,
String orderBy,
String limit)

參數table是要查詢的數據表。參數columns指定要依次獲取哪些字段的值。參數where和whereArgs的做用與update(…)方法中的同樣。
新增一個便利方法調用query(…)方法查詢InfoeTable中的記錄( InfoLab.java )

private Cursor queryCrimes(String whereClause, String[] whereArgs) {
Cursor cursor = mDatabase.query(
InfoTable.NAME,
null, // Columns - null selects all columns
whereClause,
whereArgs,
null, // groupBy
null, // having
null // orderBy
);
return cursor;
}

step 8 : 使用 CursorWrapper

Cursor是個神奇的表數據處理工具,其任務就是封裝數據表中的原始字段值。

建立InfoCursorWrapper類(InfoCursorWrapper.java

public class InfoCursorWrapper extends CursorWrapper {
    /** * Creates a cursor wrapper. * * @param cursor The underlying cursor to wrap. */
    public InfoCursorWrapper(Cursor cursor) {
        super(cursor);
    }

    ...
}

新增getCrime()方法(InfoCursorWrapper.java

public class InfoCursorWrapper extends CursorWrapper {
    /** * Creates a cursor wrapper. * * @param cursor The underlying cursor to wrap. */
    public InfoCursorWrapper(Cursor cursor) {
        super(cursor);
    }

    public Info getInfo() {
        String uuidString = getString(getColumnIndex(InfoTable.Col.UUID));
        String title = getString(getColumnIndex(InfoTable.Col.TITLE));
        long date = getLong(getColumnIndex(InfoTable.Col.DATE));

        Info info = new Info(UUID.fromString(uuidString));
        info.setmTtitle(title);
        info.setmDate(new Date(date));
        return info;
    }
}

step 9 : 使用cursor封裝方法(InfoLab.java)

private InfoCursorWrapper queryInfo(String whereClaues,String[] whereArgs){
        Cursor cursor = mDateBase.query(
                InfoTable.NAME,
                null, // Columns - null selects all columns
                whereClaues,
                whereArgs,
                null, // groupBy
                null, // having
                null // orderBy
                 );

        return new InfoCursorWrapper(cursor);
    }

step 10 :返回info列表(InfoLab.java)

public ArrayList<Info> getInfos(){
      // return mInfos;
        //return new ArrayList<>();

        ArrayList<Info> infos = new ArrayList<>();
       InfoCursorWrapper cursor = queryInfo(null, null);
        try {
            cursor.moveToFirst();
            while (!cursor.isAfterLast()) {
                infos.add(cursor.getInfo());
                cursor.moveToNext();
            }
        } finally {
            cursor.close();
        }
        return infos;
    }

要從cursor中取出數據,首先要調用moveToFirst()方法移動cursor指向第一個元素。讀取行記錄後,再調用moveToNext()方法,讀取下一行記錄,直到isAfterLast()告訴咱們沒有數據可取爲止。

最後,別忘了調用Cursor的close()方法關閉它。

step11 :重寫getInfo(UUID)方法(InfoLab.java)

public Info getInfo(UUID uuid){
// for(Info i:mInfos){
// if(i.getmId().equals(uuid)){
// return i;
// }
// }
       // return null;
        InfoCursorWrapper cursor = queryInfo(
                InfoTable.Col.UUID + " = ?",
                new String[] { uuid.toString() }
        );
        try {
            if (cursor.getCount() == 0) {
                return null;
            }
            cursor.moveToFirst();
            return cursor.getInfo();
        } finally {
            cursor.close();
        }
    }

上述代碼的做用以下。

如今能夠插入info記錄了。也就是說,點擊New Crime菜單項,實現將info添加到InfoLab的代碼能夠正常工做了。

數據庫查詢沒有問題了。 InfoDetailActivity如今可以看見InfoLab中的全部Info了。

InfoLab.getInfo(UUID) 方 法 也 能 正 常 工 做 了 。 InfoDetailActivity 終於能夠顯示真正的Info對象了。

step 12 : 刷新模型層數據

添加setInfos(List<Info>)方法(InfoListActivity.java):

private class InfoAdapter extends RecyclerView.Adapter<InfoHolder> {
...

@Override
public int getItemCount() {
    return mInfos.size();
}

 public void setInfos(List<Info> infos){
            mInfos = infos;
        }

}

而後在updateUI()方法中調用setInfos(List<Info> infos)方法(InfoListActivity.java)

private void updateUI() {
        InfoLab infoLab = InfoLab.get(this);
        List<Info> infos = infoLab.getInfos();

        if(mAdapter==null){
            mAdapter = new InfoAdapter(infos);
            mInfoRecyclerView.setAdapter(mAdapter);
        }
        else{
            mAdapter.setInfos(infos);
            mAdapter.notifyDataSetChanged();
        }
       ...
    }

如今,能夠驗證咱們的成果了。運行應用,新增一項info記錄,而後按回退鍵,
確認InfoListActivity中會出現剛纔新增的記錄。數據庫中也添加了info記錄。

源碼在這裏