說明
咱們知道savedInstanceState、文件與SharedPreference都可以保存數據,但他們都沒法知足應用持久化保存數據的需求,Android爲此提供了長期存儲地:即SQLite數據庫。html
SQLite是一個輕量級的關係型數據庫,運算速度快,佔用資源少,很適合在移動設備上使用, 不只支持標準SQL語法,還遵循ACID(數據庫事務)原則,無需帳號,使用起來很是方便!java
SQLite是相似於MySQL和Postgresql的開源關係型數據庫。不一樣於其餘數據庫的是, SQLite使用單個文件存儲數據,使用SQLite庫讀取數據。android
小結下特色:web
SQlite經過文件來保存數據庫,一個文件就是一個數據庫,數據庫中又包含多個表格,表格裏又有 多條記錄,每一個記錄由多個字段構成,每一個字段有對應的值,每一個值咱們能夠指定類型,也能夠不指定 類型(主鍵除外)sql
關於SQLite數據庫支持存儲的數據類型及相關的基本操做語句能夠移步到android中的數據庫操做或者SQLite在線文檔。數據庫
Android標準庫包含SQLite庫以及配套的一些Java輔助類。數組
咱們以上一篇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字段。此外,這種定義方式還給修改字段名稱或新增表元素帶來了方便。
定 義 完 數 據 庫 schema , 就 可 以 創 建 數 據 庫 了 。 openOrCreateDatabase(…) 和databaseList()方法是Android提供的Context底層方法,能夠用來打開數據庫文件並將其轉換爲SQLiteDatabase實例。
不過,實際開發時,建議老是遵循如下步驟。
確認目標數據庫是否存在。
若是不存在,首先建立數據庫,而後建立數據庫表以及必需的初始化數據。
若是存在,打開並確認InfoDbSchema是不是最新版本。
若是是舊版本,就運行相關代碼升級到最新版本。
使人高興的是, 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要作以下工做。
打開/data/data/com.example.sqlitetest2/databases/crimeBase.db數據庫;若是不存在,就先建立crimeBase.db數據庫文件。
若是是首次建立數據庫,就調用onCreate(SQLiteDatabase)方法,而後保存最新的版本號。
若是已建立過數據庫,首先檢查它的版本號。若是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表裏咱們尚未添加數據。
要使用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數據。
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;
}
}
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包含什麼,代碼執行的效果確定就是咱們想要的。
public class InfoDetailActivity extends AppCompatActivity {
...
public void onCreate(Bundle savedInstanceState) {
...
}
@Override
protected void onResume() {
super.onResume();
InfoLab.get(this).updateInfo(mInfo);
}
}
這樣,點擊按鈕,你就能夠往裏面插入數據了,由於尚未完成會致使閃退,可是數據庫中已經成功的添加了一條數據,打開數據庫目錄能夠看到:
讀取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;
}
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;
}
}
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);
}
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()方法關閉它。
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對象了。
添加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記錄。
源碼在這裏。