Android – 数据持久化技术

7、 详解数据持久化技术

我们所使用的任何一个应用程序,无非就是在随时随地了和数据交道,我们平时看新闻,聊天,所关心是里面的数据,没有数据的应用程序就变成了一个空壳,对用户来说没有任何用途,那么这些数据人何而来呢?现在多数的数据基本都是由用户来产生的,比如我们发朋友圈,评论新闻,其实都是在产生数据。

7.1 持久化技术简介

数据持久化就是指将那些内在中的临时数据保存到存储设备中,保证即使设备在关机的情况下,这些数据出不会丢失,保存在内在中的数据都是处于临时状态的,而保存在存储设备中的数据都是处于持久状态的。持久化技术提供了一些机制,可以让数据在临时状态和持久状态下相互转换。

持久化技术被广泛应用于各种程序设计领域。Android系统中主要提供了3种方式用于简单地实现数据的持久化功能:文件存储、SharedPreferences存储以及数据存储。

7.2 文件存储

文件存储是Andorid中最基本的数据存储方式。它不对存储的内容进行任何格式化处理。所有数据都是原封不动地保存到文件当中的。因而它比较适合存储一些简单的文本数据或二进制数据,如果你想使用文件存储的方式来保存一些 较为复杂的结构化的数据,就需要定义自己的格式规范,以便之后将数据从文件中重新解析出来。

7.2.1 将数据存储到文件当中

Context类中提供了一个openFileOutput()方法,可以用于将数据存储 到指定 的文件中,这个方法接收两个参数:第一个参数是文件名,在文件创建的时候使用,注意这里指定 的名不可以包含路径,因为所有的文件都默认存储左/data/data/<package name>/files/目录下;第二个参数是文件的操作模式,主要有MODE_PRIVATE和MODE_APPEND两种模式可选,默认为 MODE_PRIVATE,表示指定相同文件名的时候,所写入的数据会覆盖原文件中的内容,而MODE_APPEND则表示如果该文件已存在,就往文件里面追加内容,不存在就创建文件。

其实文件的操作模式本来还有另外两种:MODE_WORLD_READABLE和MODE_WORLD_WRITEABLE 这两种模式表示允许其他应用程序对我们程序中的文件进行读写操作,不过由于这两种操作过于危险,很容易引起应用的安全漏洞,所以在Android4.2之后被废弃。

openFileOutput()方法返回一个FileOutputStream对象,得到这个对象之后就可以使用Java IO流的方式将数据定写到文件中了。

将数据存储到文件中:

添加一个EditText用于输入数据

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".MainActivity">

   <EditText
       android:id="@+id/edittest"
       android:maxLines="10"
       android:hint="年少有为"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"/>
</LinearLayout>

MainActivity.java

package com.ziyia.filepersistencetest;

import androidx.appcompat.app.AppCompatActivity;
import androidx.viewpager.widget.PagerAdapter;

import android.content.Context;
import android.os.Bundle;
import android.widget.EditText;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.nio.Buffer;

public class MainActivity extends AppCompatActivity {
   private final String FILENAME = "data";
   private EditText inputText;
   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_main);
       inputText = findViewById(R.id.edittest);
  }

   @Override
   protected void onDestroy() {
       super.onDestroy();
       String s = inputText.getText().toString();
       save(s);
  }

   private void save(String inputText) {
       BufferedWriter bufferedWriter = null;
       FileOutputStream data = null;
       try {
           data = openFileOutput("data", Context.MODE_APPEND);
           bufferedWriter = new BufferedWriter(new OutputStreamWriter(data));
           bufferedWriter.write(inputText);
           bufferedWriter.flush();
      } catch (IOException e) {
           e.printStackTrace();
      } finally {
           if (data != null) {
               try {
                   data.close();
              } catch (IOException e) {
                   e.printStackTrace();
              }
          }
           if (bufferedWriter != null) {

               try {
                   bufferedWriter.close();
              } catch (IOException e) {
                   e.printStackTrace();
              }
          }

      }
  }
}

可见,我们重写了onDestroy()方法,以确保在Activity销毁前一定会调用这个方法,在onDestroy()方法中,我们获取了EditText中输入的内容,并调用save() 方法来将输入的数据写入到data文件中,我们来运行一下程序,并输入一些数据,并点击Back键销毁Activity触发onDestroy()来将数据存储到文件中

我们来查看数据是否真的已经存储到文件当中

这样就证实了在EditText中输入的内容已经成功保存到文件中了。

7.22 从文件中读取数据

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

MainActivity.java

package com.ziyia.filepersistencetest;

import androidx.appcompat.app.AppCompatActivity;
import androidx.viewpager.widget.PagerAdapter;

import android.content.Context;
import android.os.Bundle;
import android.widget.EditText;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.nio.Buffer;

public class MainActivity extends AppCompatActivity {
   private final String FILENAME = "data";
   private EditText inputText;
   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_main);
       inputText = findViewById(R.id.edittest);

       String result = load();
       if (!result.isEmpty()) {
           inputText.setText(result);
           inputText.setSelection(inputText.length());
      }
  }

   @Override
   protected void onDestroy() {
       super.onDestroy();
       String s = inputText.getText().toString();
       save(s);
  }

   private String load() {
       StringBuffer buffer = null;
       FileInputStream fileInputStream = null;
       BufferedReader bufferedReader = null;
       try {
           String line = "";
           buffer = new StringBuffer();
           fileInputStream = openFileInput(FILENAME);
           bufferedReader = new BufferedReader(new InputStreamReader(fileInputStream));
           while ((line = bufferedReader.readLine()) != null) {
              buffer.append(line);
          }
           return buffer.toString();
      } catch (IOException e) {
           e.printStackTrace();
      } finally {
           if (fileInputStream != null) {
               try {
                   fileInputStream.close();
              } catch (IOException e) {
                   e.printStackTrace();
              }
          }
           if (fileInputStream != null) {
               try {
                   bufferedReader.close();
              } catch (IOException e) {
                   e.printStackTrace();
              }
          }
      }

       return buffer.toString();
  }

   private void save(String inputText) {
       BufferedWriter bufferedWriter = null;
       FileOutputStream data = null;
       try {
           data = openFileOutput("data", Context.MODE_APPEND);
           bufferedWriter = new BufferedWriter(new OutputStreamWriter(data));
           bufferedWriter.write(inputText);
           bufferedWriter.flush();
      } catch (IOException e) {
           e.printStackTrace();
      } finally {
           if (data != null) {
               try {
                   data.close();
              } catch (IOException e) {
                   e.printStackTrace();
              }
          }
           if (bufferedWriter != null) {

               try {
                   bufferedWriter.close();
              } catch (IOException e) {
                   e.printStackTrace();
              }
          }

      }
  }
}

7.3 SharedPreferences存储

不同于文件存储方式。SharedPreferences是使用键值对的方式来进行存储数据的。也就是说当保存一条数据的时候,需要给这条数据提供一个对应的键,这样在读取数据的时候就可以通过这个键把数据读取出来,而且SharedPreferences还支持多种数据类型的存储,如果存储的数据类型是整型,那么读取出来的数据也是整型的;如果存储的数据是字符串,那么读取出来的数据也是字符串。

这样就应该能明显地感觉到,使用SharedPreferences进行数据持久化要比使用文件方便很多。

7.3.1 将数据存储到SharedPreferences中

要使用SharedPreferences存储数据,首选需要获取SharedPreferences对象。Android中主提供了以下两种方法用于得到SharedPreferences对象。

  1. Conteext类中的getSharedPreferences() 方法此方法接收两个参数:第一个参数用于指定SharedPreferences文件的名称,如果指定的文件不存在则会创建一个。SharedPreferences文件都是存放在/data/data/<package name>/shared_precfs/目录下的;第二个参数用于指定操作模式,目前只胡默认的MODE_FRIVATE一种可选,它和直接传入0的效果相同,表示只有当前的应用程序才可以对这个SharedPreferences文件进行读写,其他几种操作模式已废弃,MODE_WORLD_READABLET和MODE_WORLD_WRITEABLE这两种模式是在Android4.2版本中被废弃的,MODE_NULTI_FROCESS模式是在Android6.0版本中被废弃的。
  2. Activity类中的getPreferences() 方法这个方法和Context中的getSaredPreferences() 方法很类似,不过它只是接收一个操作模式参数,因为使用这个方法时会自动将当前Activity的类名作为SharedPreferences的文件名。得到了SharedPreferences对象之后,就可以开始向 SharedPreferences文件中存储数据了。简单分为三步:
    1. 调用SharedPreferences对象的edit()方法获取一个SharedPreferences.Editor对象。
    2. 向SharedPreferences.Editor对象中添加数据,比如添加一个布尔数据就使用putBoolean()方法,添加一个字符串则使用putString() 方法,以此类推。
    3. 调用apply() 方法将添加的数据提交,从而完成数据存储操作。
    实现数据的存储:activity_main.xml<?xml version=”1.0″ encoding=”utf-8″?>
    <LinearLayout xmlns:android=”http://schemas.android.com/apk/res/android”
       android:orientation=”vertical”
       xmlns:tools=”http://schemas.android.com/tools”
       android:layout_width=”match_parent”
       android:layout_height=”match_parent”
       tools:context=”.MainActivity”>

       <Button
           android:id=”@+id/saveButton”
           android:text=”Save Data”
           android:layout_width=”match_parent”
           android:layout_height=”wrap_content”/>
    </LinearLayout>

MainActivity.java

package com.ziyia.sharedpreferencestest;

import androidx.appcompat.app.AppCompatActivity;

import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;

public class MainActivity extends AppCompatActivity {
   private Button saveButton;


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

       saveButton.setOnClickListener(new View.OnClickListener() {
           @Override
           public void onClick(View v) {
               SharedPreferences.Editor data = getSharedPreferences("data", Context.MODE_PRIVATE).edit();
               data.putString("name", "年少有为");
               data.putInt("age", 18);
               data.apply();
          }
      });
  }

   private void init() {
       saveButton = findViewById(R.id.saveButton);
  }
}

运行程序,点击按钮后我们打开/data/data/<package name>/shared_prefs/data.xml文件,会发现我们存储的数据,如图

可见,我们的数据已经成功保存下来了,并且是以Sharedpreferences文件是以XML格式来对数据进行管理的。

7.3.2 从SharedPreferences中读取数据

使用SharedPreferences是非常简单的,使用SharPreferences读取数据将会更加简单,SharedPreferences对象已经提供了一系列的get方法,每种get方法都对应了SharPreferences.Editor中的put方法。比如读取一个字符串就使用getString方法,读取一个数字就使用getInt方法。这些get方法都接收两个参数:第一个参数是键,传入的是存储数据时使用的健就可以获取相应的值了;第二个参数是默认值,即表示当传入的健找不到对应的值时会以什么样的值进行返回。

使用SharedPreferences读取我们存储的数据,还是在上方例子的基本上进行修改。

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:orientation="vertical"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".MainActivity">

   <Button
       android:id="@+id/saveButton"
       android:text="Save Data"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"/>

   <Button
       android:id="@+id/restoreButton"
       android:text="Restore Data"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"/>
</LinearLayout>

MainActivity.java

package com.ziyia.sharedpreferencestest;

import androidx.appcompat.app.AppCompatActivity;

import android.content.Context;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {
   private Button saveButton, restoreButton;


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

       saveButton.setOnClickListener(new View.OnClickListener() {
           @Override
           public void onClick(View v) {
               SharedPreferences.Editor data = getSharedPreferences("data", Context.MODE_PRIVATE).edit();
               data.putString("name", "年少有为");
               data.putInt("age", 18);
               data.apply();
          }
      });

       restoreButton.setOnClickListener(new View.OnClickListener() {
           @Override
           public void onClick(View v) {
               SharedPreferences data = getSharedPreferences("data", Context.MODE_PRIVATE);
               String name = data.getString("name", "");
               Integer age = data.getInt("age", 0);
               Toast.makeText(MainActivity.this, "name = " + name + ",age = " + age, Toast.LENGTH_SHORT).show();

          }
      });

  }

   private void init() {
       saveButton = findViewById(R.id.saveButton);
       restoreButton = findViewById(R.id.restoreButton);
  }
}

效果

所有存储的数据已经读取出来了,SharPreferences和文件存储相比之下简单方便了很多。

7.3.3 简单实现记住密码功能

文件清单:

  1. ActivityCollector.java
  2. BaseActivity.java
  3. LoginActivity.java
  4. MainActivityjava
  5. activity_main.xml
  6. activity_login.xml
  7. AndroidManifest.xml

ActivityCollector.java

package com.ziyia.broacastbasepractivity;

import android.app.Activity;

import java.util.ArrayList;
import java.util.List;

public class ActivityCollector {
   private static final List<Activity> list = new ArrayList<>();

   public static void addActivity(Activity activity) {
       list.add(activity);
  }

   public static void removeActivity(Activity activity) {
       list.remove(activity);
  }

   public static void finishAll() {
       for (Activity activity : list) {
           list.remove(activity);
      }
       list.clear();
  }
}

BaseActivity.java

package com.ziyia.broacastbasepractivity;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;

import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;

public class BaseActivity extends AppCompatActivity {
   private ForceOffLineReceiver forceOffLineReceiver;
   @Override
   protected void onCreate(@Nullable @org.jetbrains.annotations.Nullable Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       ActivityCollector.addActivity(this);
  }

   @Override
   protected void onResume() {
       super.onResume();
       IntentFilter intentFilter = new IntentFilter();
       intentFilter.addAction("com.ziyia.broacastbasepractivity.FORCE_OFFLINE");
       forceOffLineReceiver = new ForceOffLineReceiver();
       registerReceiver(forceOffLineReceiver, intentFilter);
  }

   @Override
   protected void onPause() {
       super.onPause();
       unregisterReceiver(forceOffLineReceiver);

  }

       @Override
       protected void onDestroy () {
           super.onDestroy();
           ActivityCollector.removeActivity(this);
      }

       class ForceOffLineReceiver extends BroadcastReceiver {
           @Override
           public void onReceive(Context context, Intent intent) {
               AlertDialog.Builder builder = new AlertDialog.Builder(context);
               builder.setTitle("Warning");
               builder.setMessage("You are forced to be offline. Please try to login again.");
               builder.setPositiveButton("OK", new DialogInterface.OnClickListener() {
                   @Override
                   public void onClick(DialogInterface dialog, int which) {
                       ActivityCollector.finishAll();
                       Intent intent1 = new Intent(context, LoginActivity.class);
                       startActivity(intent1);
                  }
              });
               builder.setCancelable(false);
               builder.create();
               builder.show();
          }
      }
  }

LoginActivity.java

package com.ziyia.broacastbasepractivity;

import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.Toast;

import androidx.annotation.Nullable;

public class LoginActivity extends BaseActivity {
   private EditText account, password;
   private CheckBox checkBox;
   private Button login;
   @Override
   protected void onCreate(@Nullable @org.jetbrains.annotations.Nullable Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_login);
       account = findViewById(R.id.accountEdit);
       password = findViewById(R.id.passwordEdit);
       checkBox = findViewById(R.id.rememberPass);
       login = findViewById(R.id.login);


       SharedPreferences prefs = getPreferences(Context.MODE_PRIVATE);
       boolean remember_password1 = prefs.getBoolean("remember_password", false);

       if (remember_password1) {
           account.setText(prefs.getString("account", ""));
           password.setText(prefs.getString("password", ""));
           checkBox.setChecked(true);
      }

       login.setOnClickListener(new View.OnClickListener() {
           @Override
           public void onClick(View v) {
               String a = account.getText().toString();
               String p = password.getText().toString();
               if ("admin".equals(a) && "123456".equals(p)) {

                   SharedPreferences.Editor edit = prefs.edit();
                   if (checkBox.isChecked()) {
                       edit.putBoolean("remember_password", true);
                       edit.putString("account", a);
                       edit.putString("password", p);
                  } else {
                       edit.clear();
                  }
                   edit.apply();
                   Intent intent = new Intent(LoginActivity.this, MainActivity.class);
                   startActivity(intent);
                   finish();
              } else {
                   Toast.makeText(LoginActivity.this, "帐号或密码不正确", Toast.LENGTH_SHORT).show();
              }
          }
      });

  }
}

MainActivityjava

package com.ziyia.broacastbasepractivity;

import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;

public class MainActivity extends BaseActivity {


   @Override
   protected void onCreate(@Nullable @org.jetbrains.annotations.Nullable Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_main);
       View btn1 = findViewById(R.id.forceOffline);

       btn1.setOnClickListener(new View.OnClickListener() {
           @Override
           public void onClick(View v) {
               Intent intent = new Intent("com.ziyia.broacastbasepractivity.FORCE_OFFLINE");
               sendBroadcast(intent);
          }
      });

  }

}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:orientation="vertical"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".MainActivity">
   <Button
       android:id="@+id/forceOffline"
       android:text="强制下载"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"/>
</LinearLayout>

activity_login.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:orientation="vertical"
   android:layout_width="match_parent"
   android:layout_height="match_parent">

   <LinearLayout
       android:orientation="horizontal"
       android:layout_width="match_parent"
       android:layout_height="60dp">
       <TextView
           android:text="Account:"
           android:layout_width="90dp"
           android:layout_height="wrap_content"
           android:layout_gravity="center_vertical"
           android:textSize="18sp"/>

       <EditText
           android:layout_gravity="center_vertical"
           android:id="@+id/accountEdit"
           android:layout_weight="1"
           android:layout_width="0dp"
           android:layout_height="wrap_content"/>
   </LinearLayout>
   <LinearLayout
       android:orientation="horizontal"
       android:layout_width="match_parent"
       android:layout_height="60dp">
       <TextView
           android:text="Password:"
           android:layout_width="90dp"
           android:layout_height="wrap_content"
           android:layout_gravity="center_vertical"
           android:textSize="18sp"/>

       <EditText
           android:layout_gravity="center_vertical"
           android:id="@+id/passwordEdit"
           android:layout_weight="1"
           android:layout_width="0dp"
           android:layout_height="wrap_content"/>
   </LinearLayout>

   <LinearLayout
       android:orientation="horizontal"
       android:layout_width="match_parent"
       android:layout_height="wrap_content">
       <CheckBox
           android:id="@+id/rememberPass"
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"/>
       <TextView
           android:text="记叙密码"
           android:textSize="18sp"
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"/>
   </LinearLayout>
   <Button
       android:layout_gravity="center_horizontal"
       android:id="@+id/login"
       android:text="Login"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"/>
</LinearLayout>

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
   package="com.ziyia.broacastbasepractivity">

   <application
       android:allowBackup="true"
       android:icon="@mipmap/ic_launcher"
       android:label="@string/app_name"
       android:roundIcon="@mipmap/ic_launcher_round"
       android:supportsRtl="true"
       android:theme="@style/Theme.MyApplication">


       <activity android:name=".MainActivity">

       </activity>
       <activity android:name=".LoginActivity">
           <intent-filter>
               <action android:name="android.intent.action.MAIN"/>
               <category android:name="android.intent.category.LAUNCHER"/>
           </intent-filter>
       </activity>
   </application>

</manifest>

效果\

记住密码的功能仍然是个简单的示例,不能在实际的项目中使用。因为将密码以明文的形式存储在SharedPreferences文件中是非常不安全的。很容易被别人盗取。因此在项目里必须结合一定的加密算法对密码进行保护才行。

7.4 SQLite数据库存储

SQLite是一款轻量级的关系型数据库,它的运算速度非常快,占用资源很少,通常只需要几百KB的内在就足够了。因而特别适合在移动设备上使用。SQLite不仅支持标准的SQL语法,还遵循了数据库的ACID事务。所以只要你在这之前学过其他的关系型数据库,就可以很快地上手SQLite。而SQLite又比一些数据库简单得多,它甚至不用设置用户名和密码就可以使用。Android正是把这个功能极为强大的数据库嵌入到了系统当中。使得本地持久化的功能有了一次性质的飞跃,芜湖。

SharedPreferences存储毕竟只适用于保存一些简单的数据和键值对,当需要存储大量复杂的关系型数据的时候,你就会发现以上两种存储方式都很难应付得了。比如我们手机的短信程序中可以会有很多个会话,每个会话中又包含很多信息内容,并且大部分会话还可能各自对应通讯录的某个联系人。

7.4.1 创建数据库

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

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

SQLiteOpenHelper中还有两个非常重要的实例方法,getReadableDatabase() 和 getWriteDatabase() 这两个方法都 可以创建或打开一个现有的数据库(如果数据库已存在则直接打开,否则要创建一个新的数据库)。并返国不念旧恶可对数据库进行读写操作的对象。不同的是。当数据库不可写入的时候(比如磁盘空间已满)。getReadableDatabase() 方法返回的对象得以只读的方式打开数据库,而getWrireableDatabase() 方法则将出现异常。这各个构造方法可以重写,我们一般用参数少一点的那个构造方法即可,该构造方法接收4个参数:第一个参数是Context;第二个参数是数据库名;第三个参数允许我们在查询数据的时候返回一个自定义的Cursor,我们一般传null即可;第四个参数表示当前数据库的版本号,可用于对数据库进行升级操作。构造出他的实例后,再通过它的getReadableDatabas() 和 getWriteableDatabase() 方法就能够创建数据库了。

数据库文件存储在/data/data/<package name>/databases/目录下。此时,重写的onCreate() 方法也会得到执行,所以通常会在这里处理一些创建表的逻辑。

MainActivity.java

package com.ziyia.databasetest;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.view.View;
import android.widget.Button;

public class MainActivity extends AppCompatActivity {

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

       createDatabase = findViewById(R.id.createDatabase);

       MyDatabaseHelper myDatabaseHelper = new MyDatabaseHelper(this, "BookStrore.db", null, 1);

       createDatabase.setOnClickListener(new View.OnClickListener() {
           @Override
           public void onClick(View v) {
               myDatabaseHelper.getWritableDatabase();
          }
      });
  }
}

创建MyDatabaseHelper去继承SQLiteOpenHelper并实现onCreate() 和 onUpgrade() 方法

MyDatabaseHelper.java

package com.ziyia.databasetest;

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.widget.Toast;
import androidx.annotation.Nullable;

public class MyDatabaseHelper extends SQLiteOpenHelper {

   private Context context;
   private String createBook = "create table Book(" +
           "id integer primary key autoincrement," +
           "author text," +
           "price real," +
           "pages integer," +
           "name text);";

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


   @Override
   public void onCreate(SQLiteDatabase db) {
       db.execSQL(createBook);
       Toast.makeText(context, "数据库创建成功", Toast.LENGTH_SHORT).show();
  }

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

  }
}

添加一个按钮来响应创建数据库。

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:orientation="vertical"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".MainActivity">

   <Button
       android:text="@string/createDatabase"
       android:id="@+id/createDatabase"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"/>

</LinearLayout

7.4.2 升级数据库

如果你留意了上方的代码,你会发现MyDatabaseHelper中还有一个空方法onUpgrade() 该方法是用于对数据库进行升级的。它在整个数据库的管理工作中起着非常重要的作用。

目前数据库中已经有一张Book表用于存放各种详细数据,如果我们想再添加一张Category表用于记录图书的分类

比如我们Category表中有id(主键),分类名及分类代码这几个类,建表语句就可以写成:

create table Catrgory (id integer primary key autoincrement, catgory_name text, category_code integer);

我们将这条建表语句添加到MyDatabaseHelper去执行,代码如下:

package com.ziyia.databasetest;

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.widget.Toast;
import androidx.annotation.Nullable;

public class MyDatabaseHelper extends SQLiteOpenHelper {

   private Context context;

   private String createCategory = "create table Category (id integer primary key autoincrement, catgory_name text, category_code integer);";

   private String createBook = "create table Book(" +
           "id integer primary key autoincrement," +
           "author text," +
           "price real," +
           "pages integer," +
           "name text);";

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


   @Override
   public void onCreate(SQLiteDatabase db) {
       db.execSQL(createBook);
       db.execSQL(createCategory);
       Toast.makeText(context, "数据库创建成功", Toast.LENGTH_SHORT).show();
  }

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

  }
}

看上去好像没有什么问题,我们运行程序,并点击 “创建数据库” 按钮,你会发现,竟然没有弹出创建成功的提示,我们再通过工具去/data/data/<package name>/databases/目录查看,没有Category表,说明onCreate() 方法并没有执行。

没有创建成功的原因我们不难思考,因为BookStrore.db数据库已经存在了,之后不管我们如何点击 “创建数据库” 按钮中的onCreate() 方法都不会执行,因此,新表就无法得到创建了。

解决这个问题的办法也相当简单,只需要把程序卸载,然后重新编译安装,这时BookStrore.db数据库已经不存在了,如果再点击 “创建数据库” 按钮,MyDatabaseHelper中的onCreate() 方法则会执行,这时Category表就可以创建成功了,嘿嘿。

不过,通过卸载程序的方式来新增一张表毫无疑问是很极端的做法,笨蛋,其实我们需要巧妙地运用SQLiteOpenHelper的升级功能,就可以很轻松地解决这个问题。修改MyDatabaseHelper中代码如下:

package com.ziyia.databasetest;

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.widget.Toast;
import androidx.annotation.Nullable;

public class MyDatabaseHelper extends SQLiteOpenHelper {

   private Context context;

   private String createCategory = "create table Category (id integer primary key autoincrement, catgory_name text, category_code integer);";

   private String createBook = "create table Book(" +
           "id integer primary key autoincrement," +
           "author text," +
           "price real," +
           "pages integer," +
           "name text);";

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


   @Override
   public void onCreate(SQLiteDatabase db) {
       db.execSQL(createBook);
       db.execSQL(createCategory);
       Toast.makeText(context, "数据库创建成功", Toast.LENGTH_SHORT).show();
  }

   @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() 方法中执行了两条drop语句,如果数据库是存在判断的表则删除,然后调用onCreate() 方法来重新创建,把表删除的原因是,如果创建重复的表,就会报错。

最后,我们只要让onUpgrade() 方法执行,问题就解决了,该方法执行的前提是,本次版本号大于上次数据库的版本号,还记得SQLiteHelper构造器中的第四个参数吗?它表示本次创建数据库的版本号,之前我们传递的是1,则创建的数据库对应的版本号是1,我们如果想要让onUpgrade() 方法执行,只要在构造器中传入一个比1大的值,就可以让onUpgrade() 方法得到执行了。

package com.ziyia.databasetest;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.view.View;
import android.widget.Button;

public class MainActivity extends AppCompatActivity {

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

       createDatabase = findViewById(R.id.createDatabase);

       MyDatabaseHelper myDatabaseHelper = new MyDatabaseHelper(this, "BookStrore.db", null, 2);

       createDatabase.setOnClickListener(new View.OnClickListener() {
           @Override
           public void onClick(View v) {
               myDatabaseHelper.getWritableDatabase();
          }
      });
  }
}

这里我们将版本号指定为2,比当前本地已创建的数据库版本号高,表示我们对数据库进行升级了,重新编译运行程序,点击 ”创建数据库“ 按钮,此时 已经弹出”数据库创建成功的提示了“

我们去程序数据库中查看,确实创建了

7.4.3 添加数据

其实我们对数据库中的操作有4种,即CRUD,C代表(create),R代表(retrieve),U代表(update),D代表(delete)每种操作都对应了一种SQL命令,如果你在这之前学过其他数据库,一定会知道添加数据时使用 insert,更新数据时用 update,删除数据时用 delete,查询数据时用 select。但是开发都的水平都是参差不齐的,未必每一个人都能非常熟悉SQL语言,因此 Android提供了一系列的辅助方法,让你在Android中即使不用写SQL语句,也能轻松完成CRUD操作。

经过前面的学习我们知道,调用SQLiteOpenHelper的getReadableDatabase() 或 getWrieaableDatabase() 方法是可以用于创建和升级数据库的。不仅如此,这两个方还都会返回一个SqliteDatabase对象,我们可以通过这个对象对数据进行CRUD操作。

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

具体使用看下方例子:

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:orientation="vertical"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".MainActivity">

   <Button
       android:text="@string/createDatabase"
       android:id="@+id/createDatabase"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"/>

   <Button
       android:id="@+id/insert"
       android:text="@string/addData"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"/>
</LinearLayout>

MainActivity.java

package com.ziyia.databasetest;

import androidx.appcompat.app.AppCompatActivity;

import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;

public class MainActivity extends AppCompatActivity {

   private Button createDatabase, insert, update, delete, select;

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

       createDatabase = findViewById(R.id.createDatabase);
       insert = findViewById(R.id.insert);

       myDatabaseHelper = new MyDatabaseHelper(this, "BookStrore.db", null, 2);
       createDatabase.setOnClickListener(new View.OnClickListener() {
           @Override
           public void onClick(View v) {
               myDatabaseHelper.getWritableDatabase();
          }
      });

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

   private void insert() {
       SQLiteDatabase writableDatabase = myDatabaseHelper.getWritableDatabase();

       // 组装第一条数据
       ContentValues contentValues = new ContentValues();
       contentValues.put("name", "图解HTTP");
       contentValues.put("author", "Ban Brown");
       contentValues.put("pages", 400);
       contentValues.put("price", 16.96);
       // 插入第一条数据
       writableDatabase.insert("Book",null, contentValues);

       contentValues.clear();

       // 组装第二条数据
       contentValues.put("name", "Java编程思想");
       contentValues.put("author", "Ban Brown");
       contentValues.put("pages", 700);
       contentValues.put("price", 96.96);

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

点击 “插入数据”后去查看数据库

7.4.4 更新数据

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

具体使用看下方例子:

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:orientation="vertical"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".MainActivity">

   <Button
       android:text="@string/createDatabase"
       android:id="@+id/createDatabase"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"/>

   <Button
       android:id="@+id/insert"
       android:text="@string/addData"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"/>

   <Button
       android:id="@+id/update"
       android:text="修改数据"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"/>

</LinearLayout>

MainActivity.java

package com.ziyia.databasetest;

import androidx.appcompat.app.AppCompatActivity;

import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;

public class MainActivity extends AppCompatActivity {

   private Button createDatabase, insert, update, delete, select;

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

       createDatabase = findViewById(R.id.createDatabase);
       insert = findViewById(R.id.insert);
       update = findViewById(R.id.update);
       delete = findViewById(R.id.delet);
       select = findViewById(R.id.select);

       myDatabaseHelper = new MyDatabaseHelper(this, "BookStrore.db", null, 2);
       createDatabase.setOnClickListener(new View.OnClickListener() {
           @Override
           public void onClick(View v) {
               myDatabaseHelper.getWritableDatabase();
          }
      });

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

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


   private void insert() {
       SQLiteDatabase writableDatabase = myDatabaseHelper.getWritableDatabase();

       // 组装第一条数据
       ContentValues contentValues = new ContentValues();
       contentValues.put("name", "图解HTTP");
       contentValues.put("author", "Ban Brown");
       contentValues.put("pages", 400);
       contentValues.put("price", 16.96);
       // 插入第一条数据
       writableDatabase.insert("Book",null, contentValues);


       contentValues.clear();

       // 组装第二条数据
       contentValues.put("name", "Java编程思想");
       contentValues.put("author", "Ban Brown");
       contentValues.put("pages", 700);
       contentValues.put("price", 96.96);

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

   private void update() {
       SQLiteDatabase writableDatabase = myDatabaseHelper.getWritableDatabase();

       ContentValues contentValues = new ContentValues();
       contentValues.put("price", 1000);

       //
       writableDatabase.update("Book", contentValues, "name = ?", new String[]{"图解HTTP"});

  }

}

点击 “修改数据” 后去查看数据库库

7.4.5 删除数据

删除数据相对方面两个例子来说更简单,SQLiteDatabase中提供了一个delete() 方法,专门 用于删除数据。这个方法接收3个参数:第一个参数仍然是表名;第二个及第三个参数用于约束删除的某一行或某几行数据,不指定的情况下默认会删除所有行。

具体使用看下方例子:

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:orientation="vertical"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".MainActivity">

   <Button
       android:text="@string/createDatabase"
       android:id="@+id/createDatabase"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"/>

   <Button
       android:id="@+id/insert"
       android:text="@string/addData"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"/>

   <Button
       android:id="@+id/update"
       android:text="修改数据"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"/>

   <Button
       android:id="@+id/delet"
       android:text="删除数据"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"/>
</LinearLayout>

MainActivity.java

package com.ziyia.databasetest;

import androidx.appcompat.app.AppCompatActivity;

import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;

public class MainActivity extends AppCompatActivity {

   private Button createDatabase, insert, update, delete, select;

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

       createDatabase = findViewById(R.id.createDatabase);
       insert = findViewById(R.id.insert);
       update = findViewById(R.id.update);
       delete = findViewById(R.id.delet);

       myDatabaseHelper = new MyDatabaseHelper(this, "BookStrore.db", null, 2);
       createDatabase.setOnClickListener(new View.OnClickListener() {
           @Override
           public void onClick(View v) {
               myDatabaseHelper.getWritableDatabase();
          }
      });

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

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

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


   private void insert() {
       SQLiteDatabase writableDatabase = myDatabaseHelper.getWritableDatabase();

       // 组装第一条数据
       ContentValues contentValues = new ContentValues();
       contentValues.put("name", "图解HTTP");
       contentValues.put("author", "Ban Brown");
       contentValues.put("pages", 400);
       contentValues.put("price", 16.96);
       // 插入第一条数据
       writableDatabase.insert("Book",null, contentValues);


       contentValues.clear();

       // 组装第二条数据
       contentValues.put("name", "Java编程思想");
       contentValues.put("author", "Ban Brown");
       contentValues.put("pages", 700);
       contentValues.put("price", 96.96);

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

   private void update() {
       SQLiteDatabase writableDatabase = myDatabaseHelper.getWritableDatabase();

       ContentValues contentValues = new ContentValues();
       contentValues.put("price", 1000);

       //
       writableDatabase.update("Book", contentValues, "name = ?", new String[]{"图解HTTP"});

  }


   private void select () {
       SQLiteDatabase writableDatabase = myDatabaseHelper.getWritableDatabase();
       Cursor book = writableDatabase.query("Book", null, null, null, null, null, null);
//       while (book.moveToFirst()) {
//           Log.d("select", book.toString());
//       }

       if (book.moveToFirst()) {
           do {
               String name = book.getString(book.getColumnIndex("name"));
               Log.d("select", name);
          } while (book.moveToNext());
      }
  }

}

点击 “删除数据” 后,可见 “Java编程思想“ 这条记录已经被删除了

7.4.6 查询数据

SQL人全称(Structured Query Language)翻译成中文就是 “结构化查询语言”,大部分功能都体现在 “查”这个字上,而 ”增删改“只是其中的一小部分功能。

SQLiteDatabase中还提供了一个query() 方法用于对数据进行查询。这个方法的参数非常复杂。最短的一个方法重载也需要传入7个参数:第一个参数仍然还是表名;第二个参数用于指定去查询哪几列,如果不指则查询所有列;第三个及第四个参数用于约束查询某一行或某几行的数据,不指定则查询所有行的数据;第五个参数用于指定需要去group by之后的数据进行进一步的过滤,在MySqla中称为分组,不指定则不进行group by 操作;第六个参数用于对group by 之后的数据进行进一步过滤,在MySq中称为分组后筛选,第七个参数用于指定查询结果的排序方式,不指定则表示使用默认的排序方式。

query()方法参数对应的SQL部分描述
tablefrom table name指定查询的表名
columnsselect column1, column2指定查询的列名
selectionwhere column = value指定where的约束条件
selectionArgs为where中的占位符使用具体的值
groupBygroup by colum指定需要group by的列
havinghaving column = value对group by后的结果进一步约束
orderByordeer by column1, column2指定查询的结果的排序方式

虽然query() 方法的参数非常多,但是不要害怕,因为我们不必为每个查询语句都指定所有参数,多数情况下只需要传入少数几个参数就可以完成查询操作了。调用query() 方法后会返回一个Cursor对象,查询的结果集将从这个对象中取出。

具体使用看下方例子:

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:orientation="vertical"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".MainActivity">

   <Button
       android:text="@string/createDatabase"
       android:id="@+id/createDatabase"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"/>

   <Button
       android:id="@+id/insert"
       android:text="@string/addData"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"/>

   <Button
       android:id="@+id/update"
       android:text="修改数据"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"/>

   <Button
       android:id="@+id/delet"
       android:text="删除数据"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"/>

   <Button
       android:id="@+id/select"
       android:text="查询数据"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"/>
</LinearLayout>

MainActivity.java

package com.ziyia.databasetest;

import androidx.appcompat.app.AppCompatActivity;

import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;

public class MainActivity extends AppCompatActivity {

   private Button createDatabase, insert, update, delete, select;

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

       createDatabase = findViewById(R.id.createDatabase);
       insert = findViewById(R.id.insert);
       update = findViewById(R.id.update);
       delete = findViewById(R.id.delet);
       select = findViewById(R.id.select);

       myDatabaseHelper = new MyDatabaseHelper(this, "BookStrore.db", null, 2);
       createDatabase.setOnClickListener(new View.OnClickListener() {
           @Override
           public void onClick(View v) {
               myDatabaseHelper.getWritableDatabase();
          }
      });

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

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

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

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


   private void insert() {
       SQLiteDatabase writableDatabase = myDatabaseHelper.getWritableDatabase();

       // 组装第一条数据
       ContentValues contentValues = new ContentValues();
       contentValues.put("name", "图解HTTP");
       contentValues.put("author", "Ban Brown");
       contentValues.put("pages", 400);
       contentValues.put("price", 16.96);
       // 插入第一条数据
       writableDatabase.insert("Book",null, contentValues);


       contentValues.clear();

       // 组装第二条数据
       contentValues.put("name", "Java编程思想");
       contentValues.put("author", "Ban Brown");
       contentValues.put("pages", 700);
       contentValues.put("price", 96.96);

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

   private void update() {
       SQLiteDatabase writableDatabase = myDatabaseHelper.getWritableDatabase();

       ContentValues contentValues = new ContentValues();
       contentValues.put("price", 1000);

       //
       writableDatabase.update("Book", contentValues, "name = ?", new String[]{"图解HTTP"});

  }


   private void select () {
       SQLiteDatabase writableDatabase = myDatabaseHelper.getWritableDatabase();
       Cursor book = writableDatabase.query("Book", null, null, null, null, null, null);
//       while (book.moveToFirst()) {
//           Log.d("select", book.toString());
//       }

       if (book.moveToFirst()) {
           do {
               String name = book.getString(book.getColumnIndex("name"));
               Log.d("select", name);
          } while (book.moveToNext());
      }
  }

   private void delete() {
       SQLiteDatabase writableDatabase = myDatabaseHelper.getWritableDatabase();
       writableDatabase.delete("Book", "pages > ?", new String[]{"500"});
  }
}

点击 ”查询数据“ 我们只是简单取出name

7.4.7 使用SQL操作SQLite数据库

虽然Android已经给我们提供了很多非常方便的API,用于操作数据库。不过总会有一些人不习惯使用这些辅助性的方法,笔者就很不喜欢。而是更加青味于直接使用SQL来操作数据库。如果你也是其中之一的话,那么恭喜,Android充分考虑到了咱们的编程习惯,同样提供了一系列的方法,使得我们可以直接使用SQL去操作数据库。

具体使用看下方例子:

insert

db.execSQL(“INSERT INTO Book(name, author, pages, price) VALUES (?, ?, ?, ?);”, new String[]{“年少有为”, “嘿嘿”, “500”, “78.5”});

update

db.execSQL(“UPDATE Book SET price = ? WHERE name = ?”, new String[]{“10.5”. “嘿嘿”});

delete

db.execSQL(“DELETE FROM Book WHERE pages > ?”, new String[]{“500”});

select

db.execSQL(“SELECT * FROM Book”, null);

7.5 实践

7.5.1 使用事务

SQLite是支持事务的,事务的特性可以保证让一系列的操作要么全部完成,要么一个都不会完成,那么在什么情况下才需要使用事务呢?想象以下场景,比如你正在进行一次转帐操作,银行会先将转帐的金额从你的帐户中扣除,然后再向收款方的帐户中添加等量的金额,看上去好像没有什么问题对吧?可以,如果当你帐户中的金额刚刚被扣除,这时由于一些异常原因导致对方收款失败,这一部分钱就凭空消失了?当然银行肯定已经充分考虑到了这种情况,它会保证扣款和收款的操作要么都成功,要么都不成功,其中使用的技术就是事务了。

具体使用看下方例子:我们还是在之前的代码上做修改

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:orientation="vertical"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".MainActivity">

   <Button
       android:text="@string/createDatabase"
       android:id="@+id/createDatabase"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"/>

   <Button
       android:id="@+id/insert"
       android:text="@string/addData"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"/>

   <Button
       android:id="@+id/update"
       android:text="修改数据"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"/>

   <Button
       android:id="@+id/delet"
       android:text="删除数据"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"/>

   <Button
       android:id="@+id/select"
       android:text="查询数据"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"/>

   <Button
       android:id="@+id/replaceData"
       android:text="提交事务"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"/>
</LinearLayout>

MainActivity.java

package com.ziyia.databasetest;

import androidx.appcompat.app.AppCompatActivity;

import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.ExpandableListAdapter;

public class MainActivity extends AppCompatActivity {

   private Button createDatabase, insert, update, delete, select, replaceData;

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

       createDatabase = findViewById(R.id.createDatabase);
       insert = findViewById(R.id.insert);
       update = findViewById(R.id.update);
       delete = findViewById(R.id.delet);
       select = findViewById(R.id.select);
       replaceData = findViewById(R.id.replaceData);

       myDatabaseHelper = new MyDatabaseHelper(this, "BookStrore.db", null, 2);
       createDatabase.setOnClickListener(new View.OnClickListener() {
           @Override
           public void onClick(View v) {
               myDatabaseHelper.getWritableDatabase();
          }
      });

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

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

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

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


       replaceData.setOnClickListener(new View.OnClickListener() {
           @Override
           public void onClick(View v) {
               transaction();
          }
      });
  }

   private void transaction() {
       SQLiteDatabase db = myDatabaseHelper.getWritableDatabase();
       // 开启事务
       db.beginTransaction();
       try {
           db.delete("Book", null, null);
           if (true) {
               throw new NullPointerException();
          }
           ContentValues contentValues = new ContentValues();
           contentValues.put("name", "Game of Thrones");
           contentValues.put("author","George Martin");
           contentValues.put("pages", 500);
           contentValues.put("price", 99.9);

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

  }

   private void insert() {
       SQLiteDatabase writableDatabase = myDatabaseHelper.getWritableDatabase();

       // 组装第一条数据
       ContentValues contentValues = new ContentValues();
       contentValues.put("name", "图解HTTP");
       contentValues.put("author", "Ban Brown");
       contentValues.put("pages", 400);
       contentValues.put("price", 16.96);
       // 插入第一条数据
       writableDatabase.insert("Book",null, contentValues);


       contentValues.clear();

       // 组装第二条数据
       contentValues.put("name", "Java编程思想");
       contentValues.put("author", "Ban Brown");
       contentValues.put("pages", 700);
       contentValues.put("price", 96.96);

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

   private void update() {
       SQLiteDatabase writableDatabase = myDatabaseHelper.getWritableDatabase();

       ContentValues contentValues = new ContentValues();
       contentValues.put("price", 1000);

       //
       writableDatabase.update("Book", contentValues, "name = ?", new String[]{"图解HTTP"});

  }


   private void select () {
       SQLiteDatabase writableDatabase = myDatabaseHelper.getWritableDatabase();
       Cursor book = writableDatabase.query("Book", null, null, null, null, null, null);
//       while (book.moveToFirst()) {
//           Log.d("select", book.toString());
//       }

       if (book.moveToFirst()) {
           do {
               String name = book.getString(book.getColumnIndex("name"));
               Log.d("select", name);
          } while (book.moveToNext());
      }
  }

   private void delete() {
       SQLiteDatabase writableDatabase = myDatabaseHelper.getWritableDatabase();
       writableDatabase.delete("Book", "pages > ?", new String[]{"500"});
  }
}

首先调用SQLiteDatabase的beginTransaction()方法开启一个事务,然后在一个异常捕获的代码块中执行具体的数据库操作,当所有的操作都 完成后,调用setTransactionSuccessful() 表示事务已经执行成功了,最后在finally代码中调用endTransaction()结束事务。注意,我们在删除旧数据的操作完成后手动抛出了一个NullPointerException,这样添加新数据的代码就执行不到了。不过由于事务的存在,中间出现异常会导致事务的失败,此时旧数据是删除不掉的。

7.5.2 升级数据库的最佳写法

上面我们实现升级数据库的方式是非常粗暴的,为了保证数据库中的表是最新的,我们只是简单地在onUpgrade() 方法中删除了当前的所有表,然后强制重新执行了一遍onCreate() 方法。这种方式在产品的开发阶段确实可以用,但是当产品真正上线之后就绝对不行了。想象一下,你编写的某个应用程序已经成功上线了,并且还拥有了不错的下载量。现在由于添加了一些新功能,数据库需要一直升级,结果用户更新了这个版本之后却发现以前程序中存储的本地数据全部丢失,那么很遗憾,你的用户群体可以跑一大半了。

听越来好像很可怕的样子,难道在产品发布之后还不能升级数据库?这是不可能的,其实只需要进行一些合理的控制,就可以保证升级数据库的时候数据不会丢失了。

你应该已经知道,每一个数据库对应一个版本号,当指定的数据库版本号大于当前数据库版本号的时候。就会进入onUpgrade() 方法中执行更新操作。这里需要为每一个版本好赋予其所对应的数据库变动,然后在onUpgrade() 方法中对当前斩版本号进行判断,再执行相应的变动即可。

MyDatabaseHelper.java

package com.ziyia.databasetest;

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.widget.Toast;
import androidx.annotation.Nullable;

public class MyDatabaseHelper extends SQLiteOpenHelper {

   private Context context;

   private String createCategory = "create table Category (id integer primary key autoincrement, catgory_name text, category_code integer);";


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


   @Override
   public void onCreate(SQLiteDatabase db) {
       db.execSQL(createBook);
       Toast.makeText(context, "数据库创建成功", Toast.LENGTH_SHORT).show();
  }

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

不过,一段时候后有了新需求,这次需要向数据库中再添加一张Catrgory表。于是,我们应该这样修改代码:

package com.ziyia.databasetest;

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.widget.Toast;
import androidx.annotation.Nullable;

public class MyDatabaseHelper extends SQLiteOpenHelper {

   private Context context;

   private String createCategory = "create table Category (id integer primary key autoincrement, catgory_name text, category_code integer);";

   private String createBook = "create table Book(" +
           "id integer primary key autoincrement," +
           "author text," +
           "price real," +
           "pages integer," +
           "name text);";

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


   @Override
   public void onCreate(SQLiteDatabase db) {
       db.execSQL(createBook);
       db.execSQL(createCategory);
       Toast.makeText(context, "数据库创建成功", Toast.LENGTH_SHORT).show();
  }

   @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);
       
       if (oldVersion <= 1) {
           db.execSQL(createCategory);
      }
  }
}

可见,我们在onCreate() 方法里新增了一条建表语句,然后又在onUpgrade() 方法中添加了一个if判断,如果用户数据库的旧版本号小于1,就只会创建一张 Category表。

这样当用户直接安装第2版的程序时,就会进入onCreate() 方法,将两张表一起创建。

而当用户使用第2版的程序覆盖安装第1版的程序时,就会进入升级数据库的操作中,此时由于Book表已经存在了,只需要创建Category表。

嘿嘿,没过多久新需求又来了,这次要给Book表和Category表之间建立联系,需要在Book表中添加一个category_id字段,我们应该这样修改MyDatabaseHelper中的代码:

package com.ziyia.databasetest;

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.widget.Toast;
import androidx.annotation.Nullable;

public class MyDatabaseHelper extends SQLiteOpenHelper {

   private Context context;

   private String createCategory = "create table Category (id integer primary key autoincrement, catgory_name text, category_code integer);";

   private String createBook = "create table Book(" +
           "id integer primary key autoincrement," +
           "author text," +
           "price real," +
           "pages integer," +
           "name text," +
           "category_id integer);";

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


   @Override
   public void onCreate(SQLiteDatabase db) {
       db.execSQL(createBook);
       db.execSQL(createCategory);
       Toast.makeText(context, "数据库创建成功", Toast.LENGTH_SHORT).show();
  }

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

       if (oldVersion <= 1) {
           db.execSQL(createCategory);
      }
       
       if (oldVersion <= 2) {
           db.execSQL("alter table Book add column category_id integer");
      }
  }
}

此时,如果用户直接安装第3版程序时,新增的列已经创建成功。

如果用户安装第3版程序覆盖第3版以下的程序时,就会进入到升级数据库操作中,我们在升级操作的方法里添加了一个新条件,如果当前本地数据库版本小于2,就会执行alter命令添加新字段。

这里要注意一个细节,第当升级一个数据库版本的时候,onUpgrade() 方法里一定要写一个与其对应的if判断语句,为什么要这样做呢?这是为了保证App在踌版本升级的时候。每一次的数据修改都能被全部执行,比如用户当前是从2版本升级到3版本,那么只有第二条判断语句会执行,如果用户是从1版本升级到3版本的时候,那么两条判断语句都会执行。

这种方式来维护斩升级,不管版本怎样更新,都可以保证数据库的表结构是最新的,而且最重要的一点是:表中的数据完全不会丢失。

发表回复

相关

浙ICP备2021031744号-3