第 6 章 - 持久化技术

1. 简介

  • 数据持久化就是指将那些内存中的瞬时数据保存到存储设备中,保证即使在手机或电脑关机的情况下,这些数据仍然不会丢失。

2. 文件存储

  • 文件存储适合用于存储一些简单的文本数据或二进制数据。
  • openFileOutput() 方法是 Context类 提供的,接收两个参数,第一个参数是文件名,在文件创建的时候使用的就是这个名称,所有的文件都是默认存储到 /data/data//files/ 目录下。第二个参数是文件的操作模式,有两种模式可选,MODE_PRIVATEMODE_APPEND
  • MODE_PRIVATE 是默认操作模式,当指定同样文件名时,所写入的内容将会覆盖原文件的内容。MODE_APPEND 表示如果文件名已存在,就往文件里追加内容,不存在就创建新文件。
public class MainActivity extends AppCompatActivity implements View.OnClickListener{

    private EditText editText;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        editText = findViewById(R.id.edit_text);
        Button save = findViewById(R.id.save);
        Button load = findViewById(R.id.load);
        save.setOnClickListener(this);
        load.setOnClickListener(this);
    }
    @Override
    public void onClick(View v){
        switch(v.getId()){
            case R.id.save:
                save();
                break;
            case R.id.load:
                String data = load();

                // 判断加载出来的数据是否为空
                if(!TextUtils.isEmpty(data)){
                    editText.setText(data);
                    editText.setSelection(data.length());
                    Toast.makeText(MainActivity.this,
                            "已恢复", Toast.LENGTH_SHORT).show();
                }
                break;
        }
    }

    /**
     * 定义一个保存方法,用于存储输入框中的文本数据
     */
    private void save(){
        String data = editText.getText().toString();
        FileOutputStream output;
        BufferedWriter writer = null;
        try{
            if(!TextUtils.isEmpty(data)){
                output = openFileOutput("data", MODE_PRIVATE);

                /**
                 * new BufferedWriter(Writer out)
                 * 由于参数是 Writer 类型,因此这里是需要传入 new OutputStreamWriter()
                 * 作为参数,另外 OutputStreamWriter 在这里也充当字节流转字符流的桥梁
                 */
                writer = new BufferedWriter(new OutputStreamWriter(output));
                writer.write(data);
                Toast.makeText(MainActivity.this,
                        "已保存", Toast.LENGTH_SHORT).show();
            }
        }catch(IOException e){
            e.printStackTrace();
        }finally{
            try{
                if(writer != null){
                    writer.close();
                }
            }catch(IOException e){
                e.printStackTrace();
            }
        }
    }

    /**
     * 定义一个加载方法,用于在输入框中加载之前保存的数据
     *
     * @return 将已经存储在文件中的数据返回
     */
    private String load(){
        FileInputStream input;
        BufferedReader reader = null;
        StringBuilder content = new StringBuilder();
        try{
            input = openFileInput("data");
            reader = new BufferedReader(new InputStreamReader(input));
            String line;
            while((line = reader.readLine()) != null){

                // 将加载出来的数据追加到 content 中
                content.append(line);
            }
        }catch(IOException e){
            e.printStackTrace();
        }finally{
            try{
                if(reader != null){
                    reader.close();
                }
            }catch(IOException e){
                e.printStackTrace();
            }
        }
        return content.toString();
    }
}
  • setSelection() 方法可以将光标移动到文本的末尾位置以便于继续输入
  • TextUtils.isEmpty() 方法可以一次性判断两种空值,当传入的字符串等于 null 或等于 空字符串 时就会返回 true
总结:
  • 文件存储所用到的核心技术就是 Context类 中提供的 openFileInput()openFileOutput() 方法,之后利用 Java 的各种流来进行读写操作

3. SharedPreferences 存储

  • 不同于文件存储方式,SharedPreferences 是使用键值对的方式来存储数据的
3.1 获取 SharedPreferences 对象:
  • 要想使用 SharedPreferences 来存储数据,首先需要获取到 SharedPreferences 对象,有 3 种方法可以获取
  1. Context类 中的 getSharedPreferences() 方法

此方法接收两个参数,第一个参数 用于指定 SharedPreferences 文件的名称,如果指定的文件不存在则会创建一个。第二个参数 用于指定操作模式,目前只有 MODE_PRIVATE 一种模式可选,表示只有当前的应用程序才可以对这个文件进行读写

  1. Activity类 中的 getPreferences() 方法

此方法只接受一个参数,因为使用这个方法时会自动将当前活动的类名作为文件名

  1. PreferenceManager类 中的 getDefaultPreferences() 方法

这是一个静态方法,只接受一个参数,并自动使用当前应用程序的包名作为前缀来命名 SharedPreference 文件

3.2 三步实现存储文件:
  1. 调用 SharedPreferences 对象的 edit() 方法来获取一个 SharedPreferences.Editor 对象
  2. SharedPreferences.Editor 对象中添加数据,比如添加一个布尔型数据就使用 putBoolean() 方法,添加一个字符串则使用 putString() 方法,以此类推
  3. 调用 apply() 方法将添加的数据提交,从而完成数据存储操作
  • 示例代码
public class MainActivity extends AppCompatActivity implements View.OnClickListener{

    private EditText editText;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        editText = findViewById(R.id.edit_text);
        Button save = findViewById(R.id.save);
        Button load = findViewById(R.id.load);
        save.setOnClickListener(this);
        load.setOnClickListener(this);
    }
    @Override
    public void onClick(View v){
        switch(v.getId()){
            case R.id.save:
                String inputText = editText.getText().toString();
                if(!TextUtils.isEmpty(inputText)){
                    SharedPreferences.Editor editor =
                            getSharedPreferences("data",MODE_PRIVATE).edit();
                    editor.putString("inputText", inputText);
                    editor.apply();
                    Toast.makeText(MainActivity.this,
                            "已保存", Toast.LENGTH_SHORT).show();
                }
                break;
            case R.id.load:
                SharedPreferences pref = getSharedPreferences("data", MODE_PRIVATE);
                String data = pref.getString("inputText", null);

                // 判断加载出来的数据是否为空
                if(!TextUtils.isEmpty(data)){
                    editText.setText(data);
                    editText.setSelection(data.length());
                    Toast.makeText(MainActivity.this,
                            "已恢复", Toast.LENGTH_SHORT).show();
                }
                break;
        }
    }
}

4. 实现记住密码功能

public class LoginActivity extends BaseActivity {

    private EditText account;
    private EditText passWord;
    private CheckBox rememberPassword;
    private SharedPreferences pref;
    private SharedPreferences.Editor editor;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);
        ActionBar actionBar = getSupportActionBar();
        if(actionBar != null){
            actionBar.hide();
        }
        pref = PreferenceManager.getDefaultSharedPreferences(this);
        account = findViewById(R.id.account);
        passWord = findViewById(R.id.pass_word);
        rememberPassword = findViewById(R.id.check_box);
        Button login = findViewById(R.id.login);
        boolean isRemember = pref.getBoolean("remember_password", false);
        if(isRemember){
            String mAccount = pref.getString("account",null);
            String mPassword = pref.getString("password", null);
            account.setText(mAccount);
            passWord.setText(mPassword);
            rememberPassword.setChecked(true);
        }
        login.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                String accounT = account.getText().toString();
                String passWorD = passWord.getText().toString();
                if(accounT.equals("admin") && passWorD.equals("123456")){
                    editor = pref.edit();
                    if(rememberPassword.isChecked()){
                        editor.putBoolean("remember_password", true);
                        editor.putString("account", accounT);
                        editor.putString("password", passWorD);
                    }else{
                        editor.clear();
                    }
                    editor.apply();
                    Intent intent = new Intent
                            (LoginActivity.this, MainActivity.class);
                    startActivity(intent);
                }else{
                    Toast.makeText(LoginActivity.this,
                            "账号或密码错误", Toast.LENGTH_SHORT).show();
                }
            }
        });
    }
}

5. SQLite 数据库存储

  • SQLite 是一款轻量级的关系型数据库,运算速度非常快,占用资源很少,通常只需要几百 KB 的内存就够了。
5.1 创建数据库:
  • Android 提供了一个 SQLiteOpenHelper 帮助类,借助这个类就可以对数据库进行创建和升级。它是一个抽象类,所以需要创建一个自己的帮助类去继承它,并复写 onCreate()onUpgrade() 抽象方法。

  • SQLiteOpenHelper 中还有两个非常重要的实例方法,getReadableDatabase()getWritableDatabase()。这两个方法都可以创建或打开一个现有的数据库(如果数据库存在则直接打开,否则创建一个新的数据库),并返回一个可对数据库进行读写操作的对象。

  • 创建自己的帮助类

public class MyDatabaseHelper extends SQLiteOpenHelper{

    /**
     * 将建表语句定义成字符串常量
     */
    private static final String CREATE_COMPUTER = "create table Computer("
            + "id integer primary key autoincrement,"
            + "brand text,"
            + "name text,"
            + "price real)";

    private Context mContext;

    /**
     * 复写父类构造方法
     * @param context 上下文
     * @param name 数据库名称
     * @param factory 允许在查询数据时返回一个自定义的 Cuosor,一般传 null
     * @param version 当前数据库的版本号
     */
    MyDatabaseHelper(Context context, String name,
                     SQLiteDatabase.CursorFactory factory, int version){
        super(context, name, factory, version);
        mContext = context;
    }

    /**
     * 创建数据库时会调用这个方法
     * @param db 已创建的数据库
     */
    @Override
    public void onCreate(SQLiteDatabase db){
        db.execSQL(CREATE_COMPUTER);
        Toast.makeText(mContext, "创建成功", Toast.LENGTH_SHORT).show();
    }

    /**
     * 升级数据库时会调用
     * @param db 创建好的数据库
     * @param oldVersion 旧的版本号
     * @param newVersion 新的版本号
     */
    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion){

    }
}
  • 修改 MainActivity 中的代码
public class MainActivity extends AppCompatActivity implements View.OnClickListener{

    private MyDatabaseHelper dbHelper;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        dbHelper = new MyDatabaseHelper(this, "ComputerStore.db", null, 1);
        Button createDB = findViewById(R.id.create_database);
        createDB.setOnClickListener(this);
    }

    @Override
    public void onClick(View v){
        switch(v.getId()){
            case R.id.create_database:
                dbHelper.getWritableDatabase();
                break;
        }
    }
}
5.2 升级数据库:
  • 再添加一张表
private static final String CREATE_CATEGORY = "create table Category("
            + "id integer primary key autoincrement,"
            + "type text,"
            + "size real)";
  • 修改 onCreate() 方法
@Override
public void onCreate(SQLiteDatabase db){
    db.execSQL(CREATE_COMPUTER);
    // 创建另一张表
    db.execSQL(CREATE_CATEGORY);
    Toast.makeText(mContext, "创建成功", Toast.LENGTH_SHORT).show();
}
  • 主角登场,修改 onUpgrade() 方法,在 onUpgrade() 方法中执行两条 DROP 语句,如果发现数据库中已经存在这两张表,就直接删掉,然后再调用 onCreate() 方法重新创建
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion){
    db.execSQL("drop table if exists Computer");
    db.execSQL("drop table if exists Category");
    onCreate(db);
}
  • 接着修改 MainActivity 中 MyDatabaseHelper 的构造方法的参数,将版本号改成比原来数字大的数即可
// dbHelper = new MyDatabaseHelper(this, "ComputerStore.db", null, 1);
dbHelper = new MyDatabaseHelper(this, "ComputerStore.db", null, 2);

6. 操作数据(关于 SQLite)

对数据进行操作有 4 种,即 CRUD。C 代表添加(Create),R 代表查询(Retrieve),U 代表更新(Update),D 代表删除(Delete)。使用SQL 命令,insert 添加数据,select 查询数据,update 更新数据,delete 删除数据。

6.1 添加数据:
  • 使用 ContentValues 对象来组装数据,再调用 SQLiteDatabase 的 insert() 方法来插入数据,insert() 接收 3 个参数,第一个是表名;第二个用于在未指定添加数据的情况下给某些可为空的列自动赋值,一般用不到,传 null第三个是一个 ContentValues 对象,它提供了一系列 put() 方法重载,用于向 ContentValues 中添加数据。
Button addData = 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("brand", "Apple");
        values.put("name", "MacBook");
        values.put("price", 14488.00);
        // 插入第一条数据
        db.insert("Computer", null, values);
        
        // 开始组装第二条数据
        values.clear();
        values.put("brand", "Mi");
        values.put("name", "MiBook");
        values.put("price", 7999.00);
        db.insert("Computer", null, values);
    }
});
6.2 更新数据:
  • 使用 SQLiteDatabase 的 Update() 方法对数据进行更新,此方法接收 4 个参数,第一个是表名第二个是 ContentValues 对象,要把要更新的数据在这里组装进去;第三、第四个是用于约束更新某一行或某几行中的数据,不指定则默认更新所有行。
Button updateData = 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", 12888.00);
        /* 第三个参数对象 SQL 语句的 where 部分,表示更新所有 name 等于
           ?的行。而 ?是一个占位符,可以通过第四个参数提供的一个字符串
           数组为第三个参数中的每个占位符指定相应的内容。*/
        db.update("Computer", values, "name = ?", new String[]{"MacBook"});
    }
});

—— 表示更新名称为 MacBook 的数据

6.3 删除数据:
  • 使用 SQLiteDatabase 的 delete() 方法删除数据,此方法接收 3 个参数,除了没有 ContentValues ,其他和 update() 方法一样
Button deleteData = findViewById(R.id.delete_data);
deleteData.setOnClickListener(new View.OnClickListener(){
    @Override
    public void onClick(View v){
        SQLiteDatabase db = dbHelper.getWritableDatabase();
        db.udelete("Computer", "price < ?", new String[]{"10000.00"});
    }
});

—— 表示删除价格低于 10000 的数据

6.4 查询数据:
  • 使用 SQLiteDatabase 的 query() 方法对数据进行查询,最短的一个方法重载也需要传入 7 个参数。第一个是表名第二个指定查询哪几列,不指定则查询所有列;第三、第四个用于约束查询某一行或某几行的数据第五个用于指定需要去 group by 的列,不指定则表示不对查询结果进行 group by 操作;第六个用于对 group by 之后的数据进行进一步的过滤,不指定则不过滤;第七个用于指定查询结果的排序方式,不指定则表示使用默认排序。

调用 query() 方法后会返回一个 Cursor 对象,查询到的所有数据从这个对象中取出。

query()方法参数 对应SQL部分 描述
table from table_name 指定查询的表名
columns select columns1, column2 指定查询的列名
selection where column = value 指定where的约束条件
selectionArgs - 为where中的占位符提供具体的值
groupBy group by column 指定需要group by的列
havind having column = value 对group by后的结果进一步约束
orderBy order by column1, column2 指定查询结果的排序方式
  • 查询出来后,将结果打印到 Logcat:
Button queryData = findViewById(R.id.query_data);
queryData.setOnClickListener(new View.OnClickListener(){
    @Override
    public void onClick(View v){
        SQLiteDatabase db = dbHelper.getWritableDatabase();
        // 查询表中所有数据
        Cursor cursor = db.query("Computer",null, null, null, null, null, null);
        // moveToFirst() 方法将数据指针移动到第一行的位置
        if(cursor.moveToFirst()){
            do{
                // 遍历 Cursor 对象,取出数据并打印,getColumnIndex() 方法获取
                // 到某一行在表中对应的位置索引,然后将这个索引传入相应的取值
                // 方法中,即可从数据库读取到数据
                String brand = cursor.getString(cursor.getColumnIndex("brand"));
                String name = cursor.getString(cursor.getColumnIndex("name"));
                double price = cursor.getDouble(cursor.getColumnIndex("price"));
                Log.d("MainActivity", " Computer brand is " + brand);
                Log.d("MainActivity", " Computer name is " + name);
                Log.d("MainActivity", " Computer price is " + price);
            }while(cursor.moveToNext());
        }
        // 最后需要关闭 Cursor
        cursor.close();
    }
});
6.5 使用 SQL 操作数据库:
  • 添加数据:
    db.execSQL("insert into Computer(brand, name, price)values(?, ?, ?)", new String[] {"Apple", "MacBook", 14888.00});
  • 更新数据:
    db.execSQL("update Computer set price = ? where name = ?", new String[] {"12888.00", "MacBook"});
  • 删除数据:
    db.execSQL("delete from Computer where price < ?", new String[] {"10000.00"});
  • 查询数据:
    db.rawQuery("select * from Computer", null);

7. 使用 LitePal 操作数据库

7.1 简介:
  • LitePal 是一款开源的 Android 数据库框架,它采用了对象关系映射(ORM) 的模式,并将平时开发最常用到的一些数据库功能进行了封装,使得不用编写一行 SQL 语句就可以完成各种建表和增删改查的操作。
7.2 配置 LitePal:
  • app/build.gradle 中添加依赖
    implementation 'org.litepal.android:core:2.0.0'
  • app/src/main 新建目录 assets,并新建一个 litepal.xml 文件

标签用于指定数据库名, 标签用于指定数据库版本号, 标签用于指定所有的映射模型



    
    
    
    
    
    

  • 配置 AndroidManifest.xml
    将项目的 application 配置为 org.litepal.LitePalApplication,这样才能让 LitePal 的所有功能正常工作。



    
7.3 创建和升级数据库:
  • 什么事对象关系映射?将面向对象语言和面向关系数据库之间建立一种映射关系就是对象关系映射。对象关系映射可以用面向对象的思维来操作数据库,而不用再和 SQL 语句打交道了。
  • 定义一个 Computer 类:
public class Computer{
    
    private int id;
    private String brand;
    private String name;
    private double price;
    
    public int getId(){
        return id;
    }
    public void setId(int id){
        this.id = id;
    }
    
    public String getBrand(){
        return brand;
    }
    public void setBrand(String brand){
        this.brand = brand;
    }
    
    public String getName(){
        return name;
    }
    public void setName(String name){
        this.name = name;
    }
    
    public double getPrice(){
        return price;
    }
    public void setPrice(double price){
        this.price = price;
    }
}
  • 将 Computer 类添加到映射模型列表中,不管有多少模型类需要映射,都使用同样的方式配置在 下即可


    
    
    
    
    
        
    

  • 创建数据库:
public class MainActivity extends AppCompatActivity{

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button createDatabase = findViewById(R.id.create_database);
        createDatabase.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                LitePal.getDatabase();
            }
        });
    }
}
  • 升级数据库:
    相比较 SQLiteDatabaseHelper 升级数据库的方式, LitePal 不需要 drop 掉之前的表,保留了之前表中的所有数据。
  1. 添加列:直接在原来 Computer 模型类里面添加新列即可
  2. 添加表:直接新建一个模型类,并在 litepal.xml 中的 标签将新的模型类添加进去
  3. 修改数据库的版本号
7.4 添加数据:
  • 先让模型类继承 LitePalSupport,这样才能进行 CRUD 操作。
Button addData = findViewById(R.id.add_data);
addData.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Computer computer = new Computer();
        computer.setBrand("Apple");
        computer.setName("MacBook");
        computer.setPrice(14888.00);
        computer.save();
    }
});
7.5 更新数据:
Button updateData = findViewById(R.id.update_data);
updateData.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Computer computer = new Computer();
        computer.setPrice(12888.00);
        computer.updateAll("name = ?", "MacBook");
    }
});

updateAll() 方法中可以指定一个约束条件,如果不指定则更新所有数据
setToDefault() 可以恢复默认值,传入相应的列名即可将那一列恢复默认值,可以用 updateAll() 方法来指定约束条件

7.6 删除数据:
  • 调用 LitePal 的静态方法 deleteAll()来删除数据,不指定约束条件则表示删除表中所有数据
    LitePal.deleteAll(Computer.class, "price < ?", "10000.00")
7.7 查询数据:
  • 调用LitePal.findAll(Computer.class),返回一个 Computer 类型的 List 集合,接着使用 for-each 循环从中取出数据即可
Button queryData = findViewById(R.id.query_data);
queryData.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        List computers = LitePal.findAll(Computer.class);
        for(Computer computer : computers){
            Log.d("MainActivity", "brand is " + computer.getBrand());
            Log.d("MainActivity", "name is " + computer.getName());
            Log.d("MainActivity", "price is " + computer.getPrice());
        }
    }
});
  • LitePal.findFirst(Computer.class)
    查询表中第一条数据
  • LitePal.findLast(Computer.class)
    查询表中最后一条数据
  • List computers = LitePal.select("name").find(Computer.class);
    用于指定查询哪几列的数据
  • List computers = LitePal.where("price < ?", "10000.00").find(Computer.class);
    用于指定查询的约束条件
  • List computers = LitePal.order("price desc").find(Computer.class);
    用于指定结果的排序方式,desc 表示降序,asc 或不写表示升序
  • List computers = LitePal.limit(3).find(Computer.class);
    用于指定查询结果的数量,表示只查表中前 3 条数据
  • List computers = LitePal.limit(3).offset(1).find(Computer.class);
    用于指定查询结果的偏移量,表示绕过第一条数据,查第 2,3,4 条。
  • LitePal.findBySQL()
    进行原生查询,返回 Cursor 对象,然后用老方式将数据取出

你可能感兴趣的:(第 6 章 - 持久化技术)