学习Android Architecture Componets系列(五)——Room

1. Overview

App把经常需要访问的数据存储在本地将会大大改善用户的体验,这样用户在网络不好时仍然可以浏览内容,当用户网络可用时再更新数据。自从最早版本开始,Android 就提供了SQLite数据库,但是直接使用SQLite存在以下两个缺点:

  • 没有编译时SQL语句的检查。尤其是当数据库表发生变化时,需要手动更新相关代码,这个过程不仅耗时而且容易出错。
  • 需要编写大量SQL语句和Java对象之间相互转化的代码。

因此,Google推出了一款简洁易用的对象映射层——Room来解决上述问题。Room在SQLite的基础上提供了一个方便访问的抽象层,不仅保留了SQLite 强大的功能,而且避免了很多重复编码的工作。Room还支持LiveData数据返回,从而提供可观测的数据库,可以动态地将数据库中的数据变化自动更新到LiveData上,再通过LiveData自动刷新到UI上。

Room由以下三个部分组成:

  • Database:标识所需要创建的数据库。

  • Entity: 代表了持有数据库表记录的类。Room会创建一个数据库表来持有每个实体的成员变量,如果存在不想被持久化的成员变量则将其注解为@Ignore

  • DAO:即Database Access Object,定义了访问数据库的方法。

它们和App中其余部分的关系如下图所示:

学习Android Architecture Componets系列(五)——Room_第1张图片
room_architecture.png

2. 使用方法

以存储单词为例进行介绍。

2.1 定义实体类

@Entity(tableName = "word_table")
public class Word {

    @PrimaryKey
    @NonNull
    @ColumnInfo(name = "word")
    private String word;

    public Word(@NonNull String word) {
        this.word = word;
    }

    public String getWord() {
        return word;
    }
}

顶部使用注解@Entity声明这是一个实体类,为了持久化成员变量,Room 必须可以访问它,所以成员变量要么是公共的要么就提供 getter和setter 方法。@Entity注解的tableName属性定义了这个实体类对应的数据库表名。如果不写,那么默认类名就是表名。

2.2 定义数据访问对象

@Dao
public interface WordDao {
    @Insert
    void insert(Word word);

    @Query("DELETE FROM word_table")
    void deleteAll();

    @Query("SELECT * FROM word_table ORDER BY word ASC")
    LiveData> getAllWords();
}

顶部使用注解@Dao声明这是一个DAO类,数据库的CRUD 操作全部使用注解声明,具体 sql 语句的也是直接写在注解@Query中,如果sql语句存在书写或语法错误,Room可以在编译阶段检测出来。Dao可以是一个接口也可以是一个抽象类,如果是抽象类,那么它可以接受一个RoomDatabase作为构造器的唯一参数。这里我将其定义为一个接口。

2.3 定义数据库抽象类

@Database(entities = {Word.class}, version = 1)
public abstract class WordRoomDatabase extends RoomDatabase {
    public abstract WordDao wordDao();
}

顶部使用注解@Database声明这是一个数据库类,其中entities属性声明该数据库包含哪些实体从而创建相应的数据库表,version属性则声明了该数据库的版本号。该类还需要定义获得与数据库关联的实体所对应的Dao对象。

2.4 创建数据库

@Database(entities = {Word.class}, version = 1)
public abstract class WordRoomDatabase extends RoomDatabase {

    public abstract WordDao wordDao();

    //单例模式
    private static WordRoomDatabase INSTANCE;
    public static WordRoomDatabase getDatabse(final Context context) {
        if (INSTANCE == null) {
            synchronized (WordRoomDatabase.class) {
                if (INSTANCE == null) {
                    INSTANCE = Room.databaseBuilder(context.getApplicationContext(), WordRoomDatabase.class, "word_database")
                            .build();
                }
            }
        }
        return INSTANCE;
    }
}

这里将数据库的创建直接放在了WordRoomDatabase类中,核心代码就是Room.databaseBuilder(context.getApplicationContext(), WordRoomDatabase.class, "word_database").build()。目的是为了将WordRoomDatabase封装成一个单例类,这样在任何地方需要操作该数据库操作时,都可以通过方法getDatabase()直接获得全局唯一的数据库使用,而不是重新创建。

3. Room的工作原理

我们从Room.databaseBuilder(context.getApplicationContext(), WordRoomDatabase.class, "word_database").build()这条语句入手来进行分析。

这一行代码传递了以下几个信息:
① Room是通过建造者模式(Builder pattern) 来获取数据库实例的。
② Builder需要传入3个参数,第1个参数为context,第3个参数为数据库的名字。而第2个参数比较特别,是一个Class对象,对应的就是所需创建的数据库类。这是不是意味者Room还用到了反射机制呢?我们看一看Room的部分源码:

@NonNull
public static  RoomDatabase.Builder databaseBuilder(
            @NonNull Context context, @NonNull Class klass, @NonNull String name) {
    if (name == null || name.trim().length() == 0) {
        throw new IllegalArgumentException("Cannot build a database with null or empty name."
                    + " If you are trying to create an in memory database, use Room.inMemoryDatabaseBuilder");
    }
    return new RoomDatabase.Builder<>(context, klass, name);
}

@NonNull
public static  RoomDatabase.Builder inMemoryDatabaseBuilder(
            @NonNull Context context, @NonNull Class klass) {
    return new RoomDatabase.Builder<>(context, klass, null);
}

原来RoomDatabase.Builder对象不仅可以通过databaseBuilder()方法获得,还可以使用inMemoryDatabase()方法。二者的区别是:inMemoryDatabase()方法少了一个数据库名作为参数,从方法名可以看出它创建的是一个临时的数据库。

重点看一下RoomDatabase.Builder的源码实现:

public static class Builder {
    private final Class mDatabaseClass;
    private final String mName;
    private final Context mContext;
    private ArrayList mCallbacks;

    private SupportSQLiteOpenHelper.Factory mFactory;
    private boolean mAllowMainThreadQueries;
    private boolean mRequireMigration;
    //数据库升级辅助类
    private MigrationContainer mMigrationContainer;

    Builder(@NonNull Context context, @NonNull Class klass, @Nullable String name) {
        mContext = context;
        mDatabaseClass = klass;
        mName = name;
        mRequireMigration = true;
        mMigrationContainer = new MigrationContainer();
    }

    //设置数据库工厂,若不设置则默认为FrameworkSQLiteOpenHelperFactory
    @NonNull
    public Builder openHelperFactory(@Nullable SupportSQLiteOpenHelper.Factory factory) {
        mFactory = factory;
        return this;
    }

    //添加Migration对象,指明数据库从一个版本升级到另一个版本
    @NonNull
    public Builder addMigrations(Migration... migrations) {
        mMigrationContainer.addMigrations(migrations);
        return this;
    }

    //是否允许在主线程访问数据库
    @NonNull
    public Builder allowMainThreadQueries() {
        mAllowMainThreadQueries = true;
        return this;
    }

    //如果数据库升级发生错误,则重新创建数据库,而不是直接崩溃
    @NonNull
    public Builder fallbackToDestructiveMigration() {
        mRequireMigration = false;
        return this;
    }

    //添加回调
    @NonNull
    public Builder addCallback(@NonNull Callback callback) {
        if (mCallbacks == null) {
            mCallbacks = new ArrayList<>();
        }
        mCallbacks.add(callback);
        return this;
    }

    //创建并初始化数据库
    @NonNull
    public T build() {

        if (mContext == null) {
            throw new IllegalArgumentException("Cannot provide null context for the database.");
        }

        if (mDatabaseClass == null) {
            throw new IllegalArgumentException("Must provide an abstract class that extends RoomDatabase");
        }

        if (mFactory == null) {
            mFactory = new FrameworkSQLiteOpenHelperFactory();
        }

        DatabaseConfiguration configuration = new DatabaseConfiguration(mContext, mName, mFactory,
                      mMigrationContainer, mCallbacks, mAllowMainThreadQueries, mRequireMigration);

        //Room的getGeneratedImplementation()方法通过反射机制创建数据库
        T db = Room.getGeneratedImplementation(mDatabaseClass, DB_IMPL_SUFFIX);
        //数据库初始化
        db.init(configuration);
        return db;
    }
}

RoomDatabase.Builder每个方法的作用都在注释里作了说明,其中build()最终是通过调用Room.getGeneratedImplementation()方法来生成数据库实例的,那我们再来看看这个方法的源码:

@NonNull
static  T getGeneratedImplementation(Class klass, String suffix) {
    final String fullPackage = klass.getPackage().getName();
    String name = klass.getCanonicalName();
    final String postPackageName = fullPackage.isEmpty() ? name: (name.substring(fullPackage.length() + 1));
    final String implName = postPackageName.replace('.', '_') + suffix;
    try {

        final Class aClass = (Class) Class.forName(
                    fullPackage.isEmpty() ? implName : fullPackage + "." + implName);
        return aClass.newInstance();

    } catch (ClassNotFoundException e) {
        throw new RuntimeException("cannot find implementation for "
                    + klass.getCanonicalName() + ". " + implName + " does not exist");
    } catch (IllegalAccessException e) {
        throw new RuntimeException("Cannot access the constructor"
                    + klass.getCanonicalName());
    } catch (InstantiationException e) {
        throw new RuntimeException("Failed to create an instance of "
                    + klass.getCanonicalName());
    }
}

很明显,这个方法就是通过反射机制得到数据库实例的。有意思的是,它先对RoomDatabase.Builder()方法传入的xxx.class进行了扩展,然后再基于这个扩展的xxx_Impl.class反射获取实例。

这是因为Room要求我们定义的数据库类为抽象类即可,但是抽象类是无法直接实例化的,所以Room在编译阶段帮我们做了大量的工作,包括实现这个抽象类。那这个实现类是如何生成的呢?注意到我们在使用Room框架时使用了大量的注解,下图列出了Room中声明的所有注解类:

学习Android Architecture Componets系列(五)——Room_第2张图片
room-annotations.png

所以Room的核心就是基于注解在编译阶段自动生成相关代码,具体的逻辑则完全由Android的APT工具——annotationProcessor依赖注入实现。

APT (Annotation Processing Tool) 即注解处理器,是一种处理注解的工具,用来在编译时扫描和处理注解。注解处理器以Java代码(或者编译过的字节码)作为输入,生成.java文件作为输出。我们编译基于Room开发的项目,会发现在app/build/generated/source/apt/debug目录下Room为我们生成好了接口WordDao和抽象类WordRoomDatabase的具体实现类。

学习Android Architecture Componets系列(五)——Room_第3张图片
apt.png

为什么会多出一个WordDao_Impl类呢?我们还是先分析WordDao_Impl类的代码:

public class WordRoomDatabase_Impl extends WordRoomDatabase {
  private volatile WordDao _wordDao;

  protected SupportSQLiteOpenHelper createOpenHelper(DatabaseConfiguration configuration) {
    final SupportSQLiteOpenHelper.Callback _openCallback = new RoomOpenHelper(configuration, new RoomOpenHelper.Delegate(1) {
      public void createAllTables(SupportSQLiteDatabase _db) {
        _db.execSQL("CREATE TABLE IF NOT EXISTS `word_table` (`word` TEXT NOT NULL, PRIMARY KEY(`word`))");
        _db.execSQL("CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)");
        _db.execSQL("INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"d35b8f1bacddbbdac6402852f1af929f\")");
      }

      public void dropAllTables(SupportSQLiteDatabase _db) {
        _db.execSQL("DROP TABLE IF EXISTS `word_table`");
      }

      protected void onCreate(SupportSQLiteDatabase _db) {
        if (mCallbacks != null) {
          for (int _i = 0, _size = mCallbacks.size(); _i < _size; _i++) {
            mCallbacks.get(_i).onCreate(_db);
          }
        }
      }

      public void onOpen(SupportSQLiteDatabase _db) {
        mDatabase = _db;
        internalInitInvalidationTracker(_db);
        if (mCallbacks != null) {
          for (int _i = 0, _size = mCallbacks.size(); _i < _size; _i++) {
            mCallbacks.get(_i).onOpen(_db);
          }
        }
      }

      protected void validateMigration(SupportSQLiteDatabase _db) {
        final HashMap _columnsWordTable = new HashMap(1);
        _columnsWordTable.put("word", new TableInfo.Column("word", "TEXT", true, 1));
        final HashSet _foreignKeysWordTable = new HashSet(0);
        final HashSet _indicesWordTable = new HashSet(0);
        final TableInfo _infoWordTable = new TableInfo("word_table", _columnsWordTable, _foreignKeysWordTable, _indicesWordTable);
        final TableInfo _existingWordTable = TableInfo.read(_db, "word_table");
        if (! _infoWordTable.equals(_existingWordTable)) {
          throw new IllegalStateException("Migration didn't properly handle word_table(com.example.room.Word).\n"
                  + " Expected:\n" + _infoWordTable + "\n"
                  + " Found:\n" + _existingWordTable);
        }
      }
    }, "d35b8f1bacddbbdac6402852f1af929f");
    final SupportSQLiteOpenHelper.Configuration _sqliteConfig = SupportSQLiteOpenHelper.Configuration.builder(configuration.context)
        .name(configuration.name)
        .callback(_openCallback)
        .build();
    final SupportSQLiteOpenHelper _helper = configuration.sqliteOpenHelperFactory.create(_sqliteConfig);
    return _helper;
  }

  @Override
  protected InvalidationTracker createInvalidationTracker() {
    return new InvalidationTracker(this, "word_table");
  }

  @Override
  public WordDao wordDao() {
    if (_wordDao != null) {
      return _wordDao;
    } else {
      synchronized(this) {
        if(_wordDao == null) {
          _wordDao = new WordDao_Impl(this);
        }
        return _wordDao;
      }
    }
  }
}

它不仅实现了WordRoomDatabase中定义的wordDao()方法,而且还实现了WordRoomDatabase的父类RoomDatabase中createOpenHelper()方法。因为WordDao被定义成了一个接口,所以wordDao()方法得到的是WordDao实现类WordDao_Impl的实例,而这个类也是由Room框架帮我们生成的。

我们在WordDao中声明了3个方法:插入一个单词的insert()、删除单词表的deleteAll()和获取表中所有单词的getAllWords()。所以,WordDao_Impl实现了这3个方法,这样我们不再需要编写具体的代码,只需要使用Room提供的注解即可,大大简化了我们的工作。

public class WordDao_Impl implements WordDao {
  private final RoomDatabase __db;

  private final EntityInsertionAdapter __insertionAdapterOfWord;

  private final SharedSQLiteStatement __preparedStmtOfDeleteAll;

  public WordDao_Impl(RoomDatabase __db) {
    this.__db = __db;
    //EntityInsertionAdapter类的匿名内部类实现,
    this.__insertionAdapterOfWord = new EntityInsertionAdapter(__db) {
      @Override
      public String createQuery() {
        return "INSERT OR ABORT INTO `word_table`(`word`) VALUES (?)";
      }

      @Override
      public void bind(SupportSQLiteStatement stmt, Word value) {
        if (value.getWord() == null) {
          stmt.bindNull(1);
        } else {
          stmt.bindString(1, value.getWord());
        }
      }
    };
    this.__preparedStmtOfDeleteAll = new SharedSQLiteStatement(__db) {
      @Override
      public String createQuery() {
        final String _query = "DELETE FROM word_table";
        return _query;
      }
    };
  }

  @Override
  public void insert(Word word) {
    __db.beginTransaction();
    try {
      __insertionAdapterOfWord.insert(word);
      __db.setTransactionSuccessful();
    } finally {
      __db.endTransaction();
    }
  }

  @Override
  public void deleteAll() {
    final SupportSQLiteStatement _stmt = __preparedStmtOfDeleteAll.acquire();
    __db.beginTransaction();
    try {
      _stmt.executeUpdateDelete();
      __db.setTransactionSuccessful();
    } finally {
      __db.endTransaction();
      __preparedStmtOfDeleteAll.release(_stmt);
    }
  }

  @Override
  public LiveData> getAllWords() {
    final String _sql = "SELECT * FROM word_table ORDER BY word ASC";
    final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 0);
    return new ComputableLiveData>() {
      private Observer _observer;

      @Override
      protected List compute() {
        if (_observer == null) {
          _observer = new Observer("word_table") {
            @Override
            public void onInvalidated(@NonNull Set tables) {
              invalidate();
            }
          };
          __db.getInvalidationTracker().addWeakObserver(_observer);
        }
        final Cursor _cursor = __db.query(_statement);
        try {
          final int _cursorIndexOfWord = _cursor.getColumnIndexOrThrow("word");
          final List _result = new ArrayList(_cursor.getCount());
          while(_cursor.moveToNext()) {
            final Word _item;
            final String _tmpWord;
            _tmpWord = _cursor.getString(_cursorIndexOfWord);
            _item = new Word(_tmpWord);
            _result.add(_item);
          }
          return _result;
        } finally {
          _cursor.close();
        }
      }

      @Override
      protected void finalize() {
        _statement.release();
      }
    }.getLiveData();
  }
}

4. 总结

学习Android Architecture Componets系列(五)——Room_第4张图片
Room.png

你可能感兴趣的:(学习Android Architecture Componets系列(五)——Room)