6. 数据储存全方案-详解持久化技术

11-160312143956.jpg

第6章 数据储存全方案-详解持久化技术

瞬时数据: 就是指那些存储在内存当中,有可能会因为程序关闭或其他原因导致内存被回收,而丢失的数据。

6.1持久化技术简介

数据持久化就是指将那些内存中的瞬时数据保存在存储设备中,保证即使在手机或电脑关机的情况下,这些数据仍然不会丢失。保存在内存中的数据是处于瞬时状态的,而保存在存储设备中的数据是处于持久状态的,持久化技术则提供了一种机制可以让数据在瞬时状态和持久状态之间进行切换。
Android系统中主要提供了3种方式用于简单地实现数据持久化功能,即文件存储,SharedPreference存储以及数据库存储

6.2 文件存储

文件存储是Android中最基本的一种数据存储方式,它不对存储的内容进行任何的格式化处理,所有数据都是原封不动地保存到文件当中的,因而他比较适合用于存储一些简单的文本数据或二进制数据。

6.2.1 将数据存储到文件中

Context类中提供了一个openFileOutput()方法,可以用于将数据存储到指定的文件中。这个方法接收两个参数,第一个参数是文件名,在文件创建的时候使用的就是这个名称,注意这里的文件名不可以包含路径,因为所有的文件都是默认存储到/data/data/<packagename>/file/目录下的。第二个参数是文件的操作模式,主要有两种模式可选,MODE_PRIVATEMODE_APPEND。其中MODE_PRIVATE是默认的操作模式,表示当指定同样文件名的时候,所写入的内容将会覆盖原文件的内容,而MODE_APPEND则表示如果该文件已存在,就往文件里面追加内容,不存在就创建新文件。
openFileOutput()方法返回的是一个FileOutputStream对象,得到了这个对象之后就可以使用Java流的方式将数据写入到文件中了。

public class MainActivity extends AppCompatActivity
{

    private EditText editText;

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

        editText = (EditText) findViewById(R.id.edit);

        String inputText = Load();
        if (!TextUtils.isEmpty(inputText))
        {
            editText.setText(inputText);
            editText.setSelection(inputText.length());
            Toast.makeText(this,"恢复成功!",Toast.LENGTH_LONG).show();
        }

    }

    @Override
    protected void onDestroy()
    {
        super.onDestroy();

        Save(editText.getText().toString());
    }

    public void Save(String inputText)
    {
        FileOutputStream out = null;
        BufferedWriter bufferedWriter = null;

        try
        {
            out = openFileOutput("WuMwng",MODE_PRIVATE);
            bufferedWriter = new BufferedWriter(new OutputStreamWriter(out));
            bufferedWriter.write(inputText);
        } catch (FileNotFoundException e)
        {
            e.printStackTrace();
        } catch (IOException e)
        {
            e.printStackTrace();
        }
        finally
        {
            if (bufferedWriter != null)
            {
                try
                {
                    bufferedWriter.close();
                } catch (IOException e)
                {
                    e.printStackTrace();
                }
            }
        }
    }

    public String Load()
    {
        FileInputStream in = null;
        BufferedReader reader = null;
        StringBuilder builder = new StringBuilder();
        try
        {
            in = openFileInput("WuMwng");
            reader = new BufferedReader(new InputStreamReader(in));
            String line = "";
            while ((line = reader.readLine()) != null)
            {
                builder.append(line);
            }
        } catch (IOException e)
        {
            e.printStackTrace();
        }
        finally
        {
            if (reader != null)
            {
                try
                {
                    reader.close();
                } catch (IOException e)
                {
                    e.printStackTrace();
                }
            }
        }

        return builder.toString();
    }
}

public void Save(String inputText)
    {
        FileOutputStream out = null;
        BufferedWriter bufferedWriter = null;

        try
        {
            out = openFileOutput("WuMwng",MODE_PRIVATE);
            bufferedWriter = new BufferedWriter(new OutputStreamWriter(out));
            bufferedWriter.write(inputText);
        } catch (FileNotFoundException e)
        {
            e.printStackTrace();
        } catch (IOException e)
        {
            e.printStackTrace();
        }
        finally
        {
            if (bufferedWriter != null)
            {
                try
                {
                    bufferedWriter.close();
                } catch (IOException e)
                {
                    e.printStackTrace();
                }
            }
        }
    }

Save函数,这里通过openFileOutput()方法能够得到一个FileOutputStream对象,然后在借助它构建出一个OutputStreamWriter对象,接着在使用OutputStreamWriter构建出一个BufferedWriter对象,这样你就可以通过BufferedWriter来将文本内容写入到文件中了。

6.2.2 从文件中读取数据

Context类中还提供了一个openFileInput()方法,用于从文件中读取数据,这个方法要比openFileOutput()简单一些,它只接收一个参数,即要读取的文件名,然后系统会自动到/data/data/<packagename>/files/目录下面加载这个文件,并返回一个FileInputStream对象,得到了这个对象之后再通过Java流的方式就可以将数据读取出来了。

public String Load()
    {
        FileInputStream in = null;
        BufferedReader reader = null;
        StringBuilder builder = new StringBuilder();
        try
        {
            in = openFileInput("WuMwng");
            reader = new BufferedReader(new InputStreamReader(in));
            String line = "";
            while ((line = reader.readLine()) != null)
            {
                builder.append(line);
            }
        } catch (IOException e)
        {
            e.printStackTrace();
        }
        finally
        {
            if (reader != null)
            {
                try
                {
                    reader.close();
                } catch (IOException e)
                {
                    e.printStackTrace();
                }
            }
        }

        return builder.toString();
    }

通过openFileInput()方法获取到了一个FileInputStream对象,然后借助它又构建出了一个InputStreamReader对象,再使用InputStreamReader构建出一个BufferedReader对象,我们就可以通过BufferedReader进行一行行的读取,把文件中所有的文件内容全部读取出来,并存放在一个StringBuilder对象中,最后将读取到的内容返回就可以了。

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

        editText = (EditText) findViewById(R.id.edit);

        String inputText = Load();
        if (!TextUtils.isEmpty(inputText))
        {
            editText.setText(inputText);
            editText.setSelection(inputText.length());
            Toast.makeText(this,"恢复成功!",Toast.LENGTH_LONG).show();
        }

    }

onCreate()方法中调用load方法来读取文件中存储的文本内容,如果读到的内容不为null,就调用EditTextsetText()方法将内容填充到EditText里,并调用setSelection()方法将输入光标移动到文本的末尾位置以便于继续输入,然后弹出还原成功的提示。

注意,上述代码在对字符串进行非空判断的时候使用了TextUtils.isEmpty()方法这是一个非常好用的方法,它可以一次性进行两种空值的判断。当传入的字符串等于null或者等于空字符的时候,这个方法都会返回true,从而使得我们不需要先单独判断这两种空值再使用逻辑运算符连接起来了。

文件存储的方式并不适合用于保存一些较为复杂的文本数据。

6.3 SharedPreferences存储

要想使用SharedPreferences来存储数据,首先需要获取到SharedPreferences对象。Android中主要提供了3中方法用于得到SharedPreferences对象。

1.Context类中的getSharedPreferences()方法
此方法接收两个参数,第一个参数用于指定SharedPreferences文件的名称,如果指定的文件不存在则会创建一个,SharedPreferences文件都是存放在/data/data/<package name>/shared_prefs/目录下的。第二个参数用于指定操作模式,目前只有MODE_PRIVATE这一种模式可选,它是默认的操作模式,和直接传入0效果是相同的,表示只有当前的应用程序才可以对这个SharedPreferences文件进行读写。

2.Activity类中的getPreferences()方法
这个方法和Context中的getSharedPreferences()方法很相似,不过它只接收一个操作模式参数,因为使用这个方法时,会自动将当前活动的类名作为SharedPreferences的文件名。

3.preferenceManager类中的getDefaultSharedPreferences()方法
这是一个静态方法,他接受一个Context参数,并自动使用当前应用程序的包名作为前缀来命名SharedPreferences文件。

SharedPreferences文件中存储数据。主要可以分为3步实现:
1.调用SharedPreferences对象的edit()方法来获取一个SharedPreferences.Editor对象
2.向SharedPreferences.Editor对象中添加数据,比如添加一个布尔型数据就使用putBoolean()方法,添加一个字符串则使用putString()方法。
3.调用apply()方法将添加的数据提交,从而完成数据存储操作。

<LinearLayout
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <CheckBox
            android:id="@+id/remember_pass"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="18sp"
            android:text="记住密码"/>

    </LinearLayout>
public class LoginActivity extends BaseActivity
{
    private Button button;
    private EditText editText_Account,editText_Password;
    private CheckBox checkBox;
    private SharedPreferences pref;
    private SharedPreferences.Editor editor;

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

        pref = PreferenceManager.getDefaultSharedPreferences(this);

        initView();

        boolean isRemenber = pref.getBoolean("remember_password",false);
        if (isRemenber)
        {
            String account = pref.getString("account","");
            String password = pref.getString("password","");
            editText_Account.setText(account);
            editText_Password.setText(password);
            checkBox.setChecked(true);
        }

        initEvent();
    }

    public void initView()
    {
        button = (Button) findViewById(R.id.button_login);
        editText_Account = (EditText) findViewById(R.id.edit_account);
        editText_Password = (EditText) findViewById(R.id.edit_password);

        checkBox = (CheckBox) findViewById(R.id.remember_pass);
    }

    public void initEvent()
    {
        button.setOnClickListener(new View.OnClickListener()
        {
            @Override
            public void onClick(View v)
            {
                String account = editText_Account.getText().toString();
                String password = editText_Password.getText().toString();

                if (account.equals("admin") && password.equals("123456"))
                {
                    editor = pref.edit();
                    if (checkBox.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);
                    finish();
                }
                else
                {
                    Toast.makeText(LoginActivity.this, "输入的账号或密码有误!!!", Toast.LENGTH_SHORT).show();
                }
            }
        });
    }
}

调用getBoolean()方法去获取remember_password这个键对应的值,一开始当然不存在对应的值了所以会使用默认值。

这里实现的记住密码功能仍然是个简单的示例,并不能在实际的项目中直接使用因为将密码以明文的形式存储在SharedPreferences文件中是非常不安全的,很容易就会被比人盗取,因此在正式的项目中还需要结合一定的加密算法来对密码进行保护才行

SQLite数据库存储

SQLite是一款轻量级的关系型数据库,他的运算速度非常快,占用资源很少,通常只需要几百KB的内存就足够了,因而特别适合在移动设备上使用。SQLite不仅支持标准的SQL语法,还遵循了数据库的ACID事务。
前面我们所学的文件存储和SharedPreferences存储毕竟只是适用于保存一些简单的数据和键值对,当需要存储大量复杂的关系型数据的时候,你就会发现以上两种存储方式很难应付的了。比如我们手机的短信程序中可能会有很多个会话,每个会话中又包含了很多条信息的内容,并且大部分会话还可能各自对应了电话薄中的某个联系人。

创建数据库

Android为了让我们能够更加方便地管理数据库,专门提供了一个SQLiteOpenHelper帮助类,借助这个类就可以非常简单地对数据库进行创建和升级。

SQLiteOpenHelper是一个抽象类,这意味着如果我们想要使用它的话,就需要创建一个自己的帮助类去继承它。SQLiteOpenHelper中有两个抽象方法,分别是onCreate()和onUpgrade(),我们必须在自己的帮助类里面重写这两个方法,然后分别在这两个方法中去实现创建,升级数据库的逻辑。

SQLiteOpenHelper中还有两个非常重要的实例方法:getReadableDatabase()getWritableDatabase()这两个方法都可以创建或打开一个现有的数据库(如果数据库已经存在,则直接打开,否则创建一个新的数据库),并返回一个可对数据库进行读写操作的对象。不同的是,当数据库不可写入的时候(如磁盘空间已满),getReadableDatabase()方法返回的对象将以只读的方式去打开数据库,而getWritableDatabase()方法则将出现异常。

SQLiteOpenHelper中有两个构造方法可供重写,一般使用参数少一点的那个构造方法即可。这个构造方法中接收4各参数,第一个参数是Context,必须要有它才能对数据库进行操作。第二个参数是数据库名,创建数据库时使用的就是这里指定的名称。第三个参数允许我们在查询数据的时候返回一个自定义的Cursor,一般都是传入null。第四个参数表示当前数据库的版本号,可用于对数据库进行升级操作。
构建出SQLiteOpenHelper的实例之后,再调用它的getReadableDatabase()或getWritableDatabase()方法就能够创建数据库了,数据库文件会存放在/data/data/<package name>/database/目录下。此时重写的OnCreate()方法也会得到执行,所以会在这里处理一些创建表的逻辑。

create table Book (
id integer primary key autoincrement,
author text,
price real,
pages integer,
name text)

SQLite不像其他的数据库拥有众多繁杂的数据类型,它的数据类型很简单,integer表示整型,text表示文本类型,real表示浮点型,blob表示二进制类型,我们还用primary key将id列设为主键,并用autoincrement关键字表示id列是自动增长的。

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

把建表语句定义成了一个字符串常量,然后在onCreate()方法中又调用了SQLiteDatabase的execSQL()方法去执行这条建表语句。并弹出一个Toast提示创建成功。

private MyDatabaseHelper dbHelper;

  Button button_create_database;

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

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

      initView();
      initEvent();
  }

  public void initView()
  {
      button_create_database = (Button) findViewById(R.id.create_database);
  }

  public void initEvent()
  {
      button_create_database.setOnClickListener(new View.OnClickListener()
      {
          @Override
          public void onClick(View v)
          {
              dbHelper.getWritableDatabase();
          }
      });
  }

在onCreate()方法中构建了一个MyDatabaseHelper对象,并且通过构造函数的参数将数据库名指定为BookStore.db,版本号指定为1,然后在Create database按钮的点击事件里调用了getWritableDatabase()方法。这样当第一次点击Create database按钮时,就会检测到当前的程序中并没有BookStore.db这个数据库,于是会创建该数据库,并调用MyDatabaseHelper中的onCreate()方法,这样Book表也就得到了创建,然后会弹出一个Toast,提示创建成功。再次点击Create database按钮时,会发现此时已经存在BookStroe.db数据库了,因此不会再创建数据库了。

adb是AndroidSDK中自带的一个调试工具,使用这个工具可以直接对连接在电脑上的手机或模拟器进行调试操作,它存放在SDK的platform-tools目录下,如果想要在命令行中使用这个工具,就需要先把它的路径配置到环境变量里。
如果你使用的是Windows系统,右击计算机-属性-高级系统设置-环境变量,然后在系统变量里面找到Path并点击编辑,将platform-tools目录配置进去。

打开 命令行界面,输入adb shell,就会进入到设备的控制台。
使用cd命令进入到/data/data/com.example.databasetest/database/目录下,并使用ls命令查看到该目录里的文件。

image

BookStore.db-journal则是为了让数据库能够支持事务而产生的临时日志文件。通常情况下,这个文件的大小是0kb。
打开数据库

只需要键入sqlite3 后面跟上数据库名,即可。


image

看一下目前数据库中有哪些表,键入.table命令。
android_metadata表是每个数据库中都会自动生成的,不用管它。这里还可以通过.schema命令来查看它们的建表语句。
键入.exit或.quit命令可以退出数据库的编辑,再键入exit命令就可以退出设备控制台了。
注意:在打开sqlite3打开数据库后,以后的命令是隐藏的

升级数据库

onUpgrade()方法是用于对数据库进行升级的,它在数据库的整个管理工作中起着非常重要的作用,可千万不要忽视它嗷。

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

通过卸载程序来新增一张表毫无疑问是很极端的做法。
在onUpgrade()方法中执行了两条DROP语句,如果发现数据库中已经存在Book表或Category表了,就将这两张表删除掉,然后再调用onCreate()方法重新创建,这里现将已经存在的表删除掉,因为如果在创建表时发现这张表已经存在了,就会直接报错。

 @Override
    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);
    }

如何让onUpgrade()方法执行,还记得SQLiteOpenHelper的构造方法里接收的第四个参数吗?它表示当前数据库的版本号,之前我们传入的是1,现在只要传入一个比1大的数,就可以让onUpgrade()方法执行了。

image

添加数据

我们可以对数据进行的操作无非有4种,即CRUD。其中C代表添加(Create),R代表查询(Retrieve),U代表更新(Updata),D代表删除(Delete)。
添加数据时使用insert,查询数据时使用select,更新数据时使用update,删除数据时使用delete。

调用SQLiteOpenHelper的getReadableDatabase()或getWritableDatabase()方法是可以用于创建和升级数据库的,这两个方法还都会返回一个SQLiteDatabase对象,借助这个对象就可以对数据进行CRUD操作了。

 button_add_data.setOnClickListener(new View.OnClickListener()
        {
            @Override
            public void onClick(View v)
            {
                sqLiteDatabase = dbHelper.getWritableDatabase();

                ContentValues values = new ContentValues();

                values.put("name","Apple");
                values.put("author","wuMeng");
                values.put("pages",454);
                values.put("price",16.96);

                sqLiteDatabase.insert("Book",null,values);
                values.clear();

                values.put("name","User");
                values.put("author","WuMeng2");
                values.put("pages",510);
                values.put("price",19.95);
                sqLiteDatabase.insert("Book",null,values);
            }
        });
    }

SQLiteDatabase中提供了一个insert()方法,这个方法就是专门用于添加数据的。它接收3个参数,第一个参数是表名,我们希望向哪张表中添加数据,这里就传入该表的名字。第二个参数用于在未指定添加数据的情况下给某些可为空的列自动赋值null,一般我们用不到这个功能,直接传入null即可。第三个参数是一个ContentValues对象,它提供了一系列的put()方法重载,用于向ContentValues中添加数据,只需要将表中的每个列名以及相应的待添加数据传入即可。

image

更新数据

SQLiteDatabase中也提供了一个非常好用的update()方法,用于对数据进行更新,这个方法接收4个参数,第一个参数和insert()方法一样,也是表名,在这里指定去更新哪张表的数据。第二个参数是ContentValues对象,要把更新数据在这里组装进去。第三,第四个参数用于约束某一行或某几行的数据,不指定的话,默认就是更新所有行。

//更新数据
        button_update_data.setOnClickListener(new View.OnClickListener()
        {
            @Override
            public void onClick(View v)
            {
                sqLiteDatabase = dbHelper.getWritableDatabase();
                ContentValues values = new ContentValues();
                values.put("author","guojun");
                sqLiteDatabase.update("Book",values,"name=?",new String[]{"User"});

            }
        });

在更新数据的点击事件里面构架了一个ContentValues对象,并且只给它指定了一组数据,这里使用了第三,第四个参数来指定具体更新哪几行,第三个参数对应的是SQL语句的where部分,表示更新所有name等于?的行,而?是一个占位符,可以通过第四个参数提供的一个字符串数组为第三个参数中的每个占位符指定相应的内容。


image

删除数据

SQLiteDatabase中提供了一个delete()方法,专门用于删除数据,这个方法接收三个参数,第一个参数仍然是表名,第二,第三个参数又是用于约束删除某一行或某几行的数据,不指定的话默认就是删除所有行。


        //删除数据
        button_delete_data.setOnClickListener(new View.OnClickListener()
        {
            @Override
            public void onClick(View v)
            {
                sqLiteDatabase = dbHelper.getWritableDatabase();
                sqLiteDatabase.delete("Book","pages > ?",new String[]{"500"});
            }
        });

在删除按钮的点击事件里指明去删除Book表中的数据,并且通过第二第三个参数来指定仅删除那些页数超过500页的书。

image

查询数据

查询数据是CRUD中最复杂的一种操作。
SQLiteDatabase中还提供了一个query()方法用于对数据进行查询,这个方法的参数非常复杂,最短的一个方法重载也需要传入7个参数。
第一个参数当然不用说还是表名,表示我们希望从哪张表中查询数据。
第二个参数用于指定去查询哪几列,如果不指定则默认查询所有列。
第三,第四个参数用于约束查询某一行或某几行的数据,不指定则默认查询所有行的数据。
第五个参数用于指定需要去group by 的列,不指定则表示不对查询结果进行group by 操作。第六个参数用于对group by 之后的数据进行进一步的过滤,不指定则表示不进行过滤。
第七个参数用于指定查询结果的排序方式,不指定则表示使用默认的排序方式。
调用query()方法后会返回一个Cursor对象,查询到的所有数据都将从这个对象中取出。

 //查询数据
        button_query_data.setOnClickListener(new View.OnClickListener()
        {
            @Override
            public void onClick(View v)
            {
                sqLiteDatabase = dbHelper.getWritableDatabase();
                Cursor cursor = sqLiteDatabase.query("Book",null,null,null,null,null,null);
                if (cursor.moveToFirst())
                {
                    do
                    {
                        String name = cursor.getString(cursor.getColumnIndex("name"));
                        String author = cursor.getString(cursor.getColumnIndex("author"));
                        int pages = cursor.getInt(cursor.getColumnIndex("pages"));
                        Log.d(TAG, "name: "+name);
                        Log.d(TAG, "author: "+author);
                        Log.d(TAG, "pages: "+pages);
                    }while (cursor.moveToNext());
                }
                cursor.close();
            }
        });

查询完之后就得到了一个Cursor对象,接着我们调用它的moveToFirst()方法将数据的指针移动到第一行的位置,然后进入到了一个循环当中,去遍历查询到的每一行数据。在这个循环中可以通过Cursor的getColumnIndex()方法获取到某一列在表中对应的位置索引,然后将这个索引传入到相应的取值方法中,就可以得到从数据库中读取到的数据了。

image

使用LitePal操作数据库

公司里的代码非常强调稳定性,而我们自己写出的代码往往越复杂就越容易出问题。相反,开源项目的代码都是经过时间验证的,通常比我们自己的代码要稳定的多。因此,现在有很多公司为了追求开发效率以及项目的稳定性,都会选择使用开源库。

LitePal是一款开源的Android数据库框架,它采用了对象关系映射(ORM)的模式,并将我们平时开发最常用到的一些数据库功能进行了封装,使得不用编写一行SQL语句就可以完成各种建表和增删改查的操作。

配置LitePal

大多数的开源项目都会将版本提交到jcenter上,我们只需要在app/build.gradle文件中声明该开源库的引用就可以了。
接下来需要配置litepal.xml文件,右击app/src/main目录--New--Directory--,创建一个assets目录,然后在assets目录下面再新建一个litepal.xml文件。

<?xml version="1.0" encoding="utf-8" ?>
<litepal>
    
    <dbname value="BookStore"></dbname>

    <version value="1"></version>

    <list>

    </list>
</litepal>

<dbname>标签用于指定数据库名,<version>标签用于指定数据库的版本号。<list>标签用于指定所有的映射模型。

<application
        android:name="org.litepal.LitePalApplication"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">

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

创建和升级数据库

LitePal采取的是对象关系映射(ORM)的模式,那么什么是对象关系映射呢?简单点说,我们使用的编程语言是面向对象语言,而使用的数据库则是关系型数据库,那么将面向对象的语言和面向关系的数据库之间建立一种映射关系,这就是对象关系映射了。

public class Book
{
    private int id;
    private String author;
    private double price;
    private int pages;
    private String name;

    public int getId()
    {
        return id;
    }

    public void setId(int id)
    {
        this.id = id;
    }

    public String getAuthor()
    {
        return author;
    }

    public void setAuthor(String author)
    {
        this.author = author;
    }

    public double getPrice()
    {
        return price;
    }

    public void setPrice(double price)
    {
        this.price = price;
    }

    public int getPages()
    {
        return pages;
    }

    public void setPages(int pages)
    {
        this.pages = pages;
    }

    public String getName()
    {
        return name;
    }

    public void setName(String name)
    {
        this.name = name;
    }
}

Book类就会对应数据库中的Book表,而类中的每一个字段分别对应了表中的每一个列,这就是对象关系映射的最直观的体现。

 <list>
        <mapping class="com.example.litepaltest.Book"></mapping>
    </list>

<mapping>标签来声明我们要配置的映射模型类,注意一定要使用完整的类名。

button_create_database.setOnClickListener(new View.OnClickListener()
        {
            @Override
            public void onClick(View v)
            {
                Connector.getDatabase();
            }
        });

升级数据库的时候我们需要先把之前的表drop掉,然后再重新创建才行。这其实是一个非常严重的问题,因为这样会造成数据丢失,每当升级一次数据库,之前表中的数据就全没了。


image

当然如果你是非常有经验的程序员,也可以通过复杂的逻辑控制来避免这种情况,但是维护成本很高。

//Book表
private String press;

    public String getPress()
    {
        return press;
    }

    public void setPress(String press)
    {
        this.press = press;
    }
public class Category
{
    private int id;
    private String categoryName;
    private int categoryCode;

    public int getId()
    {
        return id;
    }

    public void setId(int id)
    {
        this.id = id;
    }

    public String getCategoryName()
    {
        return categoryName;
    }

    public void setCategoryName(String categoryName)
    {
        this.categoryName = categoryName;
    }

    public int getCategoryCode()
    {
        return categoryCode;
    }

    public void setCategoryCode(int categoryCode)
    {
        this.categoryCode = categoryCode;
    }
}

 <?xml version="1.0" encoding="utf-8" ?>
<litepal>

    <dbname value="BookStore"></dbname>

    <version value="2"></version>

    <list>
        <mapping class="com.example.litepaltest.Book"></mapping>
        <mapping class="com.example.litepaltest.Category"></mapping>
    </list>
</litepal>

改完了所有我们想要修改的东西,只需要记得将版本号加1就行了。

当然LitePal还自动帮我们做了一项非常重要的工作,就是保留之前表中的所有数据,这样就再也不用担心数据丢失的问题了

使用LitePal添加数据

LitePal进行表管理操作时不需要模型类有任何的继承结构,但是进行CRUD操作时就不行了。必须要继承自DataSupport类才行,因此我们需要先把继承结构给加上。

public class Book extends DataSupport
//添加数据
       button_add_data.setOnClickListener(new View.OnClickListener()
       {
           @Override
           public void onClick(View v)
           {
               Book book = new Book();

               book.setName("The Da Vinci Code");
               book.setAuthor("Dan Brown");
               book.setPages(454);
               book.setPrice(16.96);
               book.setPress("Unknow");

               book.save();
           }
       });

首先是创建出了一个Book的实例,然后调用Book类中的各种set方法对数据进行设置,最后在调用book.save()方法就能完成数据添加的操作了。save()方法是从DataSupport类中继承而来的。

image

使用LitePal更新数据

更新数据要比添加数据稍微为复杂一点,因为他的API接口比较多,这里我们只介绍最常用的几种更新方式。
最简单的一种更新方式就是对已存储的对象重新设值,然后重新调用save()方法即可。
对于LitePal来说,对象是否已存储就是根据调用model.isSaved()方法的结果来判断的,返回true就表示已存储,返回false就表示为存储。
实际上只有在两种情况下model.isSave()方法才会返回true,一种情况是已经调用过model.save()方法去添加数据了,此时model会被认为是已存储的对象。另一种情况是model对象是通过LitePal提供的查询API查出来的,由于是从数据库中查到的对象,因此也会被认为是已存储的对象。

//更新数据
        button_update_data.setOnClickListener(new View.OnClickListener()
        {
            @Override
            public void onClick(View v)
            {
                Book book = new Book();

                book.setName("The Lost Symbol");
                book.setAuthor("Dan Brown");
                book.setPages(510);
                book.setPrice(19.95);
                book.setPress("Unknow");

                book.save();

                book.setPrice(10.99);
                book.save();
            }
        });

先是通过上一小节中学习的知识添加了一条Book数据,然后调用setPrice()方法将这本书的价格进行了修改,之后再次调用了save()方法。此时LitePal会发现当前的Book对象是已存储的,因此不会再向数据库中去添加一条新数据,而是会直接更新当前的数据。


image

但是这种更新方式只能对已存储的对象进行操作,限制性表较大。

 Book book = new Book();
                book.setPrice(14.95);
                book.setPress("Anchor");
                book.updateAll("name = ? and author = ? ","The Lost Symbol","Dan Brown");

首先new出了一个Book的实例,然后直接调用setPrice()和setPress()方法来设置要更新的数据,最后在调用updateAll()方法去执行更新操作。注意updateAll()方法中可以指定一个约束条件,和SQLiteDatabase中update()方法的where参数部分有点类似,但更加简洁,如果不指定条件语句的话,就表示更新所有数据。


image

在使用updateAll()方法时,还有一个非常重要的知识点是你需要知道的,就是当你想把一个字段的值更新成默认值时,是不可以使用上面的方式来set数据的,我们都知道在Java中任何一种数据类型的字段都会有默认值,例如int类型的默认值是0,Boolean类型的默认值是false,String类型的默认值是null,那么当new出一个Book对象时,其实所有字段都已经被初始化成默认值了。如果我们想把数据库表中的pages列更新成0,直接调用set.Pages(0)是不可以的,因为即使不调用这行代码,pages字段本身也是0,LitePal此时是不会对这个列进行更新的。对于所有想要将要数据更新成默认值的操作,LitePal统一提供了一个setToDefault()方法,然后传入相应的列名就可以实现了。

Book book = new Book();
book.setToDefault("pages");
book.updateAll();

使用LitePal删除数据

使用LitePal删除数据的方式主要有两种,第一种比较简单,就是调用已存储对象的delete()方法就可以了,就是说调用过save()方法的对象,或者是通过LitePal提供的查询API查出来的对象,都是可以直接使用delete()方法来删除数据的。

//删除数据
        button_delete_data.setOnClickListener(new View.OnClickListener()
        {
            @Override
            public void onClick(View v)
            {
                DataSupport.deleteAll(Book.class,"price < ?","15");
            }
        });

调用了DataSupport.deleteAll()方法来删除数据,其中deleteAll()方法的第一个参数用于指定删除哪张表中的数据,Book.class就意味着删除Book表中的数据,后面的参数用于指定约束条件。


image

deleteAll()方法如果不指定约束条件,就意味着要删除表中的所有数据,这一点和updateAll()方法是比较相识的。

使用LiteP查询数据

findAll()方法的返回值是一个Book类型的List集合。

//查询数据
        button_query_data.setOnClickListener(new View.OnClickListener()
        {
            @Override
            public void onClick(View v)
            {
                List<Book> bookList = DataSupport.findAll(Book.class);

                for (Book book : bookList)
                {
                    Log.d(TAG, "book name is " + book.getName());
                    Log.d(TAG, "book author is " + book.getAuthor());
                    Log.d(TAG, "book pages is " + book.getPages());
                    Log.d(TAG, "book price is " + book.getPrice());
                    Log.d(TAG, "book press is " + book.getPress());
                }
            }
        });
image

除了findAll()方法之外,LitePal还提供了很多其他非常有用的查询API。

比如我们想要查询Book表中的第一条数据:
Book firstBook = DataSupport.findFirst(Book.class);

查询Book表中的最后一条数据就可以这样写:
Book lastBook = DataSupport.findLast(Book.class);

我们还可以通过连缀查询来定制更多的查询功能。

1.select()方法用于指定查询哪几列的数据,比如只查name和author这两列的数据,可以这样写:

List<Book> books = DataSupport.select("name","author").find(Book.class);

2.where()方法用于指定查询的约束条件,比如只查页数大于400的数据。可以这样写:

List<Book> books = DataSupport.where("pages > ?","400").find(Book.class);

3.order()方法用于指定结果的排序方式,默认是升序。比如将查询结果按照书价从高到低排序:

List<Book> books = DataSupport.order("price desc").find(Book.class);

4.limit()方法用于指定查询结果的数量,比如只查表中前三条数据:

List<Book> books = DataSupport.limit(3).find(Book.class);

5.offset()方法用于指定查询结果的偏移量,比如查询表中的第2条,第3条,第4条数据。

List<Book> books = DataSupport.limit(3).offset(1).find(Book.class);

你还可以对这5个方法进行任意的连缀组合。

List<Book> books = DataSupport.select("name","author","pages")
.where("pages > ?","400")
.order("pages")
.limit(10)
.offset(10)
.find(Book.class);

查询Book表中第11`20条满足页数大于400这个条件的name,author和pages这3列数据,并将查询结果按照页数升序排列。

如果你实在有一些特殊需求,上诉的API都满足不了你的时候,LitePal仍然支持使用原生的SQL来进行查询:

Cursor c = DataSupport.findBySQL("select * from Book where pages > ? and price < ?","400","20");

调用DataSupport.findBySQL()方法来进行原生查询,其中第一个参数用于指定SQL语句,后面的参数用于指定占位符的值。注意findBySQL()方法返回的是一个Cursor对象,接下来你还需要通过之前所学的老方式将数据一一取出才行。

推荐阅读更多精彩内容