第一行程式碼 第6章 資料儲存全方案——詳解持久化技術
整體大綱:
一、 檔案儲存
1.1 將資料儲存到檔案
在onDestroy()方法中儲存文字資料,先通過openFileOutput(String filename, int mode)方法返回得到FileOutputStream物件,得到這個物件之後再用Java流的方式將資料寫入檔案。以下為示例程式碼:
public void save(String inputText) { FileOutputStream out = null; BufferedWriter writer = null; try { out = openFileOutput("data", Context.MODE_PRIVATE); writer= new BufferedWriter(new OutputStreamWriter(out)); writer.write(inputText); } catch (IOException e) { e.printStackTrace(); } finally { try { if (writer != null) { writer.close(); } } catch (IOException e) { e.printStackTrace(); } } }
1.2 從檔案中讀取資料
在onCreate()方法中恢復文字資料,先通過openFileInput(String filename)方法返回得到FileInputStream物件,得到這個物件之後再用Java流的方式將資料從檔案中讀取出來。以下為示例程式碼:
public String load() { FileInputStream in = null; BufferedReader reader = null; StringBuilder content = new StringBuilder(); try { in = openFileInput("data"); reader= new BufferedReader(new InputStreamReader(in)); String line = ""; while ((line = reader.readLine()) != null) { content.append(line); } } catch (IOException e) { e.printStackTrace(); } finally { if (reader != null) { try { reader.close(); } catch (IOException e) { e.printStackTrace(); } } } return content.toString(); }
在onCreate()方法中呼叫load()方法來讀取檔案中儲存的文字內容,如果讀到的內容不為null,就將內容填充到EditText裡,並呼叫setSelection()方法將輸入游標移動到文字的末尾位置以便於繼續輸入。
P.S.對字串進行非空判斷的時候使用了TextUtils.isEmpty()方法(當傳入的字串等於null或者等於空字串的時候都會返回true)。
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mEditText = findViewById(R.id.edit); String inputText = load(); if (!TextUtils.isEmpty(inputText)) { mEditText.setText(inputText); mEditText.setSelection(inputText.length()); Toast.makeText(this, "Restoring succeeded", Toast.LENGTH_SHORT).show(); } }
二、 SharedPreferences儲存
SharedPreferences檔案是使用XML格式來對資料進行管理的。
2.1 將資料儲存到SharedPreferences中
(1) 獲取SharedPreferences物件
a) Context類中的getSharedPreferences(String filename, int mode)方法,操作模式只能選MODE_PRIVATE(值為0)
b) Activity類中的getPreferences()方法
(2) 呼叫SharedPreferences物件的edit()方法來獲取一個SharedPreferences.Editor物件。
(3) 向SharedPreferences.Editor物件中新增資料,通過putBoolean()方法、putString()方法等等。
(4) 呼叫apply()方法將新增的資料提交,從而完成資料儲存操作。
以下為示例程式碼:
SharedPreferences.Editor editor = getSharedPreferences("data", MODE_PRIVATE).edit(); //(1).a)Context方法、(2) editor.putString("name", "HELLO"); // (3) editor.putInt("age", 28); editor.putBoolean("married", false); editor.apply(); //(4)
2.2 從SharedPreferences中讀取資料
SharedPreferences物件中提供了一系列的get(String key, String defValue)方法,如getBoolean()方法、getString()方法等等。
很多應用程式的偏好設定功能其實都用到了SharedPreferences技術。
以下為示例程式碼:
SharedPreferences pref = getSharedPreferences("data", MODE_PRIVATE); String name = pref.getString("name", ""); int age = pref.getInt("age", 0); boolean married = pref.getBoolean("married", false);
三、 SQLite資料庫儲存
SQLite是一款輕量級的關係型資料庫,它的運算速度非常快,佔用資源很少,通常只需要幾百KB的記憶體就足夠了。SQLite不僅支援標準的SQL語法,還遵循了資料庫的ACID事務。
SQLiteOpenHelper幫助類可以對資料庫進行建立和升級。這是一個抽象類,裡面有兩個抽象方法,分別是onCreate()和onUpgrade()。我們需要建立一個自己的幫助類去繼承它並在裡面重寫這兩個方法,然後分別在這兩個方法中去實現建立、升級資料庫的邏輯,一般在重寫的onCreate()方法中處理一些建立表的邏輯。
SQLiteOpenHelper中還有兩個非常重要的例項方法:getReadableDatabase()和getWritableDatabase(),當資料庫不可寫入的時候兩者有區別。
SQLiteOpenHelper中重寫構造方法。
SQLite資料庫的資料型別很簡單,integer表示整型,real表示浮點型,text表示文字型別,blob表示二進位制型別。primary key表示主鍵,autoincrement表示自增長。
以下為自己的幫助類程式碼:
public class MyDatabaseHelper extends SQLiteOpenHelper { public static final String CREATE_BOOK = "create table Book (" + "id integer primary key autoincrement, " + "author text, " + "price real, " + "pages integer, " + "name text)"; public static final String CREATE_CATEGORY = "create table Category (" + "id integer primary key autoincrement, " + "category_name text, " + "category_code integer)"; private Context mContext; public MyDatabaseHelper(@Nullable Context context, @Nullable String name, @Nullable SQLiteDatabase.CursorFactory factory, int version) { super(context, name, factory, version); mContext = context; } @Override public void onCreate(SQLiteDatabase db) { db.execSQL(CREATE_BOOK); db.execSQL(CREATE_CATEGORY); Toast.makeText(mContext, "Create succeeded", Toast.LENGTH_SHORT).show(); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { db.execSQL("drop table if exists Book"); db.execSQL("drop table if exists Category"); onCreate(db); } }
3.1 建立資料庫
構建出SQLiteOpenHelper的例項之後,再呼叫它的getWritableDatabase()或getReadableDatabase()方法就能夠建立資料庫了,以下為示例程式碼:
dbHelper = new MyDatabaseHelper(this, "BookStore.db", null, 1); dbHelper.getWritableDatabase();
3.2 升級資料庫(以再建一張表為例)
在重寫的onCreate()方法中新增db.execSQL()建表執行語句,接著在重寫的onUpgrade()方法中執行兩條DROP語句,如果資料庫中表已存在,就刪除這些表再呼叫onCreate()方法重新建立,但這樣會造成資料丟失。最後,修改構造方法中的第四個引數(代表當前資料庫的版本號)+1,則onUpgrade()方法可以得到執行,以下為示例程式碼:
dbHelper = new MyDatabaseHelper(this, "BookStore.db", null, 2); dbHelper.getWritableDatabase();
3.3 新增資料
先獲取到了SQLiteDatabase物件,然後使用ContentValues來對要新增的資料進行組裝,接下來呼叫了insert(String table, String nullColumnHack, android.content.ContentValues values)方法將資料新增到表中。ContentValues物件提供了一系列的put()方法過載,只要將表中的每個列名以及相應的待新增資料傳入即可,以下為示例程式碼:
SQLiteDatabase db = dbHelper.getWritableDatabase(); ContentValues values = new ContentValues(); // 開始組裝第一條資料 values.put("name", "The Da Vinci Code"); values.put("author", "Dan Brown"); values.put("pages", 454); values.put("price", 16.96); db.insert("Book", null, values); // 插入第一條資料 values.clear(); // 開始組裝第二條資料 values.put("name", "The Lost Symbol"); values.put("author", "Dan Brown"); values.put("pages", 510); values.put("price", 19.95); db.insert("Book", null, values); // 插入第二條資料
3.4 更新資料
用update(String table, android.content.ContentValues values, String whereClause, String[] whereArgs)方法對資料進行更新,第三、四個引數用於約束更新某一行或某幾行中的資料,不指定則預設所有行,以下為示例程式碼:
SQLiteDatabase db = dbHelper.getWritableDatabase(); ContentValues values = new ContentValues(); values.put("price", 10.99); db.update("Book", values, "name = ?", new String[] { "The Da Vinci Code" });
P.S.第三個引數對應SQL語句的where部分,表示更新所有name=?的行(?是一個佔位符),第四個引數提供的一個字串陣列為第三個引數中的每個佔位符指定相應的內容。
3.5 刪除資料
delete(String table, String whereClause, String[] whereArgs)方法專門用於刪除資料,以下為示例程式碼:
SQLiteDatabase db = dbHelper.getWritableDatabase(); db.delete("Book", "pages > ?", new String[] { "500" });
3.6 查詢資料
query(String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy)方法用於資料查詢。呼叫query()方法後會返回一個Cursor物件,查詢到的所有資料都將從這個物件中取出。
Cursor物件的moveToFirst()方法將資料的指標移動到第一行的位置,然後進去迴圈中遍歷查詢到的每一行資料。在這個迴圈中可以通過Cursor的getColumnIndex(String columnName)方法獲取到某一列在表中對應位置的索引,然後將這個索引傳入到對應的取值方法getXxxx(int columnIndex)中,就可以得到從資料庫中讀取到的資料了。最後呼叫close()方法關閉Cursor,以下為示例程式碼:
SQLiteDatabase db = dbHelper.getWritableDatabase(); // 查詢Book表中所有的資料 Cursor cursor = db.query("Book", null, null, null, null, null, null); if (cursor.moveToFirst()) { do { // 遍歷 Cursor 物件,取出資料並列印 String name = cursor.getString(cursor.getColumnIndex("name")); String author = cursor.getString(cursor.getColumnIndex("author")); int pages = cursor.getInt(cursor.getColumnIndex("pages")); double price = cursor.getDouble(cursor.getColumnIndex("price")); Log.d(TAG, "onClick: book name is " + name); Log.d(TAG, "onClick: book author is " + author); Log.d(TAG, "onClick: book pages is " + pages); Log.d(TAG, "onClick: book price is " + price); } while (cursor.moveToNext()); } cursor.close();
3.7 使用SQL操作資料庫
新增資料,以下為示例程式碼:
db.execSQL("insert into Book (name, author, pages, price) values(?, ?, ?, ?)", new String[] { "The Da Vinci Code", "Dan Brown", "454", "16.96"}); db.execSQL("insert into Book (name, author, pages, price) values(?, ?, ?, ?)", new String[] { "The Lost Symbol", "Dan Brown", "510", "19.95"});
更新資料,以下為示例程式碼:
db.execSQL("update Book set price = ? where name = ?", new String[] { "10.99", "The Da Vinci Code" });
刪除資料,以下為示例程式碼:
db.execSQL("delete from Book where pages > ?", new String[] { "500" });
查詢資料,以下為示例程式碼:
db.rawQuery("select * from Book", null);
四、 使用LitePal操作資料庫
LitePal是一款開源的Android資料庫框架,它採用了物件關係對映(ORM)的模式,即將面向物件的語言和麵向關係的資料庫之間建立一種對映關係。
LitePal專案主頁有詳細的使用文件,地址是:https://github.com/guolindev/LitePal
4.1 配置LitePal
(1) 在app/builde.gradle檔案中宣告該開源庫的引用,以下為示例程式碼:
dependencies { implementation fileTree(dir: "libs", include: ["*.jar"]) implementation 'androidx.appcompat:appcompat:1.1.0' testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' implementation 'org.litepal.guolindev:core:3.2.3' //在LitePal的專案主頁上檢視最新版本號 }
(2) 配置spp/src/main/assets/litepal.xml檔案,檔案內容如下:
<?xml version="1.0" encoding="utf-8" ?> <litepal> <dbname value="BookStore"></dbname> <version value="1"></version> <list></list> </litepal>
其中,<dbname>標籤用於指定資料庫名,<version>標籤用於指定資料庫版本號,<list>標籤用於指定所有的對映模型。
(3) 配置LitePalApplication(讓LitePal的所有功能都可以正常工作)。
a) 修改AndroidManifest.xml中的程式碼,如下所示:
<application android:name="org.litepal.LitePalApplication" ……> …… </application>
b) 如果已經有自己的Application配置,例如:
<manifest> <application android:name="com.example.MyOwnApplication" ... > ... </application> </manifest>
則只需要在你自己的Application中使用LitePal.initialize(context),以下為示例程式碼:
public class MyOwnApplication extends Application { @Override public void onCreate() { super.onCreate(); LitePal.initialize(this); } ... }
4.2 建立資料庫
ORM模式使我們可以用面向物件的思維來操作資料庫。
(1)建表。定義一個Book類,Book類就會對應資料庫中的Book表,而類中的每一個欄位分別對應了表中的每一列,這就是物件關係對映最直觀的體驗,以下為示例程式碼:
public class Book { private int id; private String author; private double price; private int pages; private String name; //省略getter和setter方法 }
(2)修改配置。接下來將Book類新增到對映模型列表中,修改litepal.xml中的程式碼,如下所示:
<litepal>
<dbname value="BookStore"></dbname>
<version value="1"></version>
<list>
<mapping class="com.bignerdranch.android.litepaltest.Book"></mapping>
</list>
</litepal>
P.S.<mapping>標籤來宣告我們要配置的對映模型類,一定要使用完整的類名。所有的對映模型類都配置在<list>標籤下即可。
(3)建立資料庫。進行任意一次資料庫的操作,以下為示例程式碼:
LitePal.getDatabase();
4.3升級資料庫
LitePal避免了SQLiteOpenHelper升級資料庫方式中造成表資料丟失的問題,不需要思考任何邏輯,直接修改任何想改的內容,然後將版本號加1就行了。
以Book表中新增一個press(出版社)列,再新增一張Categroy表為例,以下為示例程式碼:
public class Book { private int id; private String author; private double price; private int pages; private String name; private String press; //省略getter和setter方法 }
public class Category { private int id; private String categoryName; private int categoryCode; //省略setter方法 }
litepal.xml:
<?xml version="1.0" encoding="utf-8" ?> <litepal> <dbname value="BookStore"></dbname> <version value="2"></version> <list> <mapping class="com.bignerdranch.android.litepaltest.Book"></mapping> <mapping class="com.bignerdranch.android.litepaltest.Category"></mapping> </list> </litepal>
4.4 使用LitePal新增資料
只需要創建出模型類的例項,再將所有要儲存的資料設定好,最後呼叫一下save()方法就可以了,以下為示例程式碼:
Book book = new Book(); book.setName("The Da Vinci Code"); book.setAuthor("Dan Brown"); book.setPages(454); book.setPrice(16.96); book.setPress("Unknown"); book.save();
P.S. LitePal進行表管理操作時不需要模型類有任何的繼承結構,但是進行CRUD操作時就不行了,必須要繼承自LitePalSupport類才行。
public class Book extends LitePalSupport { }
4.5 使用LitePal更新資料
最簡單的一種更新方式(使用物件限制性比較大)就是對已儲存的物件重新設定,然後重新呼叫save()方法,示例程式碼如下:
Book book = new Book(); book.setName("The Lost Symbol"); book.setAuthor("Dan Brown"); book.setPages(510); book.setPrice(19.95); book.setPress("Unknown"); book.save(); book.setPrice(10.99); book.save();
P.S.呼叫model.isSaved()方法的結果為true表示已儲存物件,false表示未儲存物件。實際上只有在兩種情況下model.isSave()方法才返回true,一種情況是已經呼叫過model.save()方法去新增資料了。另一種情況是model物件是通過LitePal提供的查詢API查出來的(能從資料庫中查到當然已儲存)。
更為靈巧的更新方式,先新建一個model例項,再直接通過setXxx()方法設定要更新的資料,最後呼叫updateAll(java.lang.String... conditions)方法,以下為示例程式碼:
Book book = new Book(); book.setPrice(16.95); book.setPress("Anchor"); book.updateAll("name = ? and author = ?", "The Lost Symbol", "Dan Brown");
P.S.不可以使用上述方式來set資料使欄位值更新成對應預設值。若想將資料更新成預設值,可使用LitePal提供的setToDefault(String fieldName)方法,以下為示例程式碼:
Book book = new Book(); book.setToDefault("pages"); book.updateAll(); //若不指定條件語句就表示更新所有資料
4.6 使用LitePal刪除資料
主要有兩種方式,第一種是直接呼叫已儲存物件的delete()方法,第二種呼叫了LitePal.deleteAll(Class<?> modelClass, String... conditions)方法來刪除資料。與updateAll()類似,deleteAll()方法如果不指定約束條件,就意味著要刪除表中的所有資料,以下為示例程式碼:
LitePal.deleteAll(Book.class, "price < ?", "15");
4.7 使用LitePal查詢資料
使用LitePal.findAll(Class<?> modelClass, Long... ids)方法,findAll()方法的返回值是一個Model型別的List集合,即不用再通過Cursor物件一行行取值,LitePal已經自動完成了賦值操作,以下為示例程式碼:
List<Book> books = LitePal.findAll(Book.class); //查詢表中的所有資料 for (Book book : books) { Log.d(TAG, "onClick: book name is " + book.getName()); Log.d(TAG, "onClick: book author is " + book.getAuthor()); Log.d(TAG, "onClick: book pages is " + book.getPages()); Log.d(TAG, "onClick: book price is " + book.getPrice()); Log.d(TAG, "onClick: book press is " + book.getPress()); }
LitePal中其他的查詢API
- findFirst(Class<T> modelClass)方法用於查詢表中的第一條資料:
Book firstBook = LitePal.findFirst(Book.class);
- findLast(Class<T> modelClass)方法用於查詢表中的最後一條資料:
Book lastBook = LitePal.findLast(Book.class);
連綴查詢
- select(String... columns)方法用於指定查詢哪幾列的資料:
List<Book> books1 = LitePal.select("name","author").find(Book.class);
- where(String... conditions)方法用於指定查詢的約束條件:
List<Book> books2 = LitePal.where("pages > ?", "400").find(Book.class);
- order(String column)方法用於指定結果的排序方式,其中desc表示降序排列,asc或者不寫表示升序排列:
List<Book> books3 = LitePal.order("price desc").find(Book.class);
- limit(int value)方法用於指定查詢結果的數量,limit(3)表示只查表中的前3條資料:
List<Book> books4 = LitePal.limit(3).find(Book.class);
- offset(int value)方法用於制定查詢結果的偏移量,limit(3).offset(1)表示查詢表中的第2、3、4條資料:
List<Book> books5 = LitePal.limit(3).offset(1).find(Book.class);
- LitePal.findBySQL(String... sql)方法用於進行原生查詢,該方法返回的是一個Cursor物件,還需要用SQLite資料庫中學習的操作方式將資料一一取出才行:
Cursor cursor = LitePal.findBySQL("select * from Book where pages > ? and price < ?", "400", "20");
END