从Room源码看抽象与封装——Dao

目录

源码解析目录
从Room源码看抽象与封装——SQLite的抽象
从Room源码看抽象与封装——数据库的创建
从Room源码看抽象与封装——数据库的升降级
从Room源码看抽象与封装——Dao
从Room源码看抽象与封装——数据流

前言

这一系列的文章的长度大大超出了我的预期,我本想着大概需要两篇文章来分析Room的源码,结果现在这都第四篇了,居然还没来到Dao,没关系,好消息来了,这篇文章我们终于要来看看Room对于“增删改查”的实现了。

1. Dao

Dao是Data Access Objects的缩写,翻译成中文就是“数据访问对象”,这是一个面向对象的数据库接口,再翻译成大白话就是,这是我们访问数据库的接口,能进行对象数据跟数据库数据的双向转换,方便我们使用数据库。

Room架构

如上图所示,这是Room的架构图,一般而言App是通过Dao来操作数据库的,包括从数据库中获取数据和保存数据到数据库,显然,我们对于数据库的存和取使用的都是Entity,即Java对象,这就体现出了Dao的职责:对象数据跟数据库数据的双向转换,并且帮我们完成“增删改查”。


Long long ago,在第一篇文章SQLite的抽象中,讲了Room对“增删改查”的抽象,这种抽象奠定了Dao的基础,其实也体现出了Room实现“增删改查”的一些细节。这里我帮你复习一下。

SupportSQLiteStatement和SupportSQLiteQuery

“增删改”是通过SupportSQLiteStatement完成的,查询是通过SupportSQLiteQuery完成的。话虽这么说,但是从我们定义的Dao到最终“增删改查”的实现,这中间经历了漫漫长途,没错,这是辆长途汽车,把车门封死,我要飙车了。

2. RoomDatabase

在看“增删改查”之前我们再来重新认识一下RoomDatabase。很“神奇”的一点是,RoomDatabase它并不是个Database。之所以这么说,是因为RoomDatabase并没有实现SupportSQLiteDatabase这个接口。在Room体系当中,SupportSQLiteDatabase接口的唯一实现类是FrameworkSQLiteDatabaseRoomDatabase虽然并不是个Database,但是它却包含了一个SupportSQLiteDatabase,也就是说RoomDatabase采用了组合的模式来代替了继承,之所以这么做是因为,严格来讲RoomDatabase并不是一个Database,但是它也包括了部分Database的职责,除此之外,RoomDatabase还有更多别的职责。RoomDatabase是对SupportSQLiteDatabase的收缩和扩展,使用组合的模式让实现变得更加灵活。

public abstract class RoomDatabase {
    //包含有 SupportSQLiteDatabase
    protected volatile SupportSQLiteDatabase mDatabase;
    private SupportSQLiteOpenHelper mOpenHelper;

    @CallSuper
    public void init(@NonNull DatabaseConfiguration configuration) {
        mOpenHelper = createOpenHelper(configuration);
        //...
    }

    @NonNull
    public SupportSQLiteOpenHelper getOpenHelper() {
        return mOpenHelper;
    }

    @NonNull
    protected abstract SupportSQLiteOpenHelper createOpenHelper(DatabaseConfiguration config);

    // used in generated code
    public void assertNotMainThread() {
        if (mAllowMainThreadQueries) {
            return;
        }
        if (isMainThread()) {
            throw new IllegalStateException("Cannot access database on the main thread since"
                    + " it may potentially lock the UI for a long period of time.");
        }
    }

    // 以下方法是对 SupportSQLiteDatabase 方法的包装,只保留了少量用得到的方法
    // 同时也方便我们在单元测试时 mock RoomDatabase

    public Cursor query(String query, @Nullable Object[] args) {
        return mOpenHelper.getWritableDatabase().query(new SimpleSQLiteQuery(query, args));
    }

    public Cursor query(SupportSQLiteQuery query) {
        //不能在主线程中
        assertNotMainThread();
        return mOpenHelper.getWritableDatabase().query(query);
    }

    //把SQL语句(String类型)编译成可以执行的SQL语句(SupportSQLiteStatement类型)
    public SupportSQLiteStatement compileStatement(@NonNull String sql) {
        //不能在主线程中
        assertNotMainThread();
        return mOpenHelper.getWritableDatabase().compileStatement(sql);
    }

    public void beginTransaction() {
        //不能在主线程中
        assertNotMainThread();
        SupportSQLiteDatabase database = mOpenHelper.getWritableDatabase();
        database.beginTransaction();
    }

    public void endTransaction() {
        mOpenHelper.getWritableDatabase().endTransaction();
    }
    
    public void setTransactionSuccessful() {
        mOpenHelper.getWritableDatabase().setTransactionSuccessful();
    }
    
    public boolean inTransaction() {
        return mOpenHelper.getWritableDatabase().inTransaction();
    }
}

RoomDatabase源码当中可以看出,虽然RoomDatabase并不是Database,但是它包含了相当Database的职责,RoomDatabaseSupportSQLiteDatabase中的方法进行了大量的“收缩”,只保留了几个“用得着”的方法,此外,RoomDatabase中还包含了除Database之外的其它职责,例如,Database的创建及打开等,这就是我说的,RoomDatabase是对SupportSQLiteDatabase的收缩和扩展。
注意,RoomDatabase中的query,compileStatementbeginTransaction方法中都加有判断,强制要求不能在主线程中调用,因为Room把“增删改”都放到了Transaction中,也就是说,“增删改查”都不能发生在主线程中,避免我们不小心在主线程操作数据库。

3. 增删改

先来回顾一下SupportSQLiteStatement

public interface SupportSQLiteStatement extends SupportSQLiteProgram {
    /**
     * 执行SQL, 非 SELECT / INSERT / DELETE / UPDATE
     * 例如CREATE / DROP table, view, trigger, index 等
     */
    void execute();

    /**
     * 例如 UPDATE / DELETE 这种
     * 返回有多少行受了影响
     */
    int executeUpdateDelete();

    /**
     * INSERT这种
     * 返回 row ID,-1代表失败
     */
    long executeInsert();

    /**
     * 执行SQL后返回一个数字
     * 例如 SELECT COUNT(*) FROM table;
     */
    long simpleQueryForLong();
    
    /**
     * 执行SQL后返回一个String
     */
    String simpleQueryForString();
}

可以看出,通过SupportSQLiteStatement我们可以方便地执行增(executeInsert)删改(executeUpdateDelete),这是因为SupportSQLiteStatement本身就是对于SQLiteStatement的映射,要知道在SQLite中,“增删改”原本就是通过SQLiteStatement来执行的,只不过藏的比较深,我们不经常接触到。
到目前为止,即使我们不去查看Room的源码,也基本上可以推测出Dao中的“增删改”是如何被执行的。首先注解处理器帮我们生成出来了相应的SQL语句,然后这些SQL语句通过RoomDatabasecompileStatement方法被编译成了SupportSQLiteStatement,最后调用SupportSQLiteStatement上的executeInsert或者executeUpdateDelete方法来完成“增删改”。这种推测从流程上讲是对的,要是我们自己设计可能也就这么干了,但是,Google就是Google,总是在抽象层次上能更进一步。下面我就来看看Room是如何实现的。

3.1 SharedSQLiteStatement

我们方才的推测最大的问题就在于,没有考虑增删改SupportSQLiteStatement的共性,而共性往往是抽象的基础或者说切入点。增删改SupportSQLiteStatement的共性就在于:

  1. 一般情况下“增删改”SQL语句都会包含有若干占位符(?),而后在执行之前需要绑定具体的参数。
  2. “增删改”SQL语句是可以重用的,因为SQL语句使用了占位符(?),而不是直接把参数硬编码进SQL语句中,只需要在执行之前把最新的参数重新绑定到SQL语句中就可以重用了。
  3. “增删改”重用的几率是很大的,以UPDATE为例,向数据库多次插入数据再正常不过了,只是我们每次插入的数据不一样,如果使用带占位符的SQL语句,那么SQL语句是一模一样的。
  4. 当然,重用是有益的,降低内存占用,提升执行效率。

基于以上几点,Room抽象出了SharedSQLiteStatement,用于解决增删改SupportSQLiteStatement重用的问题。

public abstract class SharedSQLiteStatement {
    //多线程重用的标记
    private final AtomicBoolean mLock = new AtomicBoolean(false);

    private final RoomDatabase mDatabase;
    private volatile SupportSQLiteStatement mStmt;

    /**
     * Creates an SQLite prepared statement that can be re-used across threads. If it is in use,
     * it automatically creates a new one.
     * 如注释所说,statement会跨线程重用,如果statement正在使用就会再新建一个
     */
    public SharedSQLiteStatement(RoomDatabase database) {
        mDatabase = database;
    }

    /**
     * SQL语句,注解处理器会帮我们生成
     */
    protected abstract String createQuery();

    protected void assertNotMainThread() {
        mDatabase.assertNotMainThread();
    }

    private SupportSQLiteStatement createNewStatement() {
        String query = createQuery();
        return mDatabase.compileStatement(query);
    }

    //如果 mStmt存在并且没有在使用就重用,不然就新建一个
    private SupportSQLiteStatement getStmt(boolean canUseCached) {
        final SupportSQLiteStatement stmt;
        if (canUseCached) {
            if (mStmt == null) {
                mStmt = createNewStatement();
            }
            stmt = mStmt;
        } else {
            // it is in use, create a one off statement
            stmt = createNewStatement();
        }
        return stmt;
    }

    /**
     * 通过这个方法获取 SupportSQLiteStatement,使用完之后必须调用 release
     */
    public SupportSQLiteStatement acquire() {
        assertNotMainThread();
        return getStmt(mLock.compareAndSet(false, true));
    }

    /**
     * 当 SupportSQLiteStatement不再使用时,必须调用这个方法
     */
    public void release(SupportSQLiteStatement statement) {
        if (statement == mStmt) {
            mLock.set(false);
        }
    }
}

SharedSQLiteStatement的基本使用方式是,通过acquire方法获取一个SupportSQLiteStatement对象,在使用完后通过release方法释放。SharedSQLiteStatement只保留了一个SupportSQLiteStatement,在我们使用完毕调用release后才会被重用。在多线程的情形下,如果保留的SupportSQLiteStatement还未被release,就会新建一个SupportSQLiteStatement(但是这个新建的Statement不会被保留重用),不会因为要重用而影响正常的执行。


由于增、删改需要调用SupportSQLiteStatement不同的方法,所以增、删改分别各自实现了SharedSQLiteStatement

//插入Entity
public abstract class EntityInsertionAdapter<T> extends SharedSQLiteStatement {
    public EntityInsertionAdapter(RoomDatabase database) {
        super(database);
    }

    //绑定参数
    protected abstract void bind(SupportSQLiteStatement statement, T entity);

    public final void insert(T entity) {
        //获取,可能会重用之前的 SupportSQLiteStatement
        final SupportSQLiteStatement stmt = acquire();
        try {
            //绑定
            bind(stmt, entity);
            //执行 executeInsert
            stmt.executeInsert();
        } finally {
            //释放
            release(stmt);
        }
    }

    //其它方法都是类似的
}

//删除、更新Entity
public abstract class EntityDeletionOrUpdateAdapter<T> extends SharedSQLiteStatement {
    public EntityDeletionOrUpdateAdapter(RoomDatabase database) {
        super(database);
    }

    //绑定参数
    protected abstract void bind(SupportSQLiteStatement statement, T entity);

    public final int handle(T entity) {
        //获取,可能会重用之前的 SupportSQLiteStatement
        final SupportSQLiteStatement stmt = acquire();
        try {
            //绑定
            bind(stmt, entity);
            //执行 executeUpdateDelete
            return stmt.executeUpdateDelete();
        } finally {
            //释放
            release(stmt);
        }
    }

    //其它方法都是类似的
}

万事俱备,只欠东风。我们只需要在Dao中实现对应的EntityInsertionAdapter<T>或者EntityDeletionOrUpdateAdapter<T>,就可以执行“增删改”操作了。而这些是注解处理器帮我们实现的,我们留待最后再看。

4. 查询

依然是Long long ago,在第一篇文章SQLite的抽象中就介绍了,SupportSQLiteDatabase收紧了查询方法,并且底层使用rawQueryWithFactory方法来真正的执行查询。我们再来复习一下。

public interface SupportSQLiteQuery {
    String getSql();
    
    //重点在这个方法
    void bindTo(SupportSQLiteProgram statement);

    int getArgCount();
}

class FrameworkSQLiteDatabase implements SupportSQLiteDatabase {
    private final SQLiteDatabase mDelegate;

    @Override
    public Cursor query(final SupportSQLiteQuery supportQuery) {
        return mDelegate.rawQueryWithFactory(new SQLiteDatabase.CursorFactory() {
            @Override
            public Cursor newCursor(SQLiteDatabase db, SQLiteCursorDriver masterQuery,
                    String editTable, SQLiteQuery query) {
                supportQuery.bindTo(new FrameworkSQLiteProgram(query));
                return new SQLiteCursor(masterQuery, editTable, query);
            }
        }, supportQuery.getSql(), EMPTY_STRING_ARRAY, null);
    }
    
    //...
}

查询SQL的执行看上去很简单,但是却暗含着很多并不简单的内容。
首先,SupportSQLiteQuery的定义就很奇怪,getSql表示SQL语句(包含占位符),getArgCount表示SQL语句中有几个占位符,单凭这两个信息,这个查询SQL根本无法执行,SupportSQLiteQuery中最重要的方法是bindTo,调用这个方法进行参数绑定,在这之后SQL语句才变得完整,才能被执行。按道理来说,SQL语句和绑定参数是配套的,正常流程应该是我们向数据库提供SQL语句及其绑定参数,然后执行查询,但是Room并没有这么做。不这么做居然也能完成查询,真是神奇。
其次,再来看看真正执行查询的底层方法rawQueryWithFactory,它的完整签名是

Cursor rawQueryWithFactory(
            CursorFactory cursorFactory, String sql, String[] selectionArgs,
            String editTable)

第一个参数是cursorFactory,即Cursor工厂,这个方法本身我们就不经常使用,即使用第一个参数也往往传null,表示使用默认Cursor工厂;第二个参数是sql,没啥好说的;第三个参数是selectionArgs,即SQL语句的绑定参数;最后一个参数可以忽略。
Room对于rawQueryWithFactory方法的使用也很奇怪,明明必须要提供SQL语句的绑定参数,但是Room却使用了一个空字符串数组EMPTY_STRING_ARRAY敷衍了过去,这么说来真相就只有一个了,必然是传递的第一个参数cursorFactory有啥魔法,那我们就来仔细看看。

class FrameworkSQLiteDatabase implements SupportSQLiteDatabase {
    @Override
    public Cursor query(final SupportSQLiteQuery supportQuery) {
        return mDelegate.rawQueryWithFactory(new SQLiteDatabase.CursorFactory() {
            @Override
            public Cursor newCursor(SQLiteDatabase db, SQLiteCursorDriver masterQuery,
                    String editTable, SQLiteQuery query) {
                //调用了SupportSQLiteQuery上的bindTo方法,很可疑
                supportQuery.bindTo(new FrameworkSQLiteProgram(query));
                return new SQLiteCursor(masterQuery, editTable, query);
            }
        }, supportQuery.getSql(), EMPTY_STRING_ARRAY, null);
    }
}

第一个参数是Cursor工厂,要求我们创建一个Cursor出来,按道理来说,如果不是为了创建自定义的Cursor,是没必要传递这个参数的。Room这里还真的没有创建什么特别的、自定义的Cursor出来,还是使用的原来的SQLiteCursor,Room这么做的唯一目的就是为了完成SQL语句的参数绑定,因为我们提供的SQL语句(String类型)会被转化为SQLiteQuery,然后作为参数传递到newCursor方法当中,SQLiteQuerySQLiteProgram的子类,可以被包装成FrameworkSQLiteProgram,正好回调SupportSQLiteQuery上的bindTo方法,进而完成参数的绑定。在newCursor被调用时,查询SQL还未真正执行,此时绑定参数还来得及。
总结一下,Room使用SQLiteDatabase底层的rawQueryWithFactory方法来执行查询操作,并且在需要提供selectionArgs参数时使用空字符串数组敷衍过去,由于此时查询尚未执行,SQLiteDatabase并不知道我们给的绑定参数是错误的,它只是会默默地帮我们把SQL语句转化为SQLiteQuery,然后Room使用一个并没有实质作用的CursorFactory(仅仅利用了它的调用时机),在查询要执行前的最后一刻,把缺失的绑定参数给补上。真是演了一出偷梁换柱的好戏,妙啊!
此时,我的内心是忐忑的,生怕有个还没被我绕晕的人,上来怼我一句,有个卵用?!好好地提供selectionArgs参数不就完事了。

额。。。好,我们进行下一个话题。
其实,最根本的原因在于“类型安全”,Room是很强调这一点的。这里的类型安全指的是SQL语句在绑定参数时应该依据参数的类型绑定,例如bindStringbindLong等等,而不是什么类型都转换成String再绑定,这么做不安全,也不方便。SQLiteProgram其实是包含有一个叫bindAllArgsAsStrings的方法的,SupportSQLiteProgram在定义时选择将其剔除,不暴露出来。非常遗憾的是,rawQueryWithFactory方法的selectionArgs参数就是一个String[],这并不符合Room的设计理念。

我们姑且把FrameworkSQLiteDatabase设计出的这套query的方式称之为“延迟绑定”。

扯了这么多,现在的问题就归结为了如何向SupportSQLiteQuery提供绑定参数,SupportSQLiteQuery只定义了三个方法,并没有定义一个提供绑定参数的方法。

4.1 SimpleSQLiteQuery和RoomSQLiteQuery

public interface SupportSQLiteQuery {
    String getSql();
    
    void bindTo(SupportSQLiteProgram statement);

    int getArgCount();
}

如上所述,SupportSQLiteQuery并没有定义一个提供绑定参数的方法。即使FrameworkSQLiteDatabase费那么大劲设计出了“延迟绑定”的方式,在bindTo方法被调用时,我们依然不知道哪里可以提供要绑定的参数。不把绑定参数放到SupportSQLiteQuery中,这似乎非常不合理,毕竟,SQL语句和绑定参数是配套的。其实,Room当中的确包含有这种“正常思路”的SQLiteQuery,这就是SupportSQLiteQuery的实现类SimpleSQLiteQuery

public final class SimpleSQLiteQuery implements SupportSQLiteQuery {
    private final String mQuery;
    
    //这里保存了要绑定的参数
    @Nullable
    private final Object[] mBindArgs;

    public SimpleSQLiteQuery(String query, @Nullable Object[] bindArgs) {
        mQuery = query;
        mBindArgs = bindArgs;
    }

    public SimpleSQLiteQuery(String query) {
        this(query, null);
    }

    @Override
    public String getSql() {
        return mQuery;
    }

    @Override
    public void bindTo(SupportSQLiteProgram statement) {
        bind(statement, mBindArgs);
    }

    @Override
    public int getArgCount() {
        return mBindArgs == null ? 0 : mBindArgs.length;
    }

    public static void bind(SupportSQLiteProgram statement, Object[] bindArgs) {
        if (bindArgs == null) {
            return;
        }
        final int limit = bindArgs.length;
        for (int i = 0; i < limit; i++) {
            final Object arg = bindArgs[i];
            bind(statement, i + 1, arg);
        }
    }

    private static void bind(SupportSQLiteProgram statement, int index, Object arg) {
        // extracted from android.database.sqlite.SQLiteConnection
        if (arg == null) {
            statement.bindNull(index);
        } else if (arg instanceof byte[]) {
            statement.bindBlob(index, (byte[]) arg);
        } else if (arg instanceof Float) {
            statement.bindDouble(index, (Float) arg);
        } else if (arg instanceof Double) {
            statement.bindDouble(index, (Double) arg);
        } else if (arg instanceof Long) {
            statement.bindLong(index, (Long) arg);
        } else if (arg instanceof Integer) {
            statement.bindLong(index, (Integer) arg);
        } else if (arg instanceof Short) {
            statement.bindLong(index, (Short) arg);
        } else if (arg instanceof Byte) {
            statement.bindLong(index, (Byte) arg);
        } else if (arg instanceof String) {
            statement.bindString(index, (String) arg);
        } else if (arg instanceof Boolean) {
            statement.bindLong(index, ((Boolean) arg) ? 1 : 0);
        } else {
            throw new IllegalArgumentException("Cannot bind " + arg + " at index " + index
                    + " Supported types: null, byte[], float, double, long, int, short, byte,"
                    + " string");
        }
    }
}

可以看出,SimpleSQLiteQuery的确是SupportSQLiteQuery的简单实现类。SimpleSQLiteQuery非常符合“正常思路”下SQLiteQuery该有的样子,有SQL语句,有绑定参数mBindArgs,在bindTo方法被调用时,根据mBindArgs的参数类型绑定一下子就完事了。但是,事实上,Room只在非常特定的条件下使用了SimpleSQLiteQuery,使用条件都是那种,第一,查询语句是固定的;第二,没有绑定参数的。
Room并没有把SimpleSQLiteQuery作为执行查询操作的通用手段。为什么?主要原因有如下几个方面:

  1. FrameworkSQLiteDatabase费劲巴拉地设计出“延迟绑定”的机制,目的就是为了“类型安全”,如果按照SimpleSQLiteQuery的做法,把所有绑定参数放入Object[]中,那还不如把绑定参数全转换为String类型,然后直接提供给rawQueryWithFactory方法。
  2. 把绑定参数全部放入Object[]中是有性能损耗的,因为需要装箱拆箱操作。
  3. 查询可以说是数据库最重要最常用的操作了,在一个应用中“增删改”操作加在一起可能也没有查询操作多,这么多的查询操作,如果每一个都要生成一个SimpleSQLiteQuery对象,每个SimpleSQLiteQuery对象再包含若干个绑定参数,每个参数的存取又都需要装箱拆箱的话,这对性能的影响是不容忽视的。

综上所述,比较好的SupportSQLiteQuery的实现应该是:

  1. 绑定参数按照其类型存储,避免不必要的装箱拆箱操作。
  2. 查询操作虽然比较多,但不见得都会用到,SupportSQLiteQuery的对象应该是“懒加载”的,最好SupportSQLiteQuery对象可以复用。

如你所想,RoomSQLiteQuery就是这么实现的。

public class RoomSQLiteQuery implements SupportSQLiteQuery, SupportSQLiteProgram {
    
    private volatile String mQuery;

    //绑定参数按照其类型存储,避免装箱拆箱
    final long[] mLongBindings;
    final double[] mDoubleBindings;
    final String[] mStringBindings;
    final byte[][] mBlobBindings;

    //记录了对应位置的参数类型
    @Binding
    private final int[] mBindingTypes;
    
    final int mCapacity;
    // number of arguments in the query
    int mArgCount;

    //回收复用池,按照绑定参数的个数进行分类
    static final TreeMap<Integer, RoomSQLiteQuery> sQueryPool = new TreeMap<>();

    /**
     * 获取 RoomSQLiteQuery,按照绑定参数的个数来,可能会复用之前的 RoomSQLiteQuery
     */
    public static RoomSQLiteQuery acquire(String query, int argumentCount) {
        synchronized (sQueryPool) {
            //关键操作,取sQueryPool的ceilingEntry
            //ceiling是天花板的意思,ceilingEntry就是取不小于argumentCount的Entity
            final Map.Entry<Integer, RoomSQLiteQuery> entry =
                    sQueryPool.ceilingEntry(argumentCount);
            if (entry != null) {
                //如果存在会把它从sQueryPool,这样就不会被别的查询用到了
                sQueryPool.remove(entry.getKey());
                final RoomSQLiteQuery sqliteQuery = entry.getValue();
                //对它进行初始化
                sqliteQuery.init(query, argumentCount);
                return sqliteQuery;
            }
        }
        //没有可复用的,则新建一个
        RoomSQLiteQuery sqLiteQuery = new RoomSQLiteQuery(argumentCount);
        sqLiteQuery.init(query, argumentCount);
        return sqLiteQuery;
    }

    private RoomSQLiteQuery(int capacity) {
        mCapacity = capacity;
        // SQL的参数绑定,位置是从1开始计数的,不是从0
        int limit = capacity + 1;
        //noinspection WrongConstant
        mBindingTypes = new int[limit];
        mLongBindings = new long[limit];
        mDoubleBindings = new double[limit];
        mStringBindings = new String[limit];
        mBlobBindings = new byte[limit][];
    }

    void init(String query, int argCount) {
        mQuery = query;
        mArgCount = argCount;
    }

    /**
     * 释放掉 query,让其回到sQueryPool,以便复用
     */
    public void release() {
        synchronized (sQueryPool) {
            sQueryPool.put(mCapacity, this);
            prunePoolLocked();
        }
    }

    //sQueryPool如果超过15个,会被修剪到10个
    private static void prunePoolLocked() {
        if (sQueryPool.size() > POOL_LIMIT) {
            int toBeRemoved = sQueryPool.size() - DESIRED_POOL_SIZE;
            final Iterator<Integer> iterator = sQueryPool.descendingKeySet().iterator();
            while (toBeRemoved-- > 0) {
                iterator.next();
                iterator.remove();
            }
        }
    }

    @Override
    public String getSql() {
        return mQuery;
    }

    @Override
    public int getArgCount() {
        return mArgCount;
    }

    //根据 mBindingTypes中记录的类型,进行相应的绑定
    @Override
    public void bindTo(SupportSQLiteProgram program) {
        for (int index = 1; index <= mArgCount; index++) {
            switch (mBindingTypes[index]) {
                case NULL:
                    program.bindNull(index);
                    break;
                case LONG:
                    program.bindLong(index, mLongBindings[index]);
                    break;
                case DOUBLE:
                    program.bindDouble(index, mDoubleBindings[index]);
                    break;
                case STRING:
                    program.bindString(index, mStringBindings[index]);
                    break;
                case BLOB:
                    program.bindBlob(index, mBlobBindings[index]);
                    break;
            }
        }
    }

    @Override
    public void bindNull(int index) {
        mBindingTypes[index] = NULL;
    }

    //记录对应位置的参数类型,并且保存相应参数
    @Override
    public void bindLong(int index, long value) {
        mBindingTypes[index] = LONG;
        mLongBindings[index] = value;
    }

    @Override
    public void bindDouble(int index, double value) {
        mBindingTypes[index] = DOUBLE;
        mDoubleBindings[index] = value;
    }

    @Override
    public void bindString(int index, String value) {
        mBindingTypes[index] = STRING;
        mStringBindings[index] = value;
    }

    @Override
    public void bindBlob(int index, byte[] value) {
        mBindingTypes[index] = BLOB;
        mBlobBindings[index] = value;
    }

    private static final int NULL = 1;
    private static final int LONG = 2;
    private static final int DOUBLE = 3;
    private static final int STRING = 4;
    private static final int BLOB = 5;

    //参数类型标记
    @Retention(RetentionPolicy.SOURCE)
    @IntDef({NULL, LONG, DOUBLE, STRING, BLOB})
    @interface Binding {
    }
}

RoomSQLiteQuerySupportSQLiteQuery的实现类,是Room进行数据库查询真正用到的类。它的实现主要包含如下内容:

  1. 按照绑定参数的个数分配相应存储空间,以便按照参数的类型保存参数。
  2. RoomSQLiteQuery中包含有回收复用池,我们通过acquire方法获取一个RoomSQLiteQuery对象,再使用完毕后使用release方法释放,该对象就会回到回收复用池。在这两个方法之间,RoomSQLiteQuery对象已经脱离了回收复用池,不会被“打扰”。
  3. 回收复用池是按照RoomSQLiteQuery的绑定参数个数来分类的(TreeMap),从回收复用池中拿对象,会通过ceilingEntity方法去取,保证给我们一个不小于绑定参数个数argumentCount的对象(如果有的话),这样既保证了RoomSQLiteQuery对象的正常使用,又提高了复用的效率。
  4. 诸如bindLong等方法,会在获取到RoomSQLiteQuery对象后被调用(生成的代码),这些方法会记录对应位置的参数类型并保存相应参数,在真正需要绑定参数(bindTo方法被调用)时,根据这些记录的信息进行绑定。

查询SQL之所以可以被复用的原因在于,对于RoomSQLiteQuery而言,SQL语句是什么并不重要,重要的是绑定参数要有足够的空间去存储(getArgCount决定需要多大空间),getSql返回的SQL语句和bindTo时绑定参数是对应的,就可以了。

5. Dao实现

以上所说都是Dao实现“增删改查”的基础,有了这些基础,再来看Dao的实现是很简单的。假设我们的Dao定义如下:

@Entity
data class User(
    @PrimaryKey val uid: Int,
    @ColumnInfo(name = "first_name") val firstName: String?,
    @ColumnInfo(name = "last_name") val lastName: String?
)

@Dao
interface UserDao {
    @Query("SELECT * FROM user WHERE uid = :userId")
    fun getUserById(userId: Int): User?

    @Insert
    fun insert(user: User)

    @Delete
    fun delete(user: User)

    @Update
    fun update(user: User)
}

abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao
}

那么生成的代码如下:

public final class AppDatabase_Impl extends AppDatabase {
    private volatile UserDao _userDao;
    
    //...
    
    @Override
    public UserDao userDao() {
        if (_userDao != null) {
            return _userDao;
        } else {
            synchronized(this) {
                if(_userDao == null) {
                    _userDao = new UserDao_Impl(this);
                }
                return _userDao;
            }
        }
    }
}

public final class UserDao_Impl implements UserDao {
  private final RoomDatabase __db;

  private final EntityInsertionAdapter __insertionAdapterOfUser;

  private final EntityDeletionOrUpdateAdapter __deletionAdapterOfUser;

  private final EntityDeletionOrUpdateAdapter __updateAdapterOfUser;

  public UserDao_Impl(RoomDatabase __db) {
    this.__db = __db;
    this.__insertionAdapterOfUser = new EntityInsertionAdapter<User>(__db) {
      @Override
      public String createQuery() {
        //SQL语句帮我们生成
        return "INSERT OR ABORT INTO `User`(`uid`,`first_name`,`last_name`) VALUES (?,?,?)";
      }

      @Override
      public void bind(SupportSQLiteStatement stmt, User value) {
        //绑定帮我们做好
        stmt.bindLong(1, value.getUid());
        if (value.getFirstName() == null) {
          stmt.bindNull(2);
        } else {
          stmt.bindString(2, value.getFirstName());
        }
        if (value.getLastName() == null) {
          stmt.bindNull(3);
        } else {
          stmt.bindString(3, value.getLastName());
        }
      }
    };
    
    //__deletionAdapterOfUser, __updateAdapterOfUser是类似的
  }

  @Override
  public void insert(final User user) {
    __db.assertNotSuspendingTransaction();
    __db.beginTransaction();
    try {
      //插入调用EntityInsertionAdapter上的insert方法
      __insertionAdapterOfUser.insert(user);
      __db.setTransactionSuccessful();
    } finally {
      __db.endTransaction();
    }
  }

  @Override
  public void delete(final User user) {
    __db.assertNotSuspendingTransaction();
    __db.beginTransaction();
    try {
      //删除和更新调用EntityDeletionOrUpdateAdapter上handle方法
      __deletionAdapterOfUser.handle(user);
      __db.setTransactionSuccessful();
    } finally {
      __db.endTransaction();
    }
  }

  @Override
  public void update(final User user) {
    __db.assertNotSuspendingTransaction();
    __db.beginTransaction();
    try {
      //删除和更新调用EntityDeletionOrUpdateAdapter上handle方法
      __updateAdapterOfUser.handle(user);
      __db.setTransactionSuccessful();
    } finally {
      __db.endTransaction();
    }
  }

  @Override
  public User getUserById(final int userId) {
    final String _sql = "SELECT * FROM user WHERE uid = ?";
    //获取RoomSQLiteQuery对象
    final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 1);
    int _argIndex = 1;
    //绑定参数,这里不是最终的参数绑定,只是要记录对应位置的参数类型及参数本身,方便之后真正的参数绑定
    _statement.bindLong(_argIndex, userId);
    __db.assertNotSuspendingTransaction();
    //执行查询拿到Cursor
    final Cursor _cursor = DBUtil.query(__db, _statement, false);
    //把Cursor数据转换成Entity对象
    try {
      final int _cursorIndexOfUid = CursorUtil.getColumnIndexOrThrow(_cursor, "uid");
      final int _cursorIndexOfFirstName = CursorUtil.getColumnIndexOrThrow(_cursor, "first_name");
      final int _cursorIndexOfLastName = CursorUtil.getColumnIndexOrThrow(_cursor, "last_name");
      final User _result;
      if(_cursor.moveToFirst()) {
        final int _tmpUid;
        _tmpUid = _cursor.getInt(_cursorIndexOfUid);
        final String _tmpFirstName;
        _tmpFirstName = _cursor.getString(_cursorIndexOfFirstName);
        final String _tmpLastName;
        _tmpLastName = _cursor.getString(_cursorIndexOfLastName);
        _result = new User(_tmpUid,_tmpFirstName,_tmpLastName);
      } else {
        _result = null;
      }
      return _result;
    } finally {
      _cursor.close();
      //释放RoomSQLiteQuery对象(回到回收复用池)
      _statement.release();
    }
  }
}

虽然生成的代码很长,但是都是些套路,也说明这些代码确实适合用注解处理器生成。Dao的实现只是对之前分析内容的运用,没有什么实质性的内容。

6. 总结

Room对于“增删改查”实现的关键在于“效率”,针对增删改查不同的执行方式,使用了不同的复用机制。“增删改”复用的核心在于,针对特定的Entity(例如User),“增删改”各自的SQL语句是固定的,绑定参数是固定的,使用SharedSQLiteStatement的方式,只保留一个Statement进行复用是很合理的;查询复用的核心在于,“延迟绑定”+绑定参数存储空间的复用,Room中所有的查询共享一个查询复用池,只在查询真正发生时才从复用池中取,因为查询操作是“最多”的,使用查询复用池大大提升了查询效率。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343

推荐阅读更多精彩内容