美文网首页
稿-Android SQL你用对了吗

稿-Android SQL你用对了吗

作者: 罗力 | 来源:发表于2016-10-28 02:16 被阅读463次

SQLiteDatabase这个类,大家都不陌生。其中:

增删改查,有三个接口需要进行条件限定。限定条件通过参数String whereClause, String[] whereArgs来指定。

whereClause的值形如_id = ? AND condition1 >= ? OR condition2 != ?,其中的?用于参数绑定,按顺序,填入whereArgs数组内。

但说实话,使用这种方式,需要先将限定部分的SQL语句写出来,将限定的参数替换为?,然后记住次序,填入数组内。一次还好,写多了挺烦人的,如果是修改的话,还需要仔细确保SQL语句书写正确,确保修改不会弄错参数顺序。

为了方便,有的同学们就直接放弃了whereClausewhereArgs这种搭配,直接传入完整的SQL限定字符串作为whereClause参数的值,在whereArgs参数传入了null

这种用法同样能达成我们的需求,为什么SDK要搞得这么复杂呢?答:一切都是为了性能。

让我们来看下源码里是怎么处理的:

int delete(String table, String whereClause, String[] whereArgs)这个方法为切入点,相关实现在SQLiteDatabase.java里。

    /**
     * Convenience method for deleting rows in the database.
     *
     * @param table the table to delete from
     * @param whereClause the optional WHERE clause to apply when deleting.
     *            Passing null will delete all rows.
     * @param whereArgs You may include ?s in the where clause, which
     *            will be replaced by the values from whereArgs. The values
     *            will be bound as Strings.
     * @return the number of rows affected if a whereClause is passed in, 0
     *         otherwise. To remove all rows and get a count pass "1" as the
     *         whereClause.
     */
    public int delete(String table, String whereClause, String[] whereArgs) {
        acquireReference();
        try {
            // 组装成完整的SQL语句,实例化SQLiteStatement
            SQLiteStatement statement =  new SQLiteStatement(this, "DELETE FROM " + table +
                    (!TextUtils.isEmpty(whereClause) ? " WHERE " + whereClause : ""), whereArgs);
            try {
                // 执行SQL语句
                return statement.executeUpdateDelete();
            } finally {
                statement.close();
            }
        } finally {
            releaseReference();
        }
    }

源码里并没做什么神奇的事情,仅仅是组装成完整的SQL语句,和参数数组一起,实例化SQLiteStatement,然后执行这个语句。

执行的过程实现在SQLiteStatement.java

    /**
     * Execute this SQL statement, if the the number of rows affected by execution of this SQL
     * statement is of any importance to the caller - for example, UPDATE / DELETE SQL statements.
     *
     * @return the number of rows affected by this SQL statement execution.
     * @throws android.database.SQLException If the SQL string is invalid for
     *         some reason
     */
    public int executeUpdateDelete() {
        acquireReference();
        try {
            // 获取各个参数:sql语句、要绑定的参数等
            // 然后才是真正的执行
            return getSession().executeForChangedRowCount(
                    getSql(), getBindArgs(), getConnectionFlags(), null);
        } catch (SQLiteDatabaseCorruptException ex) {
            onCorruption();
            throw ex;
        } finally {
            releaseReference();
        }
    }

获取参数,然后调用executeForChangedRowCount方法。这个方法在SQLiteSession.java


    /**
     * Executes a statement that returns a count of the number of rows
     * that were changed.  Use for UPDATE or DELETE SQL statements.
     *
     * @param sql The SQL statement to execute.
     * @param bindArgs The arguments to bind, or null if none.
     * @param connectionFlags The connection flags to use if a connection must be
     * acquired by this operation.  Refer to {@link SQLiteConnectionPool}.
     * @param cancellationSignal A signal to cancel the operation in progress, or null if none.
     * @return The number of rows that were changed.
     *
     * @throws SQLiteException if an error occurs, such as a syntax error
     * or invalid number of bind arguments.
     * @throws OperationCanceledException if the operation was canceled.
     */
    public int executeForChangedRowCount(String sql, Object[] bindArgs, int connectionFlags,
            CancellationSignal cancellationSignal) {
        if (sql == null) {
            throw new IllegalArgumentException("sql must not be null.");
        }

        // 这里虽然传入了bindArgs,但并没用到
        if (executeSpecial(sql, bindArgs, connectionFlags, cancellationSignal)) {
            return 0;
        }

        acquireConnection(sql, connectionFlags, cancellationSignal); // might throw
        try {
            // 真正用到sql和bindArgs的地方
            return mConnection.executeForChangedRowCount(sql, bindArgs,
                    cancellationSignal); // might throw
        } finally {
            releaseConnection(); // might throw
        }
    }

Ok,继续深入,来到SQLiteConnection.java

    /**
     * Executes a statement that returns a count of the number of rows
     * that were changed.  Use for UPDATE or DELETE SQL statements.
     *
     * @param sql The SQL statement to execute.
     * @param bindArgs The arguments to bind, or null if none.
     * @param cancellationSignal A signal to cancel the operation in progress, or null if none.
     * @return The number of rows that were changed.
     *
     * @throws SQLiteException if an error occurs, such as a syntax error
     * or invalid number of bind arguments.
     * @throws OperationCanceledException if the operation was canceled.
     */
    public int executeForChangedRowCount(String sql, Object[] bindArgs,
            CancellationSignal cancellationSignal) {
        if (sql == null) {
            throw new IllegalArgumentException("sql must not be null.");
        }

        int changedRows = 0;
        final int cookie = mRecentOperations.beginOperation("executeForChangedRowCount",
                sql, bindArgs);
        try {
            // 获取预先编译过的SQL
            final PreparedStatement statement = acquirePreparedStatement(sql);
            try {
                throwIfStatementForbidden(statement);
                // 参数绑定
                bindArguments(statement, bindArgs);
                applyBlockGuardPolicy(statement);
                attachCancellationSignal(cancellationSignal);
                try {
                    // 交给SQLiteEngine执行
                    changedRows = nativeExecuteForChangedRowCount(
                            mConnectionPtr, statement.mStatementPtr);
                    return changedRows;
                } finally {
                    detachCancellationSignal(cancellationSignal);
                }
            } finally {
                releasePreparedStatement(statement);
            }
        } catch (RuntimeException ex) {
            mRecentOperations.failOperation(cookie, ex);
            throw ex;
        } finally {
            if (mRecentOperations.endOperationDeferLog(cookie)) {
                mRecentOperations.logOperation(cookie, "changedRows=" + changedRows);
            }
        }
    }

首先,会通过acquirePreparedStatement去获取PreparedStatement实例,源码如下:

    private PreparedStatement acquirePreparedStatement(String sql) {
        PreparedStatement statement = mPreparedStatementCache.get(sql);
        boolean skipCache = false;
        if (statement != null) {
            if (!statement.mInUse) {
                return statement;
            }
            // The statement is already in the cache but is in use (this statement appears
            // to be not only re-entrant but recursive!).  So prepare a new copy of the
            // statement but do not cache it.
            skipCache = true;
        }

        final long statementPtr = nativePrepareStatement(mConnectionPtr, sql);
        try {
            final int numParameters = nativeGetParameterCount(mConnectionPtr, statementPtr);
            final int type = DatabaseUtils.getSqlStatementType(sql);
            final boolean readOnly = nativeIsReadOnly(mConnectionPtr, statementPtr);
            statement = obtainPreparedStatement(sql, statementPtr, numParameters, type, readOnly);
            if (!skipCache && isCacheable(type)) {
                mPreparedStatementCache.put(sql, statement);
                statement.mInCache = true;
            }
        } catch (RuntimeException ex) {
            // Finalize the statement if an exception occurred and we did not add
            // it to the cache.  If it is already in the cache, then leave it there.
            if (statement == null || !statement.mInCache) {
                nativeFinalizeStatement(mConnectionPtr, statementPtr);
            }
            throw ex;
        }
        statement.mInUse = true;
        return statement;
    }

可以看到,这里有个mPreparedStatementCache用于缓存之前生成过的PreparedStatement,如果之前有相同的SQL语句,则取出重用,避免重复编译SQL。这个缓存本质上是一个LruCache<String, PreparedStatement>key为sql语句。

也即是,如果我们使用whereClausewhereArgs的方式操作数据库的话,同样的whereClause,不同的whereArgs取值,将能利用到这个缓存。但如果直接将限定语句拼接好,通常情况下,参数取值会改变,已有的缓存就派不上用场,白白浪费了已有的PreparedStatement缓存。

顺便贴下绑定参数的代码:


    private void bindArguments(PreparedStatement statement, Object[] bindArgs) {
        final int count = bindArgs != null ? bindArgs.length : 0;
        if (count != statement.mNumParameters) {
            throw new SQLiteBindOrColumnIndexOutOfRangeException(
                    "Expected " + statement.mNumParameters + " bind arguments but "
                    + count + " were provided.");
        }
        if (count == 0) {
            return;
        }

        final long statementPtr = statement.mStatementPtr;
        for (int i = 0; i < count; i++) {
            final Object arg = bindArgs[i];
            switch (DatabaseUtils.getTypeOfObject(arg)) {
                case Cursor.FIELD_TYPE_NULL:
                    nativeBindNull(mConnectionPtr, statementPtr, i + 1);
                    break;
                case Cursor.FIELD_TYPE_INTEGER:
                    nativeBindLong(mConnectionPtr, statementPtr, i + 1,
                            ((Number)arg).longValue());
                    break;
                case Cursor.FIELD_TYPE_FLOAT:
                    nativeBindDouble(mConnectionPtr, statementPtr, i + 1,
                            ((Number)arg).doubleValue());
                    break;
                case Cursor.FIELD_TYPE_BLOB:
                    nativeBindBlob(mConnectionPtr, statementPtr, i + 1, (byte[])arg);
                    break;
                case Cursor.FIELD_TYPE_STRING:
                default:
                    if (arg instanceof Boolean) {
                        // Provide compatibility with legacy applications which may pass
                        // Boolean values in bind args.
                        nativeBindLong(mConnectionPtr, statementPtr, i + 1,
                                ((Boolean)arg).booleanValue() ? 1 : 0);
                    } else {
                        nativeBindString(mConnectionPtr, statementPtr, i + 1, arg.toString());
                    }
                    break;
            }
        }
    }

代码很简单,就不多解释了。

啰啰嗦嗦贴了这么多源码,其实只是为了证明,whereClause搭配whereArgs是很有意义的。为了性能考虑,写代码的时候不要再用拼接字符串的方式直接生成限定语句了。

但,最开始提及的那种不便的使用方式,难道就只能默默忍受了?答案显然并不是,通过简单的抽象、封装,能够实现如下的效果:

Statement statement =
        Statement.where(UPDATE_TIME).lessOrEqual(now)
        .and(EXPIRY_TIME).moreThan(now)
        .or(AGE).eq(23)
        .end();
statement.sql(); // 生成sql语句
statement.whereClause(); // 生成whereClause语句
statement.args(); // 对应的参数数组

这是我尝试造的一个轮子,用于通过语义化的方式,定义和生成whereClausewhereArgs。用起来就像是写sql语句一样自然,同时还能避免人工书写sql语句导致的一些拼写错误,生成的whereClause的参数顺序也和whereArgs参数数组严格对应。

找时间整理下,分享到Gayhub供大家批判下。哈哈。

相关文章

  • 稿-Android SQL你用对了吗

    SQLiteDatabase这个类,大家都不陌生。其中: [int delete(String table, St...

  • 11期 | 实践-一个Android小布局引发的思考

    你确定你对Android的布局精通了吗?如果你能立马用LinearLayout,RelativeLayout,Co...

  • 你用对牙膏了吗?

    牙膏作为日常生活常用的清洁用品,有着很悠久的历史。随着科学技术的不断发展,工艺装备的不断改进和完善,各种类型的牙膏...

  • 你用对思维了吗?

    我们知道大学学的是思维、方法,而不是知识。或许毕业后很对知识都忘光了,可是思考问题的维度以及采取解决问题的方法依旧...

  • 你用对synchronized了吗

    最近遇到一个crash问题,是关于线程同步锁的,检查代码的时候发现方法已经使用synchronized同步了,为什...

  • 你用对逻辑了吗?

    人类究竟是怎么了,竟会把胡言乱语也当成妙笔生花?——塔勒布 先说一个看起来不太相关,实际非常值得深思的话题。 19...

  • SQL稿

  • 你用对遮瑕了吗?

    平时我们都会用遮瑕去遮我们的痘痘,还有黑眼圈。还有毛孔,目的就是让我们的皮肤看起来更加细腻,但是有时候我们画太多就...

  • 性能优化与保活

    ------内存泄漏优化------Android 内存优化你的 Handler 内存泄露 了吗?Android卡...

  • 你的线稿过关了吗丨子绘娱

    你的线稿过关了吗丨子绘娱 “当你点下关注的那一刻,就进入我们的故事了” SKETCH 你的线稿过关了吗? 众所周知...

网友评论

      本文标题:稿-Android SQL你用对了吗

      本文链接:https://www.haomeiwen.com/subject/niisuttx.html