在Android开发中,少不了对数据文件进行存取,需要掌握的Android存储功能技术如下:
SharedPreferences:即用户偏好,可以通过这个类,存取一些简单的用户设置信息;
外部存储:即面向外部SD卡存取数据,可为数据做持久化保存;
内部存储:即面向移动设备内部自带存储区域的存取(并不是手机的CPU内存),同样可为数据做持久化保存,由于移动设备可能并未安装SD卡而且SD卡可能被随时取出,所以使用内部存储相对外部存储更加安全,但内部存储空间有限,远小于外部存储。
SQLite数据库:Android设备内嵌SQLite数据库,相较于大型的Oracle、MySQL数据库,SQLite数据库更加轻便:该数据库是文件形式的、它并不需要用户名和密码就能访问、功能十分齐全,同样是关系型数据库、增删查改也能方便实现,非常适合作为移动设备的数据库;
ContentProvider:Android平台的四大组件之一,通过ContentProvider,每个应用都可以将自己的数据库信息以接口的形式暴露给其他应用来访问(使用ContentResolver来访问ContentProvider提供的数据),这大大增加了应用的安全性。例如:通讯录应用的ContentProvider就提供了其他应用存取自己通讯录信息的接口,而微信应用就可以通过自己的ContentResolver来访问通讯录应用的ContentProvider提供的内容,从而获得设备的通讯录信息。
下面将介绍上述Android存储技术的实现。
使用SharedPreferences类存取用户偏好
使用SharedPreferences可以保存一些简单的用户偏好信息,比如:音量大小、屏幕亮度、颜色主题、用户名及密码等。
创建两个Activity,其中一个Activity用于模拟登录界面,点击登录后跳转到第二个Activity显示输入的用户名密码。在首次进入应用时,需要输入用户名和密码,以后每次进入应用都将把第一次设置的用户名和密码自动填充在输入框中。
使用SharedPreferences存储数据
步骤如下:
创建SharedPreferences对象,传入持久化数据的文件名以及保存模式;
调用SharedPreferences.edit()方法,返回SharedPreferences.Editor对象;
调用SharedPreferences.Editor.putXxx(String key, xxx Value)方法以键值对的形式保存数据;
调用SharedPreferences.Editor.commit()方法提交数据。
界面及实现上述步骤的代码示例如下:
//第一个Activity,用于模拟登录界面public class MainActivity extends AppCompatActivity { private EditText mEditTextUserName; private EditText mEditTextPassword; private Button mButtonLoginIn; private SharedPreferences mSharePreferences; /** * @param savedInstanceState */ @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //1. 创建SharedPreferences对象,传入持久化数据的文件名(文件将默认保存为XML格式,故无需后缀名)以及保存模式,MODE_PRIVATE模式表示访问权限为本应用,MODE_APPEND模式表示在文件尾部追加内容 mSharePreferences = getSharedPreferences("data", MODE_PRIVATE); mEditTextUserName = (EditText) findViewById(R.id.edit_text_name); mEditTextPassword = (EditText) findViewById(R.id.edit_text_password); //取出数据并渲染 mButtonLoginIn = (Button) findViewById(R.id.button_submit); mButtonLoginIn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { String stringUsername = mEditTextUserName.getText().toString(); String stringPassword = mEditTextPassword.getText().toString(); // 2. 调用SharedPreferences.edit()方法,返回SharedPreferences.Editor对象,用于写入数据; SharedPreferences.Editor editor = mSharePreferences.edit(); //3. 调用SharedPreferences.Editor.putXxx(String key, xxx Value)方法以键值对的形式保存数据; editor.putString("user", stringUsername); editor.putString("password", stringPassword); // 4. 调用SharedPreferences.Editor.commit()方法提交数据。 editor.commit(); Intent i = new Intent(MainActivity.this, Main2Activity.class); Bundle bundle = new Bundle(); bundle.putString("username", stringUsername); bundle.putString("password", stringPassword); i.putExtra("data", bundle); startActivity(i); } }); }}
//第二个Activity,接收第一个Activity设置的用户名及密码public class Main2Activity extends AppCompatActivity { private TextView mTextViewGetUserName; private TextView mTextViewGetPassword; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main2); mTextViewGetUserName = (TextView) findViewById(R.id.textview_get_username); mTextViewGetPassword = (TextView) findViewById(R.id.textview_get_password); Bundle b = getIntent().getBundleExtra("data"); String getStringUserName = b.getString("username"); mTextViewGetUserName.setText(getStringUserName); String getStringPassword = b.getString("password"); mTextViewGetPassword.setText(getStringPassword); }}
效果如下,输入用户名和密码,点击按钮,程序跳转至第二个Activity并显示输入的用户名和密码:
打开DDMS,找到如下路径:data/data/com.sharedpreferencesdemotest(应用包名)/shared_prefs
在该路径下有一个data.xml文件,导出后打开,显示如下内容:
这说明数据已经存储成功。
使用SharedPreferences读取数据
如果要实现的功能是:当点击back键退出程序(第一个activity已被销毁),再次进入程序后,第一个activity的仍有上次数据的内容。
这就需要使用SharedPreferences读取数据,步骤如下:
创建SharedPreferences对象,传入需要读取的数据的文件名;
调用SharedPreferences.getXxx(String key, Xxx defaultValue)方法,取出需要获得的值并设置在相应位置;
添加如下代码:
//取出数据并渲染:调用SharedPreferences.getXxx(String key, Xxx defaultValue)方法,取出需要获得的值并设置在相应位置,第二个参数是缺省值 mEditTextUserName.setText(mSharePreferences.getString("user", null)); mEditTextPassword.setText(mSharePreferences.getString("password", null));
重新安装应用,当输入用户名和密码后,退出程序,再次进入应用后,输入的用户名及密码已被设置到相应位置。
外部存储
将数据保存在SD卡中的存储称为外部存储。
示例中有一个Activity,中间是一个ImageView,左下角和右下角各一个按钮,当点击左下角按钮时,图片将被保存到外部存储中;当点击右下角按钮时,图片将从外部存储中获取该图片并显示在ImageView中,界面如下所示:
实现外部存储的步骤为:
使用外部存储需调用Environment.getExternalStorageState()方法判断当前设备SD介质是否可用;
调用Environment.getExternalStorageDirectory()方法获取设备SD卡的根路径;
通过File的构造方法File(File dir, String fileName);
将构造的File对象传入FileOutputStream,可将写入文件;
调用BitmapFactory.decodeFile(String pathName)方法读取外存文件,返回Bitmap对象,将该对象设定在指定位置。
代码示例如下:
获取当前设备的外部存储状态,并判断其读写权限:
/** * @return 外部存储是否可用 */ private boolean getExternalState() { //获得外部存储介质的状态 String state = Environment.getExternalStorageState(); //外部存储处于可读可写状态 if (state.equals(Environment.MEDIA_MOUNTED)) { Log.i("TAG", "外部存储可读写"); flag = true; } //外部存储处于只读状态 else if (state.equals(Environment.MEDIA_MOUNTED_READ_ONLY)) { Log.i("TAG", "外部存储只读"); flag = false; } else { Log.i("TAG", "外部存储不可用"); flag = false; } return flag; }
从res/drawable中读取一张图片,并写入外部存储中:
private void saveImage() { //获取外部存储的根路径 File sdPath = Environment.getExternalStorageDirectory(); String absolutePath = sdPath.getAbsolutePath(); //将文件存储在SDPath目录下,名字为as_launcher.jpg Log.i("TAG", "外置存储的绝对路径: " + absolutePath); filePath = new File(sdPath, "as_launcher.jpg"); BufferedInputStream bis = null; BufferedOutputStream bos = null; try { //获取OutputStream对象,写入外部存储 OutputStream os = new FileOutputStream(filePath); //获取drawable中的图片并转化为InputStream对象,读入到程序中 InputStream is = getResources().openRawResource(R.drawable.as); bis = new BufferedInputStream(is); bos = new BufferedOutputStream(os); int len = 0; byte[] buf = new byte[1024]; //边读取边写入 while ((len = bis.read(buf)) != -1) { bos.write(buf, 0, len); bos.flush(); } bis.close(); bos.close(); Toast.makeText(this, "save!", Toast.LENGTH_SHORT).show(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } }
读取外部存储中的图片:
private void getImage() { //从制定路径中获取图片文件 Bitmap image = BitmapFactory.decodeFile(filePath.getPath()); mImageView.setImageBitmap(image);
点击保存按钮,再点击显示按钮,显示效果如下:
打开DDMS,在storage/emulated/0 路径中,可查看到as_launcher.jpg文件,说明该文件已被成功写入外部存储:
内部存储
与外部存储类似,使用内部存储也是遵循保存的方式,也就是开辟流对象进行文件的保存和读取。
创建一个Activity,点击左下角的save按钮,上方的输入内容将被保存至内部存储中,点击右下角的display按钮,将读取内部存储的内容,并toast出来,布局如下:
实现向内部存储写入数据,应调用上下文对象的如下方法:
ContextWrapper.openFileOutput(String fileName, int mode);
其中第一个参数是写入文件的文件名,第二个参数是存储的权限模式。该方法返回一个FileOutputStream文件处理流对象,通过该对象可进行写入操作。
实现从内部存储读取数据,应调用上下文的如下方法:
Context.openFileInput(String fileName);
方法的唯一参数用于从指定文件名的文件中读取数据,该方法返回一个FileInputStream对象,通过该对象可进行读取操作。
具体代码示例如下:
public class MainActivity extends AppCompatActivity { private Button mButtonSave; private Button mButtonDisplay; private EditText mEditTextContent; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mEditTextContent = (EditText) findViewById(R.id.edittext_content); mButtonSave = (Button) findViewById(R.id.button_save); mButtonSave.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { //向内部存储写入数据 saveContent(); Toast.makeText(MainActivity.this, "saved!", Toast.LENGTH_SHORT).show(); } }); mButtonDisplay = (Button) findViewById(R.id.button_toast); mButtonDisplay.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { //从内部存储读取数据 String s = getContent(); Toast.makeText(MainActivity.this, s, Toast.LENGTH_SHORT).show(); } }); } private String getContent() { String s = null; FileInputStream fis = null; try { //通过该方法从内部存储中的edit_data.txt文件中读取数据 fis = this.openFileInput("edit_data.txt"); int len = 0; byte[] buf = new byte[1024]; while ((len = fis.read(buf)) != -1) { s = new String(buf, 0, len, "UTF-8"); } } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { if (fis != null) { try { fis.close(); } catch (IOException e) { e.printStackTrace(); } } } return s; } //写入数据 private void saveContent() { FileOutputStream fos = null; String content = mEditTextContent.getText().toString(); try { //通过该方法向内部存储中写入数据,文件名为edit_data.txt,模式为MODE_PRIVATE,表示该文件操作权限为文本应用,另一个常用的权限MODE_APPEND表示在文件末尾添加内容 fos = this.openFileOutput("edit_data.txt", MODE_PRIVATE); fos.write(content.getBytes()); fos.flush(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { if (fos != null) { try { fos.close(); } catch (IOException e) { e.printStackTrace(); } } } }}
打开DDMS,在data/data/com.datasavetest.lenovo.datasavetest(应用包名)/files目录下,新生成了一个edit_data.txt文件,导出该文件,将显示下列内容:
说明文件在内部存储中保存成功。
关于内部存储和外部存储,需要注意以下几点:
无论是内部存储还是外部存储,实质都是将文件进行IO流操作;
使用真机测试程序时,若需要查看DDMS中data文件夹中的内容,需要对真机进行ROOT,并借助工具修改data文件夹及其子文件(夹)的读写权限,或通过adb shell 命令修改权限,具体方法请参考这篇博文:《Android配置—-DDMS 连接真机(己ROOT),用file explore看不到data/data文件夹的解决办法》;
无论使用内部存储还是外部存储,给定的文件名需包含后缀,给定的后缀是什么类型,该文件就将以什么格式打开,而SharedPreferences默认保存为xml格式的文件,故无需指定后缀名;
使用外部存储,需要在AndroidManiFest中添加如下权限:
<!-- 允许写入外部存储 --> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"></uses-permission> <!-- 允许挂载与反挂载文件系统 --> <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"></uses-permission>
而使用内部存储无需添加任何权限。
SQLite数据库
与MySQL、Oracle等大型数据库类似,SQLite数据库同样支持标准的sql语句,包括建表、增删改查字段和项以及表的约束,下面先回顾一下基础的sql语句:
基本的sql语句
1.创建表
create table 表的名字(字段名字 字段的类型,.. ..);
2.为所有的字段一一赋值
insert into 表的名字 values(字段值列表);
3.表的添加记录
insert into 表的名字(字段列表) values(字段值列表);
4.更新一条记录
update 表名 set 字段名=新值,字段名=新值,.. where 条件;
5.在原来值的基础上做更改
update 表名 set 字段名=字段名(+-*/)+值 where 条件;
6.更新某几记录
update 表名 set 字段名=新值… where 主键=值 or 字段=值;
update 表名 set 字段名=新值… where 主键 in(值列表);
7.删除一条记录
delete from 表名字 +条件;
8.清空所有的记录
delete from 表名字
9.查询表中所有的数据
select * from 表的名字;
10.查询某些字段的数据
select 字段1,字段2… from 表的名字;
11.where条件查询
select 字段1,字段2… from 表的名字 where 条件1 and 条件2 ….;
表的约束
not null 不能为空
1.创建表的时候设置
字段名字 字段类型 not null;2.创建表的时候设置默认值
字段名字 字段类型 default 默认值 not null;
3.修改表的约束是不能为空
alter table 表的名字 modify 字段名字 字段的类型 not null;3.修改表的约束是可以为空
alter table 表的名字 modify 字段名字 字段的类型 null;unique 不能重复 (null可以重复)
1.创建表的设置唯一
字段名字 字段类型 unique;2.创建表的设置唯一并且不能为空
字段名字 字段类型 unique not null|(not null unique);3.修改表的约束不能重复
alter table 表的名字 modify 字段名字 字段的类型 unique;4.修改表的约束不能重复
alter table 表的名字 add unique(字段名);5.删除表的约束不能重复(只针对mysql数据库)
alter table 表的名字 drop index 字段名(有约束的字段);6.修改表的约束:多个字段的值合起来是不能重复的。
alter table 表的名字 add constraint 约束名 unique(字段1,..);7.删除表的约束:多个字段的值合起来是不能重复的。(只针对mysql数据库)
alter table 表的名字 drop index 约束名;主键(primary key)
1.创建表的时候直接设置主键
字段名字 字段类型 primary key2.创建表的时候设置主键,并且自动增长(+1)
字段名 int primary key auto_increment3.修改表的约束主键
alter table 表的名字 add primary key(字段名);4.删除表的主键
alter table 表的名字 drop primary key;
alter table 表的名字 modify 字段名 null;5.修改表的约束 多个字段联合做主键
alter table 表的名字 add constraint 约束名 primary key(字段1,..);
SQLite数据库的特征
与大型的关系型数据库(MySQL、Oracle等)类似,SQLite数据库支持标准的sql语句,但其更加便捷,是以文件的形式存在于应用内部;
SQLite数据库无需导入任何辅助类库、无需任何配置、无需设置用户名和密码就能(不存在用户的概念)在程序中获得数据库对象从而对数据库进行操作;
SQLite中的字段可以不定义类型,但为了数据库的移植,最好指定字段的类型;
使用SQLiteOpenHelper类创建数据库和表
创建数据库需要继承SQLiteOpenHelper类,实现其构造方法以便初始化父类的构造方法:
public class MyOpenHelper extends SQLiteOpenHelper {/** * @param context 创建数据库对象时使用的上下文对象 * @param name 数据库名字 * @param factory 用于创建Cursor对象的一个工厂对象,默认为空 * @param version 数据库版本号,默认从1开始 */ public MyOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) { super(context, name, factory, version); } public MyOpenHelper(Context context) { this(context, "MyBDMessage.db", null, 1); }}
构造方法有四个参数,第三个参数一般为空(默认也为空),第四个参数是数据库版本号,从1开始。
接着重写SQLiteOpenHelper类中的两个回调方法onCreate和onUpdate:数据库被创建后,onCreate方法立刻被回调(仅在第一次创建数据库时被回调),所以一般在onCreate方法中创建表,而当数据库版本号递增时,onUpdate被回调:
//建表语句 public static final String CREATE_TABLE = "create table Person(" + "_id integer primary key autoincrement," + "age integer not null," + "name varchar(20)," + "address varchar(40))"; @Override public void onCreate(SQLiteDatabase db) { Log.i("TAG", "onCreate"); //建表,可执行表的增删改操作 db.execSQL(CREATE_TABLE); } //数据可升级时回调该方法,可修改表中的字段、增加表 @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { Log.i("TAG", "onUpgrade"); }
打开DDMS,在路径 data/data/com.datasavetest.lenovo.sqlitedatabasetest(应用包名)/databases 目录下,生成了一个MyDBMessage.db文件,导出该文件,并用SQLite Expert打开:
数据库中新建了一个Person表并设置了字段,每个字段的类型如下:
在SQLite Expert中查看建表语句:
这说明创建数据库及表结构成功。
升级SQLite数据库
当应用增加需求时,可能会增加数据库中的字段,这就需要升级数据库,当人为地把数据库版本号递增1时,就代表在为数据库做升级操作,这时,方法onUpdate将被回调,为了同时保证新安装应用的用户和升级应用的用户拥有的数据库的表的字段一致,需要在onUpdate方法中作如下操作:
//为Person表增加一个color字段 public static final String ALTER_TABLE = "alter table Person add color varchar(20)"; //为Person表增加一个language字段 public static final String ALTER_TABLE_2 = "alter table Person add language varchar(20)"; //只在第一次创建数据库时回调 @Override public void onCreate(SQLiteDatabase db) { Log.i("TAG", "onCreate"); //建表,可执行表的增删改操作 db.execSQL(CREATE_TABLE); db.execSQL(ALTER_TABLE); db.execSQL(ALTER_TABLE_2); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { Log.i("TAG", "onUpgrade"); switch (oldVersion) { case 1: db.execSQL(ALTER_TABLE); case 2: db.execSQL(ALTER_TABLE_2); default: break; } }
根据上述代码,数据库版本为3,说明数据库已经作了2次升级,每次升级分别为Person表增加了一个字段,这就带来如下三种情况:
(1). 用户之前从未安装过该应用,也就是数据库将被创建,onCreate方法会被回调,建立表结构,那么建表和两次升级语句,都必须执行到,也就是onCreate方法中需要一次执行这三个方法:
//第一次创建表db.execSQL(CREATE_TABLE);//第一次升级,添加一个字段db.execSQL(ALTER_TABLE);//第二次升级,添加一个字段db.execSQL(ALTER_TABLE_2);
(2). 用户从版本1(oldVersion)升级到版本3,需要添加两个字段,此时数据库已存在,onCreate方法不会回调,而onUpdate被回调,switch语句将从case1开始执行,欲实现添加两个字段,每个case条件后不能有break关键字,这样才能让每个case语句都执行:
//switch语句中没有break关键字switch (oldVersion) { case 1: db.execSQL(ALTER_TABLE); case 2: db.execSQL(ALTER_TABLE_2); default: break; }
(3). 用户从版本2(oldVersion)升级到版本3,与第二种情况类似,switch语句将从case2开始执行。
从DDMS中导出数据库文件并打开,字段被成功添加:
至此,无论用户从哪个版本升级到版本3,都将保证表数据不被删且拥有相同字段,关键就是switch语句中不能有break关键字。
向表中插入数据
向表中插入数据常用的有三种方式:
(1). 使用字符串拼接的方式:
public void insertData(Person p) { String INSERT_DATA = "insert into Person(age,name,address,color,language) values(" + p.getAge() + ",\'" + p.getName() + "\',\'" + p.getAddress() + "\',\'" + p.getColor() + "\',\'" + p.getLanguage() + "\')"; //调用SQLiteDatabase.execSQL(String sql)方法 mDB.execSQL(INSERT_DATA);}
这种方法在标准sql语句中包含参数,需要拼接字符串。所以它的缺点是书写起来比较麻烦,因为在标准的sql语句中,必须使用单引号包含字符串,而在拼接字符串时,还需要将这些单引号书写成转义字符的形式。
(2). 使用占位符的形式:
String INSERT_DATA_2 = "insert into Person(age,name,address,color,language) values(?,?,?,?,?)"; //调用SQLiteDatabase.execSQL(String sql, Object[] obj)方法插入数据 mDB.execSQL(INSERT_DATA_2, new Object[]{p.getAge(), p.getName(), p.getAddress(), p.getColor(), p.getLanguage()});
这种方法将需要插入的数据以问号占位,并使用SQLiteDatabase.execSQL(String sql, Object[] obj)方法的Object数组替换占位符,书写起来较为方便,不易出错,故比较常用。
(3).使用SQLiteDatabase.insert(String table, String nullColumnHack, ContentValues values)方法,第一个参数表名,第二个参数一般为空,第三个参数是要插入的数据,以键值对的形式初始化:
ContentValues cv = new ContentValues(); //ContentValues.put()方法的第一个参数是字段名,第二个参数是插入的值 cv.put("name", p.getName()); cv.put("age", p.getAge()); cv.put("address", p.getAddress()); cv.put("color", p.getColor()); cv.put("language", p.getLanguage()); //调用SQLiteDatabase.insert(String table, String nullColumnHack, ContentValues values)方法插入数据 mDB.insert("Person", null, cv);
这种方法书写不易出错,也比较常用。
点击按钮,初始化一个Person对象并插入到Person表中:
mButtonInsert.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Person p = new Person(20, "vanpersie", "beijing", "yellow", "Chinese"); mPersonDao.insertData(p); Toast.makeText(MyOpenHelperActivity.this, "insert data row to Person table!", Toast.LENGTH_SHORT).show(); } });
,从DDMS中导出数据库文件并查看:
可见上述三种插入数据的方式均成功执行。
修改和删除表中的数据
在布局中添加两个按钮,分别用于修改、删除数据:
修改、删除表中的数据与插入数据类似:
对照插入数据,修改数据的方法同样有三种:
(1).拼接字符串
/** * 修改id条目的address字段 * * @param address * @param id */ public void alterData(String address, int id) { String ALTER_DATA = "update Person set address = \'" + address + "\' where _id = " + id; mDB.execSQL(ALTER_DATA); }
将id号为3的条目中的address字段修改为”Shanghai”:
@Override public void onClick(View v) { mPersonDao.alterData("Shanghai", 3); }
点击修改按钮,从DDMS中导出数据库文件:
修改成功。
(2).使用占位符
String ALTER_DATA_2 = "update Person set address = ? where _id = ?";mDB.execSQL(ALTER_DATA_2, new Object[]{address, id});
在点击事件中将id为2条目的address修改为”Guangzhou”:
mPersonDao.alterData("Guangzhou", 2);
从DDMS中导出数据库并查看:
修改成功。
(3).使用SQLiteDatabase.update()的方法修改数据:
ContentValues cv = new ContentValues(); cv.put("address", address); //参数1:表名; 参数2:要修改的字段键值;参数3:该行数据需要满足的条件;参数4:条件值 mDB.update("Person", cv, "_id = ?", new String[]{id + ""});
在点击事件中将id为1的条目的address字段修改为”Chengdu”:
mPersonDao.alterData("Chengdu", 1);
运行程序,点击修改按钮,从DDMS中导出数据库文件:
修改成功。
删除表中的数据:
添加删除的方法:
(1).使用字符串拼接的形式:
public void deleteData(int id) { String DELETE_DATA = "delete from Person where _id = " + id; mDB.execSQL(DELETE_DATA); }
删除id为3的条目:
mPersonDao.deleteData(3);
导出数据库文件:
id为3的条目已被删除。
(2).使用占位符:
String DELETE_DATA_2 = "delete from Person where _id = ?"; mDB.execSQL(DELETE_DATA_2, new Object[]{id});
在点击事件中添加代码:
//将id为2的条目删除mPersonDao.deleteData(2);
导出DDMS中的数据库文件:
id为2的条目已被删除。
(3).使用SQLiteDatabase.delete()方法:
mDB.delete("Person", "_id = ?", new String[]{id + ""});
在点击事件中添加代码,删除id为1的条目:
mPersonDao.deleteData(1);
导出数据库文件:
数据库为空,说明id为1的条目已被删除。
SQLite数据库操作事物
所谓的事物就是一个整体事件,比如用户希望某一个添加操作和修改操作要么都能完成,要么都不能完成,即事物具有原子性质。
下面将实现一个功能:删除数据库中的全部数据,再添加一条数据。需保证删除和添加一起完成,否则同时不成功。
首先向数据库中插入一些数据:
实现数据库事务的步骤:
- 调用SQLiteDatabase.beginTransaction()开启事务;
- 执行整体事务事件;
- 调用SQLiteDatabase.setTransactionSuccessful()表示事务完成;
- 无论事务是否完成,都应调用SQLiteDatabase.endTransaction()方法关闭事物;
代码示例:
MyOpenHelper moh = new MyOpenHelper(MyOpenHelperActivity.this); //得到SQLiteDatabase对象 SQLiteDatabase sdb = moh.getReadableDatabase(); // 1. 调用SQLiteDatabase.beginTransaction()开启事务 sdb.beginTransaction(); // 2. 执行整体事务事件; try { //删除数据库全部数据 sdb.delete("Person", null, null); //手动抛出异常,模拟程序在此出现异常 if (true) { throw new NullPointerException(); } ContentValues cv = new ContentValues(); cv.put("name", "Zhangsan"); cv.put("address", "Hongkong"); cv.put("color", "black"); cv.put("language", "Chinese"); cv.put("age", "25"); //添加一条数据 sdb.insert("Person", null, cv); // 3. 调用SQLiteDatabase.setTransactionSuccessful()表示事务完成 sdb.setTransactionSuccessful(); } catch (Exception e) { e.printStackTrace(); } finally { 4. 无论事务是否完成,都应调用SQLiteDatabase.endTransaction()方法关闭事物 sdb.endTransaction(); }
将上述代码放在点击事件中,点击按钮,程序执行到全部删除数据后抛出异常,最后关闭事物。在这期间,添加数据未执行,而由于在程序中开启了事务,要求删除和添加一并执行,否则全都不执行,运行程序,数据库中的内容未发生变化,表明数据删除操作并未执行,原子性得到验证:
删去手动抛出的异常,再次运行程序,数据库中显示为:
说明删除和插入数据同时完成,事务提交成功。
查询SQLite数据
查询数据仍然使用标准的sql语句,可调用SQLiteDatabase.rawQuery()方法返回一个Cursor对象,并将Cursor对象指向的数据通过SimpleCursorAdapter设置到ListView等控件上:
//查询数据 public Cursor queryData() { //查询数据库的全部数据 String QUERY_DATA = "select * from Person"; Cursor c = mDB.rawQuery(QUERY_DATA, null); return c; }
将查询的数据设置到ListView上,其中ListView的每一项的布局如下:
代码示例如下:
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_cursor_adapter); mListView = (ListView) findViewById(R.id.listview_adapter); pd = new PersonDao(this); //从数据库中查询出来的需要显示的字段组成的数组 String[] from = {"name", "address", "age", "color", "language"}; //查询出的数据需要显示到响应控件的id组成的数组 int[] to = {R.id.textview_name, R.id.textview_address, R.id.textview_age, R.id.textview_color, R.id.textview_language}; //参数1:Context对象;参数2:ListView的Item布局;参数3:查询数据库返回的Cursor对象;参数4:从数据库中查询出来的需要显示的字段组成的数组;参数5:查询出的数据需要显示到响应控件的id组成的数组,参数6:用于决定Adapter的行为 mSimpleCurorAdapter = new SimpleCursorAdapter(this, R.layout.cursor_adapter_item_view, pd.queryData(), from, to, 0); mListView.setAdapter(mSimpleCurorAdapter); }
显示效果如下(略丑):
使用SimpleCursorAdapter需要注意:
查询的数据库中必须包含”id”字段;
查询的数据库中,字段id的名字必须为”_id”。可以为查询的id字段起一个别名为” _id”,或者为简便起见,直接在建表的时候将字段id命名为” _id”。
ContentProvider与ContentResolver
ContentProvider是Android四大组件之一,该组件可以提供自身应用的数据给外界应用进行使用,例如本地通讯录应用就将自己的通讯录信息提供给其他应用,而在自己的应用中使用ContentResolver可获取ContentProvider提供的信息。
通讯录应用的数据库信息
通讯录作为一个系统自带的应用,也有自己的数据库,其数据库的路径是:data/data/com.android.providers.contacts/contacts2.db
如下图所示:
导出contacts2.db数据库,里面存有多张表,其中有几张经常使用的表:
contacts表:该表保存了所有手机联系人,常用字段有:联系人铃声、联系人语音邮件、上次联系时间、通话时长、大头照、是否添加到收藏夹 等。
contacts表
row_contacts表:该表保存了手机中所有创建过的联系人(包含已删除的),其中有一个字段就是用来标记该联系人是否被删除;该表还与contacts表进行了关联。
row_contacts表
mimetypes表:该表标识了联系人中每一个字段的唯一性id。
mimetypes表
data表:该表关联了上述三个表,可保存通讯录的所有相关信息,字段 data1到data15保存的值根据mimetypes值得不同而不同。
data表
若需要从数据库中查询设备的联系人信息,需要contacts表—>row_contacts表—>data表 的连表查询,使用起来较为繁琐。
而使用ContentResolver可快速获取通讯录的ContentProvider提供的信息。
使用ContentResolver获取通讯录信息
在自己的应用中创建ContentResolver对象,可快速获取其他应用的ContentProvider提供的数据库信息。下面演示了在自己的应用中获取通讯录应用的联系人信息并将姓名和电话显示在ListView的案例。
操作步骤:
- 获取ContentResolver对象;
- 调用ContentResolver.query()方法,传入要查询的表的字段和筛选条件,返回Cursor对象;
- 遍历Cursor对象,将每一项添加到一个集合中;
- 创建ListView Adapter适配器,将集合中的数据与Adapter匹配;
- 调用ListView.setAdapter()方法。
代码示例:
public class MainActivity extends AppCompatActivity { //仅在数据库中寻找 名称、电话号码、照片id、联系人id 四个字段 private static final String[] PHONE_PROJECTION = {Phone.DISPLAY_NAME, Phone.NUMBER, Phone.PHOTO_ID, Phone.CONTACT_ID}; //显示联系人名称字段的列号 private static final int DISPLAY_NAME_INDEX = 0; //电话号码字段的列号 private static final int NUMBER_INDEX = 1; //id的列号 private static final int CONTACT_ID_INDEX = 3; //存储联系人名称集合 private List<String> listName = new ArrayList<>(); //存储联系人电话集合 private List<String> listNumber = new ArrayList<>(); //显示结果的ListView private ListView mListView; //ContentResolver对象引用 private ContentResolver mContentResolver; //ListView的Adapter适配器 private ContactsAdapter mContactsAdapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mListView = (ListView) findViewById(R.id.listview); //获取通讯录的查询信息 getContacts(); mContactsAdapter = new ContactsAdapter(); //Adapter绑定ListView mListView.setAdapter(mContactsAdapter); //点击listview中的项,拨打电话 mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { } //获取通讯录信息 private void getContacts() { // 1. 获取ContentResolver对象; mContentResolver = getContentResolver(); //2. 调用ContentResolver.query()方法,传入要查询的表的字段和筛选条件,返回Cursor对象; Cursor cursor = mContentResolver.query(Phone.CONTENT_URI, PHONE_PROJECTION, null, null, null); // 3. 遍历Cursor对象,将每一项添加到一个集合中; if (cursor != null) { while (cursor.moveToNext()) { //获取姓名 String name = cursor.getString(DISPLAY_NAME_INDEX); //若为空,则进行下一次循环 if (TextUtils.isEmpty(name)) { continue; } //获取电话 String number = cursor.getString(NUMBER_INDEX); //获取id long id = cursor.getLong(CONTACT_ID_INDEX); //将数据添加到集合中 listName.add(name); listNumber.add(number); } } //关闭cursor游标 cursor.close(); } // 4. 创建ListView Adapter适配器,将集合中的数据与Adapter匹配; class ContactsAdapter extends BaseAdapter { @Override public int getCount() { return listName.size(); } @Override public Object getItem(int position) { return null; } @Override public long getItemId(int position) { return 0; } @Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder holder; if (convertView == null) { convertView = LayoutInflater.from(MainActivity.this).inflate(R.layout.item_view, null); holder = new ViewHolder(); holder.name = (TextView) convertView.findViewById(R.id.textview_name); holder.number = (TextView) convertView.findViewById(R.id.textview_number); convertView.setTag(holder); } else { holder = (ViewHolder) convertView.getTag(); } holder.name.setText(listName.get(position)); holder.number.setText(listNumber.get(position)); return convertView; } class ViewHolder { TextView name; TextView number; } }}
显示效果如下所示:
ContentProvider
ContentProvider主要有两个作用:
- 将自身应用的数据提供给瓦解使用;
- 操作自身数据库。
下面示例了两个应用,第一个应用创建了ContentProvider对象,实现了查询应用数据库信息,第二个应用使用ContentResolver获取第一个应用提供的查询结果并显示在ListView中,其中第一个应用的数据库Chat表中有如下内容:
第一个应用:
//继承ContentProvider类,实现其中的方法public class MyProvider extends ContentProvider { private ChatMessageDBHelper mChatHelper; private SQLiteDatabase mSdb; //创建Provider的方法 @Override public boolean onCreate() { mChatHelper = new ChatMessageDBHelper(getContext()); //得到数据库对象 mSdb = mChatHelper.getReadableDatabase(); return true; } //查询的方法 @Nullable @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { //使用ContentResolver.query方法可查询到该方法返回的Cursor对象 Cursor cursor = mSdb.query("Chat", projection, selection, selectionArgs, sortOrder, null, null); return null; } //类型匹配的方法 @Nullable @Override public String getType(Uri uri) { return null; } //添加的数据的方法 @Nullable @Override public Uri insert(Uri uri, ContentValues values) { return null; } //删除数据的方法 @Override public int delete(Uri uri, String selection, String[] selectionArgs) { return 0; } //修改数据的方法 @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { return 0; }}
需要在AndroidManiFest中注册ConentProvider:
<provider<!-- 使用ContentProvider的全限定类名注册 --> android:name="com.datasavetest.lenovo.sqlitedatabase.utility.MyProvider"<!-- 为ContentProvider设置访问权限 --> android:authorities="com.datasavetest.lenovo.sqlitedatabasetest.MyProvider"></provider>
第二个应用,使用ContentResolver接收查询结果:
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //初始化ListView 显示结果 mListView = (ListView) findViewById(R.id.listview_resolver); //初始化ContentResolver mContentResolver = getContentResolver(); //调用ContentResolver.query()返回ContentProvider的查询结果。参数1:需要查询的ContentProvider的Uri资源地址,也就是在注册ContentProvider是的authorities权限;参数2:查询的字段;参数2:筛选条件;参数3:筛选条件中占位符的值;参数5:排序方式 mCursor = mContentResolver.query(Uri.parse("content://com.datasavetest.lenovo.sqlitedatabasetest.MyProvider"), new String[]{"_id", "message"}, null, null, null); //遍历返回的Cursor对象,将结果设置到List集合中 if (mCursor != null) { while (mCursor.moveToNext()) { int id = mCursor.getInt(mCursor.getColumnIndex("_id")); String content = mCursor.getString(mCursor.getColumnIndex("message")); MessageBean mb = new MessageBean(id, content); list.add(mb); } } mProviderAdapter = new ProviderAdapter(); mListView.setAdapter(mProviderAdapter); } @Override protected void onStop() { super.onStop(); //Activity停止时,关闭Cursor对象 mCursor.close(); }//适配数据的Adapter class ProviderAdapter extends BaseAdapter { @Override public int getCount() { return list.size(); } @Override public MessageBean getItem(int position) { return list.get(position); } @Override public long getItemId(int position) { return getItem(position).getId(); } @Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder holder; if (convertView == null) { convertView = LayoutInflater.from(MainActivity.this).inflate(R.layout.layout_item_view, null); holder = new ViewHolder(); holder.id = (TextView) convertView.findViewById(R.id.textview_id); holder.message = (TextView) convertView.findViewById(R.id.textview_message); convertView.setTag(holder); } else { holder = (ViewHolder) convertView.getTag(); } holder.id.setText(getItem(position).getId() + ""); holder.message.setText(getItem(position).getContent()); return convertView; } class ViewHolder { TextView id; TextView message; } }}
返回的结果如下: