在Android應用開發中,持久化數據的方式有不少,常見的有Shared Preferences、Internal Storage、External Storage、SQLite Databases和Network Connection五種。其中,SQLite使用數據庫方式進行存儲,適合用來存儲數據量比較大的場景。html
不過,因爲SQLite寫起來比較繁瑣且容易出錯,所以,社區出現了各類ORM(Object Relational Mapping)庫,如ORMLite、Realm、LiteOrm和GreenDao等,這些第三方庫有一個共同的目的,那就是爲方便開發者方便使用ORM而出現,簡化的操做包括建立、升級、CRUD等功能。
java
爲了簡化SQLite操做,Jetpack庫提供了Room組件,用來幫助開發者簡化開發者對數據庫操做。Room 持久庫提供了一個SQLite抽象層,讓開發者訪問數據庫更加穩健,數據庫操做的性能也獲得提高。android
Room組件庫包含 3 個重要的概念,分佈是Entity、Dao和Database。sql
使用@Database註解需知足如下條件:shell
簡單來講,應用使用 Room 數據庫來獲取與該數據庫關聯的數據訪問對象 (DAO)。而後應用使用每一個 DAO從數據庫中獲取實體,再將對這些實體的全部更改保存回數據庫中。 最後應用使用實體來獲取和設置與數據庫中的表列相對應的值。數據庫
下面是使用Entity、Dao、Database三者和應用的對應架構示意圖,以下所示。
windows
首先,在app的build.gradle中增長如下配腳本。api
dependencies { def room_version = "2.2.5" implementation "androidx.room:room-runtime:$room_version" annotationProcessor "androidx.room:room-compiler:$room_version" }
Room的使用和傳統的Sqlite數據庫的使用流程是差很少的。首先,使用 @Entity註解定義一個實體類,類會被映射爲數據庫中的一張表,默認實體類的類名爲表名,字段名爲表名,以下所示。架構
@Entity public class User { @PrimaryKey(autoGenerate = true) public int uid; @ColumnInfo(name = "first_name") public String firstName; @ColumnInfo(name = "last_name") public String lastName; @Ignore public boolean sex; }
其中,@PrimaryKey註解用來標註表的主鍵,而且使用autoGenerate = true 來指定了主鍵自增加。@ColumnInfo註解用來標註表對應的列的信息好比表名、默認值等等。@Ignore 註解用來標示忽略這個字段,使用了這個註解的字段將不會在數據庫中生成對應的列信息。app
Dao類是一個接口,主要用於定義一系列操做數據庫的方法,即一般咱們所說的增刪改查。爲了方便開發者操做數據庫,Room提供了@Insert、@Delete、@Update 和 @Query等註解。
@query註解
@Query 是一個查詢註解,它的參數時String類型,咱們直接寫SQL語句進行執行。好比,咱們根據ID查詢某個用戶的信息。
@Query("SELECT * FROM user WHERE uid IN (:userIds)") List<User> loadAllByIds(int[] userIds);
@Insert註解
@Insert註解用於向表中插入一條數據,咱們定義一個方法而後使用 @Insert註解標註便可,以下所示。
@Insert void insertAll(User... users);
其中,@Insert註解有個onConflict參數,表示的是當插入的數據已經存在時候的處理邏輯,有三種操做邏輯,分佈是REPLACE、ABORT和IGNORE。
@Delete註解
@Delete註解用於刪除表的數據,以下所示。
@Delete void delete(User user);
@Update註解
@Update註解用於修改某一條數據 ,和@Delete同樣也是根據主鍵來查找要刪除的實體。
@Update void update(User user);
接下來,咱們新建一個UserDao類,並添加以下代碼。
@Dao public interface UserDao { @Query("SELECT * FROM user") List<User> getAll(); @Query("SELECT * FROM user WHERE uid IN (:userIds)") List<User> loadAllByIds(int[] userIds); @Query("SELECT * FROM user WHERE first_name LIKE :first AND last_name LIKE :last LIMIT 1") User findByName(String first, String last); @Insert void insertAll(User... users); @Delete void delete(User user); @Query("DELETE FROM user WHERE uid = :uid ") void deleteUserById(int uid); @Query("UPDATE user SET first_name = :firstName where uid = :uid") void updateUserById(int uid, String firstName); @Update void update(User user); }
首先,定義一個繼承RoomDatabase的抽象類,而且使用 @Database 註解進行標識,以下所示。
@Database(entities = {User.class}, version = 1) public abstract class AppDatabase extends RoomDatabase { public abstract UserDao userDao(); private static AppDatabase instance = null; public static synchronized AppDatabase getInstance(Context context) { if (instance == null) { instance = Room.databaseBuilder( context.getApplicationContext(), AppDatabase.class, "user.db" //數據庫名稱 ).allowMainThreadQueries().build(); } return instance; } }
完成上述操做以後,使用如下代碼得到建立數據庫的實例。
AppDatabase db = AppDatabase.getInstance(this); UserDao dao = db.userDao(); User user=new User(); user.firstName="ma"; user.lastName="jack"; dao.insertAll(user);
接下來,咱們經過一個簡單的綜合練習來講說Room的基本使用方法。首先,咱們在activity_main.xml
佈局文件中新增4個按鈕,分別用來增刪改查。
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:gravity="center" tools:context=".MainActivity"> <Button android:id="@+id/btn_insert" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textAllCaps="false" android:textSize="24dp" android:text="插入數據" /> <Button android:id="@+id/btn_delete" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="10dp" android:textSize="24dp" android:text="刪除數據" /> <Button android:id="@+id/btn_query" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="10dp" android:textSize="24dp" android:text="查詢數據" /> <Button android:id="@+id/btn_update" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="10dp" android:textSize="24dp" android:text="更新數據" /> </LinearLayout>
而後咱們編寫代碼實現相關的功能,以下所示。
public class MainActivity extends AppCompatActivity { AppDatabase db=null; UserDao dao=null; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); init(); } private void init() { db = AppDatabase.getInstance(this); dao = db.userDao(); insert(); query(); update(); } private void insert() { findViewById(R.id.btn_insert).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { for (int i=0;i<10;i++) { User user=new User("張三"+i,"100"+i); dao.insertAll(user); } } }); } private void query() { findViewById(R.id.btn_query).setOnClickListener(new View.OnClickListener() { @RequiresApi(api = Build.VERSION_CODES.N) @Override public void onClick(View v) { dao.getAll().forEach(new Consumer<User>() { @Override public void accept(User user) { Log.d("Room", user.firstName+","+user.lastName); } }); } }); } private void update() { findViewById(R.id.btn_update).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { dao.updateUserById(2, "李四"); User updateUser = dao.loadUserById(2); Log.e("Room", "update${user.firstName},${user.lastName}"); } }); } }
接下來,運行代碼,執行插入操做,生成的數據庫位於data/data/packageName/databases目錄下。而後,再執行查詢功能,控制檯輸出內容以下。
com.xzh.jetpack D/Room: 張三0,1000 com.xzh.jetpack D/Room: 張三1,1001 com.xzh.jetpack D/Room: 張三2,1002 com.xzh.jetpack D/Room: 張三3,1003 com.xzh.jetpack D/Room: 張三4,1004 com.xzh.jetpack D/Room: 張三5,1005 com.xzh.jetpack D/Room: 張三6,1006 com.xzh.jetpack D/Room: 張三7,1007 com.xzh.jetpack D/Room: 張三8,1008 com.xzh.jetpack D/Room: 張三9,1009
須要說明的是,全部對數據庫的操做都不能夠在主線程中進行,除非在數據庫的Builder上調用了allowMainThreadQueries()或者全部的操做都在子線程中完成,不然程序會崩潰報並報以下錯誤。
java.lang.IllegalStateException: Cannot access database on the main thread since it may potentially lock the UI for a long period of time.
有時候,咱們但願在應用啓動時數據庫中就已經加載了一組特定的數據,咱們將這種行爲稱爲預填充數據庫。在 Room 2.2.0 及更高版本中,開發者可使用 API 方法在初始化時用設備文件系統中預封裝的數據庫文件中的內容預填充 Room 數據庫。
預填充指的是從位於應用 assets/
目錄中的任意位置的裝數據庫文件預填充 Room 數據庫,使用的時候調用createFromAsset() 方法,而後再調用 build()方法便可,以下所示。
Room.databaseBuilder(appContext, AppDatabase.class, "Sample.db") .createFromAsset("database/myapp.db") .build();
createFromAsset() 方法接受一個包含assets/
目錄的相對路徑的字符串參數。
除了將數據內置到應用的 assets/
目錄除外, 咱們還能夠 從位於設備文件系統任意位置讀取預封裝數據庫文件來預填充 Room 數據庫,使用時須要調用createFromFile() 方法,而後再調用 build(),以下所示。
Room.databaseBuilder(appContext, AppDatabase.class, "Sample.db") .createFromFile(new File("mypath")) .build();
createFromFile() 方法接受表明預封裝數據庫文件的絕對路徑的 File 參數,Room 會建立指定文件的副本,而不是直接打開它,而且使用時請確保應用具備該文件的讀取權限。
在使用數據庫的時候就避免不了須要對數據庫進行升級。例如,隨着業務的變化,須要在數據表中新增一個字段,此時就須要對數據表進行升級。
在Room中, 數據庫的升級或者降級須要用到Migration 類。每一個 Migration 子類經過替換 Migration.migrate() 方法定義 startVersion 和 endVersion 之間的遷移路徑。當應用更新須要升級數據庫版本時,Room 會從一個或多個 Migration 子類運行 migrate() 方法,以在運行時將數據庫遷移到最新版本。
例如,當前設備中應用的數據庫版本爲1,若是要將數據庫的版本從1升級到2,那麼代碼以下。
static final Migration MIGRATION_1_2 = new Migration(1, 2) { @Override public void migrate(SupportSQLiteDatabase database) { database.execSQL("CREATE TABLE `Fruit` (`id` INTEGER, " + "`name` TEXT, PRIMARY KEY(`id`))"); } };
其中,Migration方法須要startVersion和endVersion兩個參數,startVersion表示的是升級開始的版本,endVersion表示要升級到的版本,同時須要將@Database註解中的version的值修改成和endVersion相同。
以此類推,若是當前應用的數據庫版本爲2,想要升級到到版本3,那麼代碼以下。
static final Migration MIGRATION_2_3 = new Migration(2, 3) { @Override public void migrate(SupportSQLiteDatabase database) { database.execSQL("ALTER TABLE Book " + " ADD COLUMN pub_year INTEGER"); } };
在Migration編寫完升級方案後,還須要使用addMigrations()方法將升級的方案添加到Room中,以下所示。
Room.databaseBuilder(getApplicationContext(), MyDb.class, "database-name") .addMigrations(MIGRATION_1_2, MIGRATION_2_3).build();
下面是完整代碼。
static final Migration MIGRATION_1_2 = new Migration(1, 2) { @Override public void migrate(SupportSQLiteDatabase database) { // database.execSQL("");執行sql語句 } }; static final Migration MIGRATION_2_3 = new Migration(2, 3) { @Override public void migrate(SupportSQLiteDatabase database) { // database.execSQL("");執行sql語句 } }; Room.databaseBuilder(app,AppDatabase.class, DB_NAME) .addMigrations(MIGRATION_1_2, MIGRATION_2_3) .build();
而後,在Android Studio的工具欄上依次點擊【View】->【Tool windows】->【Device File Explorer】打開數據表便可查看。
數據庫降級使用和升級的步驟差很少,也是使用addMigrations只是startVersion > endVersion 。當在升級或者降級的過程當中出現版本未匹配到的狀況的時候,默認狀況下會直接拋異常出來。
固然咱們也能夠處理異常。升級的時候能夠添加fallbackToDestructiveMigration方法,當未匹配到版本的時候就會直接刪除表而後從新建立。降級的時候添加fallbackToDestructiveMigrationOnDowngrade方法,當未匹配到版本的時候就會直接刪除表而後從新建立。
遷移一般十分複雜,而且數據庫遷移錯誤可能會致使應用崩潰。爲了保持應用的穩定性,須要開發者對遷移進行測試。爲此,Room 提供了一個 room-testing
來協助完成此測試過程。
Room 能夠在編譯時將數據庫的架構信息導出爲 JSON 文件。如需導出架構,請在 app/build.gradle 文件中設置 room.schemaLocation 註釋處理器屬性,以下所示。
android { ... defaultConfig { ... javaCompileOptions { annotationProcessorOptions { arguments += ["room.schemaLocation": "$projectDir/schemas".toString()] } } } }
導出的 JSON 文件表明數據庫的架構歷史記錄。您應將這些文件存儲在版本控制系統中,由於此係統容許 Room 出於測試目的建立較舊版本的數據庫。
測試遷移以前,須要先添加測試依賴androidx.room:room-testing
,並將導出的架構的位置添加爲資源目錄,以下所示。
android { ... sourceSets { // Adds exported schema location as test app assets. androidTest.assets.srcDirs += files("$projectDir/schemas".toString()) } } dependencies { ... testImplementation "androidx.room:room-testing:2.2.5" }
測試軟件包提供了可讀取導出的架構文件的 MigrationTestHelper 類。該軟件包還實現了 JUnit4 TestRule 接口,所以能夠管理建立的數據庫。例如,下面是單次遷移的測試的示例代碼。
@RunWith(AndroidJUnit4.class) public class MigrationTest { private static final String TEST_DB = "migration-test"; @Rule public MigrationTestHelper helper; public MigrationTest() { helper = new MigrationTestHelper(InstrumentationRegistry.getInstrumentation(), MigrationDb.class.getCanonicalName(), new FrameworkSQLiteOpenHelperFactory()); } @Test public void migrate1To2() throws IOException { SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, 1); // db has schema version 1. insert some data using SQL queries. // You cannot use DAO classes because they expect the latest schema. db.execSQL(...); // Prepare for the next version. db.close(); // Re-open the database with version 2 and provide // MIGRATION_1_2 as the migration process. db = helper.runMigrationsAndValidate(TEST_DB, 2, true, MIGRATION_1_2); // MigrationTestHelper automatically verifies the schema changes, // but you need to validate that the data was migrated properly. } }
雖然能夠測試單次增量遷移,但建議您添加一個測試,涵蓋爲應用的數據庫定義的全部遷移。這可確保最近建立的數據庫實例與遵循定義的遷移路徑的舊實例之間不存在差別。下面的示例演示了遷移全部測試。
@RunWith(AndroidJUnit4.class) public class MigrationTest { private static final String TEST_DB = "migration-test"; @Rule public MigrationTestHelper helper; public MigrationTest() { helper = new MigrationTestHelper(InstrumentationRegistry.getInstrumentation(), AppDatabase.class.getCanonicalName(), new FrameworkSQLiteOpenHelperFactory()); } @Test public void migrateAll() throws IOException { // Create earliest version of the database. SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, 1); db.close(); // Open latest version of the database. Room will validate the schema // once all migrations execute. AppDatabase appDb = Room.databaseBuilder( InstrumentationRegistry.getInstrumentation().getTargetContext(), AppDatabase.class, TEST_DB) .addMigrations(ALL_MIGRATIONS).build(); appDb.getOpenHelper().getWritableDatabase(); appDb.close(); } // Array of all migrations private static final Migration[] ALL_MIGRATIONS = new Migration[]{ MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4}; }
遷移數據庫的過程當中,不可避免的會出現一些異常,若是 Room 沒法找到將設備上的現有數據庫升級到當前版本的遷移路徑,會提示IllegalStateException錯誤。在遷移路徑缺失的狀況下,若是丟失現有數據能夠接受,那麼在建立數據庫時能夠調用 fallbackToDestructiveMigration() 構建器方法。
Room.databaseBuilder(getApplicationContext(), MyDb.class, "database-name") .fallbackToDestructiveMigration() .build();
fallbackToDestructiveMigration()方法會指示 Room 在須要執行沒有定義遷移路徑的增量遷移時,破壞性地從新建立應用的數據庫表。若是隻想讓 Room 在特定狀況下回退到破壞性從新建立,可使用 fallbackToDestructiveMigration() 的一些替代選項,以下所示。
爲了測試咱們建立的數據庫,有時候須要在Activity中編寫一些測試代碼。在Android中測試數據庫有兩種方式。
如需測試數據庫實現,推薦的方法是編寫在 Android 設備上運行的 JUnit 測試,因爲執行這些測試不須要建立 Activity,所以它們的執行速度應該比界面測試速度更快。以下是一個JUnit 測試的示例。
@RunWith(AndroidJUnit4.class) public class SimpleEntityReadWriteTest { private UserDao userDao; private TestDatabase db; @Before public void createDb() { Context context = ApplicationProvider.getApplicationContext(); db = Room.inMemoryDatabaseBuilder(context, TestDatabase.class).build(); userDao = db.getUserDao(); } @After public void closeDb() throws IOException { db.close(); } @Test public void writeUserAndReadInList() throws Exception { User user = TestUtil.createUser(3); user.setName("george"); userDao.insert(user); List<User> byName = userDao.findUsersByName("george"); assertThat(byName.get(0), equalTo(user)); } }
Room 使用 SQLite 支持庫,該支持庫提供了與 Android 框架類中的接口相對應的接口。經過此項支持,開發者能夠傳遞該支持庫的自定義實現來測試數據庫查詢。
Android SDK 包含一個 sqlite3 數據庫工具,可用於檢查應用的數據庫。它包含用於輸出表格內容的 .dump 以及用於輸出現有表格的 SQL CREATE 語句的 .schema 等命令。咱們能夠在命令行執行 SQLite 命令,以下所示。
adb -s emulator-5554 shell sqlite3 /data/data/your-app-package/databases/rssitems.db
更多的sqlite3命令行能夠參考SQLite 網站上提供的sqlite3命令行文檔。