编辑
2020-12-03
客户端技术
00
请注意,本文编写于 1033 天前,最后修改于 113 天前,其中某些信息可能已经过时。

目录

SQLite基本结构
Android中操作SQLite
ADB Shell操作SQLite
SQLiteDatabase + SQL语句
通过API来操作数据库
使用LitePal操作SQLite
环境搭建
建库建表
数据库升级
CRUD操作

SQLite是一个开源的关系型数据库,实现自包容、零配置、支持事务的SQL数据库引擎。其特点是高度便携、使用方便、结构紧凑、高效、可靠。并且SQLite是在世界上最广泛部署的 SQL 数据库引擎。SQLite 源代码不受版权限制。 本篇文章书要是记述了SQLite的基本架构以及SQLite的几种操作方式,其中比较重要的就是ADB Shell命令操作与SQL语句,另外在开发中还是Litepal这款开源ORM框架用的比较多一些,使用起来确实非常方便。

SQLite基本结构

接口由SQLite C API组成,程序与SQLite交互的基础就是用C语言编写的API,JDBC也只是JNI调用而已。

在编译器中,词法分析器与语法分析器把SQL翻译为语法树,Code Generator根据语法树生产SQLite的汇编代码,交给虚拟机执行。

虚拟机,与Java虚拟机执行class中的指令类似,SQLite的汇编代码由SQLite虚拟机来执行,由虚拟机负责SQL到数据存取的交互,关于虚拟机的更多内容可以查看官网《The Virtual Database Engine of SQLite》

更多关于SQLite架构的内容可以查看官网《 Architecture of SQLite 》, 里面介绍的比较详细。

SQLite数据类型

类型类型说明
NULL这个值为空值
VARCHAR(n)长度不固定且其最大长度为 n 的字串,n不能超过 4000
CHAR(n)长度固定为n的字串,n不能超过 254
INTEGER值被标识为整数,依据值的大小可以依次被存储为1,2,3,4,5,6,7,8
REAL所有值都是浮动的数值,被存储为8字节的IEEE浮动标记序号
TEXT值为文本字符串,使用数据库编码存储(TUTF-8, UTF-16BE or UTF-16-LE)
BLOB值是BLOB数据块,以输入的数据格式进行存储。如何输入就如何存储,不改变格式
DATA包含了 年份、月份、日期
TIME包含了 小时、分钟、秒

Android中操作SQLite

SQLite的SQL语句其实和普通SQL没什么特别的不同,Windows下可视化操作SQLite可以使用SQLite Expert Personal 4 - 这款工具,下载地址如下:http://www.sqliteexpert.com/v4/SQLiteExpertPersSetup64.exe。打开后即可通过图形化界面的方式操作SQLite,同样也可以通过SQL语句来操作:

sql
# 建表 create table stu_info ( id integer primary key autoincrement, name varchar(30) not null, age integer , gender varchar(2) not null ) # 插入数据 insert into stu_info(name, age, gender) values ('Mike', 24, '女'); insert into stu_info(name, age, gender) values ('Jone', 26, '男'); insert into stu_info(name, age, gender) values ('Tom', 28, '女'); # 查询数据 select * from stu_info; # 删除数据 delete from stu_info where id = 13; # 修改数据 update stu_info set name = 'Jack' where id = 10; # 按条件查询 select * from stu_info where age = 24;

现在主要还是看看在Android平台如何使用吧!SQLiteOpenHelper:Android平台里一个数据库辅助类,用于创建或打开数据库,并且对数据库的创建和版本进行管理。

SQLiteDatabase:用于管理和操作SQLite数据库,几乎所有的数据库操作,最终都将由这个类完成。

ADB Shell操作SQLite

打开CMD窗口,输入adb shell,找到sqlite文件,通过sqlite3 + sqlite文件名就可以进入sqlite shell:

命令作用
.database显示数据库信息
.tables显示表名称
.schema命令可以查看创建数据表时的SQL命令
.schema table_name查看创建表table_name时的SQL的命令

SQLiteDatabase + SQL语句

下面这个例子是通过SQLiteDatabase + SQL语句来操作数据库,即需要自己手动拼接SQL,如果是插入数据、修改数据、删除数据都是用sqLiteDatabase.execSQL(insertSQL/updateSQL/deleteSQL)来完成的,查询数据时使用sqLiteDatabase.rawQuery()方法来完成,由于使用起来需要拼接SQL,稍微麻烦一点。

main_activity.xml

xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:padding="20dp" tools:context=".MainActivity"> <EditText android:id="@+id/et_name" android:hint="姓名" android:layout_width="match_parent" android:layout_height="wrap_content"/> <EditText android:id="@+id/et_age" android:hint="年龄" android:inputType="number" android:layout_width="match_parent" android:layout_height="wrap_content"/> <RadioGroup android:id="@+id/rg_sex" android:orientation="horizontal" android:layout_width="match_parent" android:layout_height="wrap_content"> <RadioButton android:id="@+id/rb_male" android:text="男" android:layout_width="wrap_content" android:layout_height="wrap_content"/> <RadioButton android:id="@+id/rb_female" android:text="女" android:layout_width="wrap_content" android:layout_height="wrap_content"/> </RadioGroup> <LinearLayout android:orientation="horizontal" android:layout_width="match_parent" android:layout_height="wrap_content"> <Button android:id="@+id/btn_add" android:text="添加" android:onClick="operatorData" android:layout_width="wrap_content" android:layout_height="wrap_content"/> <Button android:id="@+id/btn_update" android:text="修改" android:onClick="operatorData" android:layout_width="wrap_content" android:layout_height="wrap_content"/> <Button android:id="@+id/btn_delete" android:onClick="operatorData" android:text="删除" android:layout_width="wrap_content" android:layout_height="wrap_content"/> </LinearLayout> <EditText android:id="@+id/et_id" android:hint="修改/删除的ID" android:inputType="number" android:layout_width="match_parent" android:layout_height="wrap_content"/> <ListView android:id="@+id/lv_data" android:layout_width="match_parent" android:layout_height="wrap_content"/> </LinearLayout>

MainActivity.java

java
public class MainActivity extends AppCompatActivity { private static final String TAG = "MainActivity"; private EditText etId; private EditText etName; private EditText etAge; private String sex; private SQLiteDatabase sqLiteDatabase; private ListView lvData; // 申请权限请求码 private static final int REQUEST_EXTERNAL_STORAGE = 1001; // 检查权限,这种写法主要是针对比较新的Android6.0及以后的版本 public static void verifyStoragePermissions(Activity activity) { int writePermission = ActivityCompat.checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE); int readPermission = ActivityCompat.checkSelfPermission(activity, Manifest.permission.READ_EXTERNAL_STORAGE); if (writePermission != PackageManager.PERMISSION_GRANTED || readPermission != PackageManager.PERMISSION_GRANTED) { // 如果没有权限需要动态地去申请权限 ActivityCompat.requestPermissions( activity, // 权限数组 new String[]{Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE}, // 权限请求码 REQUEST_EXTERNAL_STORAGE ); } } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); etId = findViewById(R.id.et_id); etName = findViewById(R.id.et_name); etAge = findViewById(R.id.et_age); // 单选框组件 RadioGroup rgSex = findViewById(R.id.rg_sex); lvData = findViewById(R.id.lv_data); verifyStoragePermissions(this); /* * 构造参数: * 1、上下文 * 2、数据库名称,默认位置应用的私有目录(内部存储的database文件夹) * 3、CursorFactory类型 * 4、数据库版本 */ String path = Environment.getExternalStorageDirectory() + "/sqlite_demo.db"; SQLiteOpenHelper helper = new SQLiteOpenHelper(this, path, null, 1){ // 创建数据库 @Override public void onCreate(SQLiteDatabase db) { Toast.makeText(MainActivity.this, "数据库创建", Toast.LENGTH_SHORT).show(); // 如果事先没有数据库的话,创建表的操作就可以在这里进行 db.execSQL("create table stu_info (id integer primary key autoincrement, name varchar(30) not null, age integer,gender varchar(2) not null)"); } // 版本号变化之后会调用这个方法 @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { Toast.makeText(MainActivity.this, "数据库升级", Toast.LENGTH_SHORT).show(); } }; // 获取数据库对象 sqLiteDatabase = helper.getWritableDatabase(); // 设置单选框的监听 rgSex.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() { @Override public void onCheckedChanged(RadioGroup group, int checkedId) { switch (checkedId){ case R.id.rb_female: sex = "女"; break; case R.id.rb_male: sex = "男"; break; } } }); flushStuData(); } private void flushStuData() { List<StudentInfo> stuList = new ArrayList<>(); String selectSQL = "select * from stu_info"; Cursor cursor = sqLiteDatabase.rawQuery(selectSQL, new String[]{}); cursor.moveToFirst(); while (!cursor.isAfterLast()){ int id = cursor.getInt(0); String name = cursor.getString(1); int age = cursor.getInt(2); String sex = cursor.getString(3); stuList.add(new StudentInfo(id, name, age, sex)); cursor.moveToNext(); } cursor.close(); lvData.setAdapter(new StuInfoAdapter(this, stuList)); } public void operatorData(View view) { int viewId = view.getId(); switch (viewId) { case R.id.btn_add: if(TextUtils.isEmpty(sex)) { Toast.makeText(MainActivity.this, "请选择性别", Toast.LENGTH_SHORT).show(); return; } String insertSQL = String.format(Locale.CHINA,"insert into stu_info(name, age, gender) values ('%s', %d, '%s')", etName.getText().toString(), Integer.parseInt(etAge.getText().toString()), sex); Log.i(TAG, "operatorData: insertSQL = " + insertSQL); sqLiteDatabase.execSQL(insertSQL); // 刷新数据展示 flushStuData(); Toast.makeText(MainActivity.this, "添加成功", Toast.LENGTH_SHORT).show(); break; case R.id.btn_update: String idStr = etId.getText().toString(); if(TextUtils.isEmpty(idStr)){ Toast.makeText(MainActivity.this, "请输入ID", Toast.LENGTH_SHORT).show(); return; } int id = Integer.parseInt(idStr); String updateSQL = String.format(Locale.CHINA, "update stu_info set name = '%s', age=%d, gender='%s' where id = %d", etName.getText().toString(), Integer.parseInt(etAge.getText().toString()), sex, id); Log.i(TAG, "operatorData: updateSQL = " + updateSQL); sqLiteDatabase.execSQL(updateSQL); // 刷新数据展示 flushStuData(); Toast.makeText(MainActivity.this, "更新成功", Toast.LENGTH_SHORT).show(); break; case R.id.btn_delete: String deleteIdStr = etId.getText().toString(); if(TextUtils.isEmpty(deleteIdStr)){ Toast.makeText(MainActivity.this, "请输入ID", Toast.LENGTH_SHORT).show(); return; } String deleteSQL = String.format(Locale.CHINA, "delete from stu_info where id = %d", Integer.parseInt(deleteIdStr)); Log.i(TAG, "operatorData: deleteSQL = " + deleteSQL); sqLiteDatabase.execSQL(deleteSQL); // 刷新数据展示 flushStuData(); Toast.makeText(MainActivity.this, "删除成功", Toast.LENGTH_SHORT).show(); break; } } } class StudentInfo { public int id; public String name; public int age; public String sex; public StudentInfo() { } public StudentInfo(int id, String name, int age, String sex) { this.id = id; this.name = name; this.age = age; this.sex = sex; } }

StuInfoAdapter.java

java
public class StuInfoAdapter extends BaseAdapter { private List<StudentInfo> stuList; private Activity context; public StuInfoAdapter(Activity context, List<StudentInfo> stuList) { this.stuList = stuList; this.context = context; } @Override public int getCount() { return stuList.size(); } @Override public Object getItem(int position) { return stuList.get(position); } @Override public long getItemId(int position) { return position; } @Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder viewHolder; if(convertView == null){ LayoutInflater inflater = context.getLayoutInflater(); convertView = inflater.inflate(R.layout.stu_item, null); viewHolder = new ViewHolder(); viewHolder.tvName = convertView.findViewById(R.id.tv_item_name); viewHolder.tvId = convertView.findViewById(R.id.tv_item_id); viewHolder.tvAge = convertView.findViewById(R.id.tv_item_age); viewHolder.tvSex = convertView.findViewById(R.id.tv_item_sex); convertView.setTag(viewHolder); }else { viewHolder = (ViewHolder) convertView.getTag(); } StudentInfo studentInfo = stuList.get(position); viewHolder.tvId.setText(String.valueOf(studentInfo.id)); viewHolder.tvName.setText(studentInfo.name); viewHolder.tvSex.setText(studentInfo.sex); viewHolder.tvAge.setText(String.valueOf(studentInfo.age)); return convertView; } // ViewHolder static class ViewHolder{ TextView tvId; TextView tvName; TextView tvSex; TextView tvAge; } }

最好不要忘记申明读写的权限(以及在Android6.0以后的动态权限申请),最后效果展示如下:

在Android 中使用SQLiteDatabase的静态方法openOrCreateDatabase(String path,SQLiteDatabae.CursorFactory factory)打开或者创建一个数据库。它会自动去检测是否存在这个数据库,如果存在则打开,不存在则创建一个数据库;创建成功则返回一个SQLiteDatabase对象,否则抛出异常FileNotFoundException。

还有需要注意的就是,查询数据返回的结果是Cursor,当我们使用SQLiteDatabase.query()方法时,会得到一个Cursor对象,Cursor指向的就是每一条数据。它提供了很多有关查询的方法,具体方法如下:

方法名称方法描述
getCount()获得总的数据项数
isFirst()判断是否第一条记录
isLast()判断是否最后一条记录
moveToFirst()移动到第一条记录
moveToLast()移动到最后一条记录
move(int offset)移动到指定记录
moveToNext()移动到下一条记录
moveToPrevious()移动到上一条记录
getColumnIndexOrThrow(String columnName)根据列名称获得列索引
getInt(int columnIndex)获得指定列索引的int类型值
getString(int columnIndex)获得指定列缩影的String类型值

这个可以参考在代码示例中flushStuData()方法中的使用:

java
private void flushStuData() { List<StudentInfo> stuList = new ArrayList<>(); // 参数解释:表名、要查询的字段、列条件、列条件参数、GroupBy、having、orderBy Cursor cursor = sqLiteDatabase.query("stu_info", null, null, null, null, null, null); if(cursor.moveToFirst()){ do{ int id = cursor.getInt(0); String name = cursor.getString(1); int age = cursor.getInt(2); String sex = cursor.getString(3); stuList.add(new StudentInfo(id, name, age, sex)); } while (cursor.moveToNext()); } cursor.close(); lvData.setAdapter(new StuInfoAdapter(this, stuList)); }

通过API来操作数据库

SQLiteDatabase也提供了insert()、delete()、update()、query()方法专门用于插入、删除、更新和查询,通过这种API的操作方式就需要编写SQL语句了,只需要传入对应的参数,即可完成CRUD操作。还是通过上面的例子,改动的地方无非就是操作数据的部分而已:

java
// ... // 查询 private void flushStuData() { List<StudentInfo> stuList = new ArrayList<>(); // 参数解释:表名、要查询的字段、列条件、列条件参数、GroupBy、having、orderBy Cursor cursor = sqLiteDatabase.query("stu_info", null, null, null, null, null, null); if(cursor.moveToFirst()){ do{ int id = cursor.getInt(0); String name = cursor.getString(1); int age = cursor.getInt(2); String sex = cursor.getString(3); stuList.add(new StudentInfo(id, name, age, sex)); } while (cursor.moveToNext()); } cursor.close(); lvData.setAdapter(new StuInfoAdapter(this, stuList)); } public void operatorData(View view) { int viewId = view.getId(); switch (viewId) { // 添加 case R.id.btn_add: // 参数解释:操作表的名称、可以为空的列、参数 ContentValues values = new ContentValues(); // Key - value values.put("name", etName.getText().toString()); values.put("age", Integer.parseInt(etAge.getText().toString())); values.put("gender", sex); // 插入成功,返回数据的ID long infoId = sqLiteDatabase.insert("stu_info", null, values); // 刷新数据展示 flushStuData(); Toast.makeText(MainActivity.this, "添加成功,Id = " + infoId, Toast.LENGTH_SHORT).show(); break; // 更新 case R.id.btn_update: String idStr = etId.getText().toString(); ContentValues updateValues = new ContentValues(); // Key - value updateValues.put("name", etName.getText().toString()); updateValues.put("age", Integer.parseInt(etAge.getText().toString())); updateValues.put("gender", sex); int info = sqLiteDatabase.update("stu_info", updateValues, "id=?", new String[]{idStr}); // 刷新数据展示 flushStuData(); Toast.makeText(MainActivity.this, "更新成功,影响行数:" + info, Toast.LENGTH_SHORT).show(); break; // 删除 case R.id.btn_delete: String deleteIdStr = etId.getText().toString(); int delete = sqLiteDatabase.delete("stu_info", "id=?", new String[]{deleteIdStr}); // 刷新数据展示 flushStuData(); Toast.makeText(MainActivity.this, "删除成功,影响行数:" + delete, Toast.LENGTH_SHORT).show(); break; } } // ...

使用LitePal操作SQLite

上面使用SQLiteDatabase来操作SQLite数据库的方法,使用起来真的很不方便,像我这种习惯使用ORM框架的人来说,SQLiteDatabase的操作方式简直太过于复杂,所以现在来看看Litepal这款开源框架吧,使用完过后自己也来尝试造一轮子,可以参考:https://github.com/huyongli/TigerDB

环境搭建

首先引入依赖:

groovy
implementation 'org.litepal.android:core:1.4.1'

接下来需要配置 litepal.xml 文件。右击 app/src/main 目录 —>New—>Directory,创建一个 assets 目录,在assets目录下新建litepal.xml文件:

xml
<?xml version="1.0" encoding="utf-8" ?> <litepal> <!-- 指定数据库名称 --> <dbname value="BookStore"/> <!-- 指定数据库版本号 --> <version value="1"/> <!-- 指定映射模型 --> <list></list> </litepal>

最后还需要修改一下 AndroidManifest.xml 中的代码:

xml
<!-- 关键就这一句:android:name="org.litepal.LitePalApplication" --> <application android:name="org.litepal.LitePalApplication" 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/AppTheme"> <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application>

现在 Litepal 的配置工作已经做完了,让我们开始正式使用它吧!

建库建表

现在开始声明一个JavaBean,也就是我们要存储的数据:

java
public class Book { private int id; private String name; private String author; public Book(){ } public Book(int id, String author, String name) { this.id = id; this.author = author; this.name = name; } // Getter and Setter ... }

并且在配置文件中配置它的映射模型:

xml
<?xml version="1.0" encoding="utf-8" ?> <litepal> <!-- 指定数据库名称 --> <dbname value="BookStore"/> <!-- 指定数据库版本号 --> <version value="1"/> <!-- 指定映射模型 --> <list> <mapping class="cn.tim.litepal_demo.Book"/> </list> </litepal>

在Activity启动时创建数据库:

java
// 创建数据库 SQLiteDatabase database = LitePal.getDatabase();

虽然有三张表,其中android_matedata表仍然不用管,table_schema表是litepal内部使用的,也可以直接忽视,Book表就是根据配置的Book类自动生成的表,是不是很方便?

数据库升级

而且Litepal很好的解决了数据库升级问题,使用SQLiteOpenHelper来升级数据库的方式:升级数据库的时候我们需要先把之前的表drop掉,然后再重新创建才行。这其实是一个非常严重的问题,因为这样会造成数据丢失,每当升级一次数据库,之前表中的数据就全没了。虽然可以通过复杂的逻辑控制来避免这种情况,但是维护成本很高。而有了LitePal,这些就都不再是问题了,使用LitePal来升级数据库非常非常简单,你完全不用思考任何的逻辑,只需要改你想改的任何内容,然后将版本号加1就行了。

比如,将图书表的中再添加一个价格的字段,再新建一张分类表:

java
public class Book { private int id; private String name; private String author; private int price; // ... } public class Category { private int id; private String name; private long count; // ... }

同样需要配置映射模型:

xml
<?xml version="1.0" encoding="utf-8" ?> <litepal> <!-- 指定数据库名称 --> <dbname value="BookStore"/> <!-- 指定数据库版本号:此时数据库版本应该为2 --> <version value="2"/> <!-- 指定映射模型 --> <list> <mapping class="cn.tim.litepal_demo.Book"/> <mapping class="cn.tim.litepal_demo.Category"/> </list> </litepal>

由此可见,book表中新增了一个price列,并且新创建了category表。

CRUD操作

下面来看看使用Litepal来对数据进行CRUD是多么方便吧:

首先需要让数据模型对象,也就是定义的Javabean来继承DataSupport

java
public class Book extends DataSupport { ... }

下面直接通过查看log日志的方式来验证LitePal框架的CRUD:

java
public class MainActivity extends AppCompatActivity { private static final String TAG = "MainActivity"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // 创建数据库 SQLiteDatabase database = LitePal.getDatabase(); // 添加数据 Book book = new Book("Think In Java", "Tim", 58); boolean saveRet = book.save(); Log.i(TAG, "onCreate: saveRet = " + saveRet); new Book("Think In C/C++", "Tom", 38).save(); Log.i(TAG, "onCreate: 添加数据成功"); // 查询数据 List<Book> bookList = DataSupport.findAll(Book.class); Book[] books = new Book[bookList.size()]; bookList.toArray(books); Log.i(TAG, "onCreate: books = " + Arrays.toString(books)); // 删除数据 int delete = DataSupport.delete(Book.class, books[0].getId()); Log.i(TAG, "onCreate: 删除数据成功,delete = " + delete); // 查询数据 bookList = DataSupport.findAll(Book.class); books = new Book[bookList.size()]; bookList.toArray(books); Log.i(TAG, "onCreate: books = " + Arrays.toString(books)); // 修改数据 Book cppBook = new Book("Think In C/C++", "Tom", 28); int update = cppBook.update(2); Log.i(TAG, "onCreate: 修改数据成功,update = " + update); // 查询数据 bookList = DataSupport.findAll(Book.class); books = new Book[bookList.size()]; bookList.toArray(books); Log.i(TAG, "onCreate: books = " + Arrays.toString(books)); } }

上面演示了具体的操作,其实还有很多高级查询方法,这里不再赘述,如果用到可以参考作者的博客《Android数据库高手秘籍》

本文作者:Tim

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!