《第一行代码》读书笔记(七)----持久化(下)

SQLite 数据库存储

SQLite 是一款轻量级的关系型数据库, 运算速度非常快, 占用资源很少, 支持标准的SQL语法, 遵循ACID事务.

创建数据库

Android 提供了一个 SQLiteOpenHelper 帮助类, 可以对数据库进行创建和升级. 这个类是抽象类, 我们要创建自己的类去继承它. 有两个抽象方法, onCreate()onUpdate(). 还有两个很重要的实例方法, getReadableDatabase()getWritableDatabase(), 两个方法都可以创建或者打开一个现有的数据库, 返回一个可对数据库进行读写操作的对象. 不同在于, 当数据库不可写入时(比如磁盘空间已满), getReadableDatabase() 方法返回的对象将以只读的方式去打开数据库, 而getWritableDatabase() 方法则将出现异常. 这个类有两个可重写的构造方法, 一般使用参数少的那个就够用了. 四个参数, 第一个是Context对象, 第二个是数据库名, 第三个是在查询数据时候返回的一个自定义的 Cursor, 一般都传入null, 第四个是当前数据库的版本号. 创建的数据库文件存放在/data/data//databases/目录下.

新建一个项目, 希望创建一个名为 BookStore.db 的数据库, 并在这个数据库中新建一张表 BOOK.
新建 MyDatabaseHelper 类继承自 SQLiteOpenHelper :

public class MyDatabaseHelper extends SQLiteOpenHelper {

    public static final String CREATE_BOOK = "create table Book ("
            + "id integer primary key autoincrement, "
            + "author text, "
            + "price real, "
            + "pages integer, "
            + "name text) ";

    private Context mContext;

    public MyDatabaseHelper(Context context, String name,
                            SQLiteDatabase.CursorFactory factory, int version) {
        super(context, name, factory, version);
        mContext = context;
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(CREATE_BOOK);//执行建表语句
        Toast.makeText(mContext, "Create succeeded", Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {

    }
}

修改 activity_main.xml 代码, 定义一个按钮, 用来创建数据库.
最后, 修改 MainActivity 的代码:

public class MainActivity extends Activity {

    private MyDatabaseHelper dbHelper;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        dbHelper = new MyDatabaseHelper(this, "BookStore.db", null, 1);

        Button createDatabase = (Button) findViewById(R.id.create_database);
        createDatabase.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                dbHelper.getWritableDatabase();
            }
        });
    }
}

建好的数据库和表怎么查看呢? 用 adb shell. 首先配置好 adb 的环境变量(Path), 进入命令行, 输入 adb shell, 使用 cd 命令进入到 数据库文件的目录下(cd /data/data/me.zipstream.database/databases/), 使用 ls 命令查看该目录里的文件, 使用 sqlite3 数据库名 来打开数据库, 使用 .table 命令来查看数据库的表, 使用 .schema 命令查看它的建表语句, 使用 .exit 退出.

升级数据库

再添加一张 Category 表, 先在 MyDatabaseHelper 中添加建表语句:

public static final String CREATE_CATEGORY = "create table Category ("
        + "id integer primary key autoincrement, "
        + "category_name text, "
        + "category_code integer) ";

onCreate() 方法中添加执行语句的代码:

db.execSQL(CREATE_CATEGORY);

注意, 这时如果运行是不会成功的, 因为再添加一张表属于数据库的升级操作, 要在 onUpdate() 中添加:

public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    db.execSQL("drop table if exists Book");
    db.execSQL("drop table if exists Category");

    onCreate(db);
}

最后, 在 MainActivity 中修改数据库的版本号, 使 onUpdate() 方法得到执行:

dbHelper = new MyDatabaseHelper(this, "BookStore.db", null, 2);

添加数据

SQLiteOpenHelper 的 getReadableDatabase()getWritableDatabase() 方法都会返回一个 SQLiteDatabase 对象, 借助这个对象进行CRUD.
insert()方法: 接收三个参数, 第一个是表名, 第二个用于在未指定添加数据的情况下给某些可为空的列自动赋值NULL, 一般传入null即可, 第三个是 ContentValues 对象.

先在 activity_main.xml 中添加一个按钮, 用来添加数据.
然后, 修改 MainActivity 的代码, 给这个按钮添加逻辑:

Button addData = (Button) findViewById(R.id.add_data);
addData.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        SQLiteDatabase db = dbHelper.getWritableDatabase();
        ContentValues values = new ContentValues();

        //开始封装第一条数据
        values.put("name", "The Da Vinci Code");
        values.put("author", "Dan Brown");
        values.put("pages", 454);
        values.put("price", 16.96);

        db.insert("Book", null, values);//插入第一条数据

        //开始封装第二条数据
        values.put("name", "The Lost Symbol");
        values.put("author", "Dan Brown");
        values.put("pages", 510);
        values.put("price", 19.95);

        db.insert("Book", null, values);//插入第二条数据
    }
});

这样就成功向数据库添加了数据.

更新数据

updata()方法: 接收四个参数, 第一个是表名, 第二个是 ContentValues 对象, 第三个和第四个用于约束更新某一行或某几行的数据, 不指定就默认更新所有行. 其实第三个参数对应的是 SQL 语句中的where 部分, 表示去更新所有 name 等于 ? 的行, ? 是一个占位符, 通过第四个参数提供的一个字符串数据为第三个参数中的每个占位符指定相应的内容.
更新布局文件, 添加一个按钮, 用来更新数据.
修改MainActivity 代码, 添加点击事件:

Button updateData = (Button) findViewById(R.id.update_data);
updateData.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        SQLiteDatabase db = dbHelper.getWritableDatabase();
        ContentValues values = new ContentValues();
        values.put("price", 10.99);

        db.update("Book", values, "name=?", new String[]{"The Da Vinci Code"});
    }
});

删除数据

delete()方法: 接收三个参数, 第一个是表名, 第二个第三个用于约束某一行或某几行的数据.
先添加一个按钮, 然后 添加点击事件:

Button deleteData = (Button) findViewById(R.id.delete_data);
deleteData.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        SQLiteDatabase db = dbHelper.getWritableDatabase();

        db.delete("Book", "pages > ?", new String[]{"500"});
    }
});

查询数据

SQLiteDatabase 提供了一个 query() 方法, 用来查询数据库.
这个方法非常复杂, 最短的一个重载也有七个参数.
* 第一个参数, 表名.
* 第二个参数, 指定去查询哪几列.
* 第三第四个参数, 约束查询某一行或某几行的参数.
* 第五个参数, 指定需要去 group by 的列.
* 第六个参数, 对 group by 之后的数据进行进一步过滤.
* 第七个参数, 指定查询结果的排序方式.

使用 query() 方法, 返回的是一个 Cursor 对象, 查询到的所有数据都将从这个对象中取出.

在布局文件中添加一个按钮, 用来查询数据.
修改 MainActivity 的代码:

Button queryData = (Button) findViewById(R.id.query_data);
queryData.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        SQLiteDatabase db = dbHelper.getWritableDatabase();
        //查询表中所有的数据
        Cursor cursor = db.query("Book", null, null, null, null, null, null);

        if (cursor.moveToFirst()) {
            do {
                //遍历Cursor对象, 取出数据并打印
                String name = cursor.getString(cursor.getColumnIndex("name"));
                String author = cursor.getString(cursor.getColumnIndex("author"));
                int pages = cursor.getInt(cursor.getColumnIndex("pages"));
                double price = cursor.getDouble(cursor.getColumnIndex("price"));

                Log.d("MainActivity", "book name is " + name);
                Log.d("MainActivity", "book author is " + author);
                Log.d("MainActivity", "book pages is " + pages);
                Log.d("MainActivity", "book price is " + price);
            } while (cursor.moveToNext());
        }

        cursor.close();
    }
});

SQLite 数据库的最佳实践

使用事务

事务的特性可以保证一系列操作要么全部完成, 要么一个都不会完成.
修改 布局文件, 添加一个按钮, 用来替换数据.
然后, 修改 MainActivity 的代码:

Button replaceData = (Button) findViewById(R.id.replace_data);
replaceData.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        SQLiteDatabase db = dbHelper.getWritableDatabase();

        db.beginTransaction();//开启事务

        try {
            db.delete("Book", null, null);

            if (true) {
                //这里手动抛出一个异常, 让事务失败
                throw new NullPointerException();
            }

            ContentValues values = new ContentValues();
            values.put("name", "Game of Thrones");
            values.put("author", "George Martin");
            values.put("pages", 720);
            values.put("price", 20.85);

            db.insert("Book", null, values);

            db.setTransactionSuccessful();//事务已经执行成功
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            db.endTransaction();//结束事务
        }
    }
});

代码中手动抛出了一个异常, 但是由于事务的存在, 中途出现异常会导致事务执行失败, 此时旧数据是删除不掉的. 将手动抛出异常的代码删除, 就可以替换数据了.

升级数据库的最佳写法

如何保证升级数据库的时候数据不会丢失?
思想: 为每一个版本号赋予它各自改变的内容, 然后再 onUpdate() 方法中对当前数据库版本号进行判断, 再执行相应的改变就可以了.

模拟一个数据库升级的案例, 第一版数据库非常简单, 只需要创建一张 Book 表. MyDatabaseHelper 代码:

public class MyDatabaseHelper extends SQLiteOpenHelper {

    public static final String CREATE_BOOK = "create table Book ("
            + "id integer primary key autoincrement, "
            + "author text, "
            + "price real, "
            + "pages integer, "
            + "name text) ";

    public MyDatabaseHelper(Context context, String name,
                            SQLiteDatabase.CursorFactory factory, int version) {
        super(context, name, factory, version);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(CREATE_BOOK);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    }
}

过一段时间, 有了新的需求, 需要向数据库中添加一张 Category 表, 修改代码:

public class MyDatabaseHelper extends SQLiteOpenHelper {

    public static final String CREATE_BOOK = "create table Book ("
            + "id integer primary key autoincrement, "
            + "author text, "
            + "price real, "
            + "pages integer, "
            + "name text) ";
    public static final String CREATE_CATEGORY = "create table Category ("
            + "id integer primary key autoincrement, "
            + "category_name text, "
            + "category_code integer) ";

    public MyDatabaseHelper(Context context, String name,
                            SQLiteDatabase.CursorFactory factory, int version) {
        super(context, name, factory, version);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(CREATE_BOOK);
        db.execSQL(CREATE_CATEGORY);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        switch (oldVersion) {
            case 1:
                db.execSQL(CREATE_CATEGORY);
            default:
        }
    }
}

onUpdate() 方法里添加了一个 switch 判断, 如果当前数据库版本号是1, 则只会创建一张 Category 表. 这样当用户直接安装第二版时, 会创建两张表. 当用户在第一版基础上覆盖安装第二版时, 会进入到升级数据库的操作中, 此时由于 Book 表已经存在了, 因此只需创建一张 Category 表即可.

又过了一段时间, 有了新的需求, 要在 Book 表中添加一个 category_id 字段, 为两个表建立关联.修改代码:

public class MyDatabaseHelper extends SQLiteOpenHelper {

    public static final String CREATE_BOOK = "create table Book ("
            + "id integer primary key autoincrement, "
            + "author text, "
            + "price real, "
            + "pages integer, "
            + "name text "
            + "category_id integer) ";

    public static final String CREATE_CATEGORY = "create table Category ("
            + "id integer primary key autoincrement, "
            + "category_name text, "
            + "category_code integer) ";


    public MyDatabaseHelper(Context context, String name,
                            SQLiteDatabase.CursorFactory factory, int version) {
        super(context, name, factory, version);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(CREATE_BOOK);
        db.execSQL(CREATE_CATEGORY);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        switch (oldVersion) {
            case 1:
                db.execSQL(CREATE_CATEGORY);
            case 2:
                db.execSQL("alter table Book add column category_id integer");
            default:
        }
    }
}

当用户直接安装第三版时, 新增的列就已经自动添加成功了, 如果覆盖安装到第三版, 就会进入到升级数据库的操作中.

注意, 以上 switch 语句中时没有 break 的, 这时为了保证在跨版本升级的时候, 每一次数据库修改都能被全部执行到, 保证数据不会丢失.

你可能感兴趣的:(Android)