美文网首页
Android编程权威指南(第二版)学习笔记(十四)—— 第14

Android编程权威指南(第二版)学习笔记(十四)—— 第14

作者: kniost | 来源:发表于2017-05-17 15:31 被阅读20次

    本章主要讲了如何使用 SQLite 进行持久化存储,包含了 CRUD 四个操作,使用基础的 SQLiteOpenHelper 与 Cursor 构造程序

    GitHub 地址:
    完成14章所有内容


    为什么要用数据库而不是文本文件来存储数据呢?
    因为如果用普通的文本文件(比如 txt)存储数据,每次读取时都要读取整个文件的内容,完成修改后再全部保存,一旦数据量较大,将十分耗费时间和资源。

    SQLite是类似于MySQL的开源关系型数据库。不同于其他数据库的是,SQLite使用单个文件存储数据,使用SQLite库读取数据。Android标准库包含SQLite库以及配套的一些Java辅助类。

    1. 定义 Schema(架构)

    本应用需要保存的应该是一个 Crime 的全部数据,比如一个表格可以下表这样。

    _id uuid title date solved
    1 13090624138324 Stolen yougurt 13090636733242 0
    2 13090274859392 Dirty sink 13090732131909 1

    程序员的一个目标,或者说信条,就是“不要重复造轮子”,也就是说,多花时间思考复用代码的编写和调用,避免在应用中到处使用重复代码。
    基于上述准则,我们可以使用能统一定义模型层对象的高级 ORM(对象关系映射)工具,不过对于本章代码来说,因为需要掌握更加基础的内容,将会自己实现数据库操作。

    首先创建一个 Package,名为 database, 然后定义数据架构类 CrimeDbSchema.java

    // CrimeDbSchema.java
    public class CrimeDbSchema {
        public static final class CrimeTable {
            public static final String NAME = "crimes";
    
            public static final class Cols {
                public static final String UUID = "uuid";
                public static final String TITLE = "title";
                public static final String DATE = "date";
                public static final String SOLVED = "solved";
            }
        }
    }
    

    这里有一个小 Tip,Android Studio 中有输入一长串前缀的 Live Template,比如要输入public static final String,只需要打 psfs 即可。

    2. 初始创建数据库

    一般来说,在实际开发中,打开一个数据库之前,由于不知道其是否存在,是否有更新,所以要经过如下步骤:

    1. 确认目标数据库是否存在。
    2. 如果不存在,首先创建数据库,然后创建数据库表以及必需的初始化数据。
    3. 如果存在,打开并确认数据库架构是否为最新版本
    4. 如果是旧版,就运行相关代码升级到最新版本

    在 Android 中,提供了一个SQLiteOpenHelper类用于处理这些打开数据库时繁杂的工作。我们可以创建一个SQLiteOpenHelper的子类用于对自己的数据库进行处理,比如:

    public class CrimeBaseHelper extends SQLiteOpenHelper {
        public static final int VERSION = 1;
        public static final String DATABASE_NAME = "crimeBase.db";
    
        public CrimeBaseHelper(Context context) {
            super(context, DATABASE_NAME, null, VERSION);
        }
    
        // 如果数据库不存在,就调用该函数创建一个数据库
        @Override
        public void onCreate(SQLiteDatabase db) {
             // 一定要注意语句之间的空格,因为语句是一个字符串
             // 如果没有空格,对于 SQL 来说,就是无意义的语句
             // 比如下面的 table 后面一定要接一个空格
            db.execSQL("create table " + CrimeTable.NAME + "(" +
                    " _id integer primary key autoincrement, " +
                    CrimeTable.Cols.UUID + ", " +
                    CrimeTable.Cols.TITLE + ", " +
                    CrimeTable.Cols.DATE + ", " +
                    CrimeTable.Cols.SOLVED +
                    ")"
            );
        }
    
        // 如果版本升级了,就调用 onUpgrade() 函数
        @Override
        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    
        }
    }
    
    

    由于数据库结构调整太麻烦,在实际开发中最好的做法应该是直接删除数据库文件(最方便的就是卸载程序) :)

    3. 打开数据库

    然后在 Model 层(在这里是 CrimeLab 单例)中打开数据库

    mContext = context.getApplicationContext();
    mDataBase = new CrimeBaseHelper(mContext)
            .getWritableDatabase();
    

    4. 写入数据库(插入与更新)

    4.1 使用 ContentValues

    负责处理数据库写入和更新操作的辅助类是 ContentValues 类。它是个键值存储类,类似于 Java 的 HashMap(查看源码可以发现它就是一个 HashMap)和前面用过的 Bundle。不同的是, ContentValues 专门用于处理SQLite数据。我们需要一个 Model 层的方法,把 Model 层数据转换为ContentValues,由于在 Model 层外,对数据的操作应该只有对 Model 对象的操作,所以这个方法应该是私有的。示例如下:

    private static ContentValues getContentValues(Crime crime) {
        ContentValues values = new ContentValues();
        values.put(Cols.UUID, crime.getId().toString());
        values.put(Cols.TITLE, crime.getTitle());
        values.put(Cols.DATE, crime.getDate().getTime());
        values.put(Cols.SOLVED, crime.isSolved() ? 1 : 0);
        
        return values;
    }
    

    4.2 插入和更新记录

    4.2.1 插入记录

    准备好了 ContentValues,就可以进行写入数据了,调用SQLiteDatabase.insert(…)方法即可。

    public void addCrime(Crime c) {
        ContentValues values = getContentValues(c);
        /**
         * SQLiteDatabase.insert(String table, String nullColumnHack, ContentValues values)
         * 第一个参数是表名,第三个参数是键值对
         * 第二个参数则是当 values 为全空时插入空行
         * 如果设为 null,则 values 为全空时不插入空行
         */
        mDatabase.insert(CrimeTable.NAME, null, values);
    }
    

    4.2.2 更新记录

    更新记录使用的是SQLiteDatabase.update(…)方法。

    public void updateCrime(Crime crime) {
        String uuidString = crime.getId().toString();
        ContentValues values = getContentValues(crime);
    
        mDatabase.update(CrimeTable.NAME, values,
                Cols.UUID + "=?",
                new String[] {uuidString});
    }
    

    update 方法的原型是:

    SQLiteDatabase.update(String table, // 表名
                    ContentValues values, // 键值对
                    // where 后面接的语句,一般是 "columnName = ?"
                    String whereClause, 
                    // whereClause 中 ? 代表的语句,可以有多个
                    String[] whereArgs
    );
    

    可以看到,实际上后面两个参数的意思就是 where columnName = columnValue,那么为什么要留出两个参数而不用一个参数解决呢?

    这样做是为了防范 SQL 脚本注入,因为 String 如果本身就带了 SQL 语句,如果不加处理放进数据库执行,就有可能造成灾难性的后果(比如直接 drop 掉所有的表)

    5. 读取数据库

    读取数据库用到的是 query() 方法,这个方法有许多个重载版本,我们使用下面的版本:

    public Cursor query(
        String table,           // 表名
        String[] columns,       // 选中的列名,为 null 时选中所有列
        String where,           // where 语句
        String[] whereArgs,     // where 语句的参数
        String groupBy,         // 分组
        String having,          // 与合计函数一起使用的 having
        String orderBy,         // 顺序
        String limit)           // 限制数量
    

    可以看到返回的是一个 Cursor 对象,下面来探究一下 Cursor 对象

    5.1 Cursor 与 CursorWrapper

    Cursor 是个神奇的表数据处理工具,其任务就是封装数据表中的原始字段值。从Cursor获取数据的代码大致如下所示:

    String uuidString = cursor.getString(cursor.getColumnIndex(CrimeTable.Cols.UUID));
    

    每次要取出一条记录中的一列,都要重复写一次上述代码,所以我们使用 CursorWrapper 建立一个 Cursor 的子类,在其中封装可以转换对象的方法。
    比如一个类可以这么写:

    public class CrimeCursorWrapper extends CursorWrapper {
        public CrimeCursorWrapper(Cursor cursor) {
            super(cursor);
        }
    
        public Crime getCrime() {
            //这里是从得到的 CursorWrapper 中取出数据
            String uuidString = getString(getColumnIndex(Cols.UUID));
            String title = getString(getColumnIndex(Cols.TITLE));
            long date = getLong(getColumnIndex(Cols.DATE));
            int isSolved = getInt(getColumnIndex(Cols.SOLVED));
            
            // 然后生成一个 Model 层对象返回,免去了重复写的繁琐
            Crime crime = new Crime(UUID.fromString(uuidString));
            crime.setTitle(title);
            crime.setDate(new Date(date));
            crime.setSolved(isSolved != 0);
    
            return crime;
        }
    }
    

    5.2 创建 Model 层对象

    首先封装一个数据库查询方法,返回的是自定义的 CrimeCursorWrapper 对象。

    private CrimeCursorWrapper queryCrimes(String whereClause, String[] whereArgs) {
        Cursor cursor = mDatabase.query(
                CrimeTable.NAME,
                null, // Columns -- use null to select all columns
                whereClause,
                whereArgs,
                null,
                null,
                null
        );
        return new CrimeCursorWrapper(cursor);
    }
    

    再从 CursorWrapper 中获取数据

    // 获取所有数据
    public List<Crime> getCrimes() {
        List<Crime> crimes = new ArrayList<>();
    
        CrimeCursorWrapper cursor = queryCrimes(null, null);
        try {
            cursor.moveToFirst();
            while (!cursor.isAfterLast()) {
                crimes.add(cursor.getCrime());
                cursor.moveToNext();
            }
        } finally {
            cursor.close();
        }
    
        return crimes;
    }
    //获取单个记录
    public Crime getCrime(UUID id) {
        CrimeCursorWrapper cursor = queryCrimes(
                Cols.UUID + " = ?",
                new String[] { id.toString() }
        );
    
        try {
            if (cursor.getCount() == 0) {
                return null;
            }
    
            cursor.moveToFirst();
            return cursor.getCrime();
        } finally {
            cursor.close();
        }
    }
    

    6 Application Context(应用上下文)

    前面,我们在CrimeLab的构造方法中使用了Application Context。

    private CrimeLab(Context context) {
        mContext = context.getApplicationContext();
        ...
    }
    

    Application Context 有什么特别呢?就上例来看,为什么要用 Application Context ,而不直接用activity 作为 context 呢?

    要回答上述问题,关键就在于考虑它们的生命周期。只要有 activity 在,Android 肯定也创建 有 application 对象。用户在应用的不同界面间导航时,各个 activity 时而存在时而消亡,但 application 对象不会受任何影响。可以说,它的生命周期要比任何 activity 都要长。

    CrimeLab 是个单例。这表明,一旦创建,它就会一直存在直至整个应用进程被销毁。由代码可知, CrimeLab 引用着 mContext 对象。显然,如果把 activity 作为 mContext 对象保存的话,这个由 CrimeLab 一直引用着的 activity 肯定会免遭垃圾回收器的清理,即便用户跳转离开这个 activity 时也是如此。

    为了避免资源浪费,我们使用了应用程序上下文。这样, CrimeLab 仍可以引用 Context 对象, 而 activity 的存在和消亡也不用受它束缚了。

    7 挑战练习:删除 Crime 记录

    在上一章中,我们为 CrimeFragment 的 ToolBar 中添加了删除按钮,这一章我们改用数据库来存储数据,需要对代码进行相应的改写:

    // CrimeLab.java
    public void deleteCrime(Crime c) {
        mDatabase.delete(
                CrimeTable.NAME,
                Cols.UUID + " = ?",
                new String[] {c.getId().toString()}
        );
    }
    
    // CrimeFragment.java
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case R.id.menu_item_delete_crime:
                CrimeLab.get(getActivity()).deleteCrime(mCrime);
                getActivity().finish();
                return true;
            default:
                return super.onOptionsItemSelected(item);
        }
    }
    

    GitHub Page: kniost.github.io
    简书:http://www.jianshu.com/u/723da691aa42

    相关文章

      网友评论

          本文标题:Android编程权威指南(第二版)学习笔记(十四)—— 第14

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