安卓动态 Sqlite 数据库管理 v.2






4.91/5 (6投票s)
关于在安卓平台上使用 Java 类反射进行动态 Sqlite 数据库管理的文章。
来源
GitHub : https://github.com/Pavel-Durov/CodeProject---Android-dynamic-Sqlite-database-management-v.2
直接
目录
- 引言
- 要求
- 反射简介
- 背景
- 安卓 Application 类
- 实现 DataBaseHandler 类
- 实现 DB_BASIC 和 TEST 类
- 在 Sqlite 数据库中创建 TEST 表
- 将 TEST 类的值插入到 sqlite 的 TEST 表中
- 创建动态表
- DataBaseHandler 方法
- 摘要
引言
在下文中,我将举例说明如何通过 Java 类的反射在安卓平台上管理和实现 Sqlite 数据库。
要求
1.对反射有基本了解。
2.对安卓应用程序结构有基本了解。
3.对 Sqlite 有基本了解。
反射简介
反射是一种语言能力,它使你能够根据特定对象的结构来检查并动态调用其类、方法、属性等,而无需事先了解这些结构。
由于反射发生在运行时,它使你的代码非常灵活和动态。即使在编译时不知道一个类的构成,你也可以确定它。
背景
起初,当我开始使用安卓 sqlite 数据库时,我非常沮丧,因为在我读到的每一个教程中,与 sqlite 数据库的集成都是通过原始的硬编码字符串查询来完成的,没有任何动态能力。
数据库结构中的每一个小改动都非常困难,耗费我大量时间,因为我需要处理那些硬编码的字符串。
安卓 application 类
安卓 application 类是安卓应用的基础类。在 AnadroidManifest.xml 文件中声明 application 类后,当你的应用程序/包的进程创建时,它将被实例化。
如果你想在整个进程生命周期中保持实例,声明自定义的 application 类是必要的。
在我们的案例中,我们将在 application 类中创建一次 DataBaseHandler,之后我们可以在程序的任何活动中访问它。
声明一个 Application 类
首先,我们需要创建我们的基类,它扩展了 SDK 的 application 类,我们称之为 DyncamicSqliteApplication。
public class DyncamicSqliteApplication extends Application
{
@Override
public void onCreate()
{
super.onCreate();
}
}
然后,我们需要在 AnadroidManifest.xml 文件的 <application> 标签下,将其声明为 “android:name” 属性的值。
<application
android:allowBackup="true"
android:name="net.simplydone.main.DyncamicSqliteApplication"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<activity
android:name="net.simplydone.gui.MainActivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
如果你在 onCreate() 方法中设置一个断点并启动你的安卓程序,你会看到,随着你的程序进程的启动,首先被调用的就是你的 application 类的创建。
实现 DataBaseHandler 类
为了实现 sqlite 集成,我们将在我们的 application 类中创建一个 DataBaseHandler
类。
为了创建我们的 DataBaseHandler
,我们将继承 SQLiteOpenHelper
这个安卓 SDK 类。
SQLiteOpenHelper
- 一个用于管理数据库创建和版本管理的帮助类。
更多信息请参见:
https://developer.android.com.cn/reference/android/database/sqlite/SQLiteOpenHelper.html
public class DataBaseHandler extends SQLiteOpenHelper
{
public DataBaseHandler (Context context)
{
super(context, ConstantsCollection.DATABASE_NAME, null,
ConstantsCollection.DATABASE_VERSION);
}
}
在 DataBaseHandler 构造函数中,我们通过调用父类的构造函数来指定数据库名称和数据库版本。
我们的 DataBaseHandler 类将成为我们的应用程序和 Sqlite 数据库之间的中介层。
实现 DB_BASIC 和 TEST 类
DataBaseHandler
类中的所有其他方法将基于 Java 反射,并根据传递的类结构或类型,实现一些 sqlite 函数,如 ‘CREATE TABLE’(创建表)、“UPDATE”(更新)、“INSERT”(插入)等。
创建 DB_BASIC 类
public class DB_BASIC
{
public Long ID;
}
DB_BASIC
类只有一个 Long 类型的字段,名为 ID
。我们用于基于 sqlite 反射的方法的每个类都将继承这个类。
ID 字段对于每个集成类来说都是必不可少的,因为每个 sqlite 行都必须包含该字段作为其主键。这为未来的 Sqlite 操作创建了一个约定。
我们的 TEST
类将继承 DB_BASIC
类,并包含各种不同类型的数字字段。
TEST
类应该是一个非常简单、清晰的类。只有可以被解析为 Sqlite 格式的原始类型成员才能成为它的一部分。
public class TEST extends DB_BASIC
{
public Double TEST_DOUBLE_FIELD_0;
public Long TEST_LONG_FIELD_1;
public Integer TEST_INTEGER_FIELD_2;
public String TEST_STRING_FIELD_3;
}
在 Sqlite 数据库中创建 TEST 表
在我们的 Application 类中,我们将实例化我们的 DataBaseHandler
,并将 Application 的 Context
实例传递给它的构造函数,然后我们将调用 addObjectTable()
方法,传递一个新的 TEST
类,以便为我们的 TEST
对象存储创建一个兼容的表。
private void InitDB()
{
//Initialize DB handler
_dbhandler = new DataBaseHandler (this);
//Adding table based on class TEST reflection
_dbhandler.addObjectTable(new TEST());
}
addObjectTable()
方法将使用反射遍历 TEST
类的字段,并根据 TEST
类的结构创建相应的 sqlite 表。
表的名称将与传递的类实例的名称相同。表中记录的名称及其类型将与对象中指定的相同。
将 TEST 类的值插入到 sqlite 的 TEST 表中
在我们实例化 DataBaseHandler
类并创建了 TEST
表之后,我们可以执行一个插入操作,如下所示:
创建一个具有一些不同值的 TEST
类的实例。
TEST result = new TEST();
result.TEST_DOUBLE_FIELD_0 = 0.890;
result.TEST_INTEGER_FIELD_2 = 34;
result.TEST_STRING_FIELD_3 = "This is string field";
当使用 AddNewObject()
方法时,我们只需要将 TEST
类传递给 DataBaseHandler
,剩下的工作就会根据类的实现自动完成。
创建动态表
首先,我们需要在数据库中创建一个对象,所以请在主活动屏幕上按下“Add new object to DB”(向数据库添加新对象)按钮。
该操作将调用我们 application 类中的 GetDB()
方法。
public DataBaseHandler GetDB()
{
if(_dbhandler == null)
{
InitDB();
}
return _dbhandler;
}
如果我们的 _dbhandler 尚未初始化,此方法将调用 InitDB() 方法。
private void InitDB()
{
//Initialize DB handler
_dbhandler = new DataBaseHandler(this);
//Adding table based on class TEST reflection
_dbhandler.CreateTable(new TEST());
}
InitDB
方法的全部目的是实例化我们的 DataBaseHandler
类,并在我们的数据库中创建一个 TEST
类型的表。
在 GetDB
方法执行后,我们从活动中调用 AddNewObject()
方法。这将遍历传递的对象的字段,并根据它们的类型和名称将数据插入到表中。
在我们创建了 handler 类和插入了值的表之后,我们可以通过运行这些 adb 命令从我们的安卓设备中提取 Sqlite 数据库进行检查。
如果你对 adb 工具不熟悉,请考虑阅读我的相关文章:
https://codeproject.org.cn/Articles/825304/Accessing-internal-data-on-Android-device
https://codeproject.org.cn/Tips/827908/Android-ADB-Commands-Using-Batch-File
cd %ANDROID_SDK%/platform-tools adb shell su -c "chmod 777 /data" adb shell su -c "chmod 777 /data/data" adb shell su -c "chmod 777 /data/data/net.simplydone.dynamicsqlite" adb shell su -c "chmod 777 /data/data/net.simplydone.dynamicsqlite/databases" adb shell su -c "chmod 777 /data/data/net.simplydone.dynamicsqlite/databases/DynamicSqlite" adb pull /data/data/net.simplydone.dynamicsqlite/databases/DynamicSqlite C:/Users/Pavel/Desktop
请注意,提取出的 DynamicSqlite 文件没有扩展名。如果你的 Sqlite 浏览器无法打开它,请尝试为其添加一个 db 扩展名。只需将其从 DynamicSqlite 重命名为 DynamicSqlite.db。
现在我们的机器上有了这个文件,我们可以检查它了。
我正在使用 DB Browser for SQLite,但任何其他类似的程序都可以。
如你所见,我们的数据库中有一个 TEST
表,其列名和类型与我们的 TEST
对象结构相同。
请注意,创建了 ID
列,因为它是 DB_BASIC
类的成员。另外,Long
类型被保存为 INTEGER
。
来自 Sqlite 网站:
INTEGER
。该值是一个有符号整数,根据值的大小存储在 1、2、3、4、6 或 8 个字节中。
https://www.sqlite.org/datatype3.html
public class TEST extends DB_BASIC
{
public Double TEST_DOUBLE_FIELD_0;
public Long TEST_LONG_FIELD_1;
public Integer TEST_INTEGER_FIELD_2;
public String TEST_STRING_FIELD_3;
}
我们的 DataBaseHandler
在 GetSqliteType(Class<?> c)
方法中决定为 Java 类型选择哪种 Sqlite 类型,它只是检查 Java 类型,并相应地从我们的 ConstantCollection
类返回 Sqlite 的原始字符串类型。
如果你打算使用除我用于我们对象的类型之外的其他类型,你可以在 GetSqliteType(Class<?> c)
方法中进行修改。
private String GetSqliteType(Class<?> c)
{
String type = "TEXT";
if (c.equals(String.class))
{
type = ConstantsCollection.SQLITE_TEXT;
}
else if ( c.equals(Integer.class)
|| c.equals(Long.class)
|| c.equals(Number.class)
|| c.equals(java.util.Date.class))
{
type = ConstantsCollection.SQLITE_INTEGER;
}
else if(c.equals(Double.class))
{
type = ConstantsCollection.SQLITE_DOUBLE;
}
return type;
}
如果我们查看创建的表的数据,我们会发现插入到表中的数据与我们实例化 TEST
对象时的数据相同。
DataBaseHandler 方法
AddNewObject 方法
正如我们之前提到的,我们的 DataBaseHandler
类中的方法使用 Java 反射。
DataBaseHandler
类中的每个公共方法都接受一个 DB_BASIC
对象实例或一个 Class
对象作为参数,因为所有方法的操作都是由 Java 类的结构定义的。
public long AddNewObject(DB_BASIC object)
{
long result = ConstantsCollection.INDEX_NOT_DEFINED;
if(object != null)
{
SQLiteDatabase db = null;
try
{
db = this.getWritableDatabase();
ContentValues values = new ContentValues();
Class<? extends DB_BASIC> c = object.getClass();
Field[] fields = c.getFields();
for (Field field : fields)
{
Object val = GetValue(field, object);
if (val != null)
{
String rawValue = null;
if (field.getType().equals(Date.class))
{
try
{
rawValue = DateUtils.DateToValue((Date) val);
}
catch (ParseException e)
{
Log.e(LOG_TAG, e.toString());
}
}
else
{
rawValue = val.toString();
}
String name = field.getName();
values.put(name, rawValue);
}
}
String tableName = ReflectionUtils.GetClassName(object.getClass());
if(values.size() > 0)
{
result = db.insert(tableName, null, values);
}
}
finally
{
CloseDB(db);
}
}
return result;
}
这个概念非常简单。该方法遍历类的字段;它将其 Java 值转换为 Sqlite 类型,并将对象添加到由类名标识的相应数据库表中。
CreateTable 方法
这个方法从我们的 application 类中调用,它是我们数据库创建过程中的一个关键步骤,因为它创建了我们的 DB_BASIC
对象表。
public void CreateTable(DB_BASIC object)
{
if(object != null)
{
SQLiteDatabase db = null;
try
{
db = getWritableDatabase();
Class<? extends DB_BASIC> c = object.getClass();
String tableName = c.getName();
tableName = ReflectionUtils.GetClassName(c);
//Fields of the object
Field[] fields = c.getFields();
StringBuilder sbCreateTable = new StringBuilder();
//Beginning of the CREATE raw query
sbCreateTable.append(ConstantsCollection.SQLITE_CREATE_TABLE_IF_NOT_EXISTS);
sbCreateTable.append(tableName);
sbCreateTable.append(ConstantsCollection.SQLITE_OPENNING_BRACKET);
//Iterates on the given object fields using reflection
//and creates appropriate column definition
for (int i = 0; i < fields.length; i++)
{
String fieldName= fields[i].getName();
if(fieldName.equalsIgnoreCase(ConstantsCollection.ID))
{//Creates an auto increament index named ID
sbCreateTable.append(fieldName);
sbCreateTable.append(ConstantsCollection
.SQLITE_INTEGER_PRIMARY_KEY_AUTOINCREMENT);
}
else
{//Creates column declaration
String rowname = GetSqliteType(fields[i].getType());
if(rowname != null)
{
sbCreateTable.append(fieldName);
sbCreateTable.append(ConstantsCollection.SQLITE_SPACE);
sbCreateTable.append(rowname);
}
}
if(i != fields.length - 1)
{//Allways adds , in the end of each column declaration except the last one
sbCreateTable.append(ConstantsCollection.SQLITE_COMMA);
sbCreateTable.append(ConstantsCollection.SQLITE_SPACE);
}
}
//Closing raw CREATE Query with }; characters
sbCreateTable.append(ConstantsCollection.SQLITE_CLOSING_BRACKET);
sbCreateTable.append(ConstantsCollection.SQLITE_CLOSING_SEMICOLUMN);
//Executes raw SQlite statement
db.execSQL(sbCreateTable.toString());
}
catch (SecurityException e)
{
Log.e(LOG_TAG, e.toString());
}
catch (Exception e)
{
Log.e(LOG_TAG, e.toString());
}
finally
{
//Closing the DB connection
CloseDB(db);
}
}
}
正如你所见,AddNewObject
和 CreateTable
方法的核心并没有太大区别,它们都接受一个 DB_BASIC
类作为参数,并通过使用反射,通过遍历给定对象的成员来生成相应的原始字符串 Sqlite 命令。
请注意,我们在这里使用的是 StringBuilder
类,而不是常规的 String
。
ConvertCursorToObject 方法
ConvertCursorToObject
方法仅由我们 DataBasehandler
类中的 GetTableData
方法调用,它负责将作为 Sqlite 查询结果的给定 Cursor
对象转换为传递的 Class<? extends DB_BASIC>
实例参数。
Cursor 是一个接口,它提供了对数据库查询返回的结果集的随机读写访问。
Cursor 文档
https://developer.android.com.cn/reference/android/database/Cursor.html
我们方法中做的第一件事是通过调用 moveToFirst()
方法移动到 Cursor
对象中的第一条记录。
之后,我们将进入 do-while 循环,该循环会一直进行,直到我们到达 Cursor
中的最后一条记录,即直到 moveToNext()
方法返回 false。
接下来,ConvertCursorToObject 方法使用 ReflectionUtils
类实例化给定的类,并对 Cursor
的列进行迭代,并相应地在实例化的对象中设置值。
它将这些对象收集到一个 List
中,并将其作为结果返回。
@SuppressWarnings("unchecked")
private <T> List<T> ConvertCursorToObjects(Cursor cursor, Class<?> clazz)
{
List<T> list = new ArrayList<T>();
//moves the cursor tyo the first row
if (cursor.moveToFirst())
{
String[] ColumnNames = cursor.getColumnNames();
do
{
Object obj = ReflectionUtils.GetObject(clazz);
//iterates on column names
for (int i = 0; i < ColumnNames.length; i++)
{
try {
Field field = obj.getClass().getField(ColumnNames[i]);
Object objectValue = null;
String str = cursor.getString(i);
if(str != null)
{
//Converting stored Sqlite data to java objects
if (field.getType().equals(java.util.Date.class))
{
Date date = DateUtils.ValueToDate(str);
objectValue = date;
field.set(obj, objectValue);
}
else if (field.getType().equals(Number.class))
{
objectValue = NumberFormat.getInstance().parse(str);
}
else if(field.getType().equals(Long.class) )
{
objectValue = NumberFormat.getInstance().parse(str);
long value = Long.parseLong(objectValue.toString());
field.set(obj, value);
}
else if(field.getType().equals(Integer.class) )
{
objectValue = NumberFormat.getInstance().parse(str);
int value = Integer.parseInt(str);
field.set(obj, value);
}
else if(field.getType().equals(Double.class) )
{
objectValue = NumberFormat.getInstance().parse(str);
double value = Double.parseDouble(objectValue.toString());
field.set(obj, value);
}
else
{
objectValue = str;
field.set(obj, objectValue);
}
}
}
catch (IllegalArgumentException e)
{
Log.e(LOG_TAG, e.toString());
}
catch (IllegalAccessException e)
{
Log.e(LOG_TAG, e.toString());
}
catch (ParseException e)
{
Log.e(LOG_TAG, e.toString());
}
catch (SecurityException e)
{
Log.e(LOG_TAG, e.toString());
}
catch (NoSuchFieldException e)
{
Log.e(LOG_TAG, e.toString());
}
}
if(obj instanceof DB_BASIC)
{
list.add((T) obj);
}
} while (cursor.moveToNext());
}
return list;
}
UpdateRow
此方法使用给定对象的新值更新相关表中的现有记录。
更新操作是通过位于 DB_BASIC
类中的 ID
标识字段执行的,这就是为什么首先要检查 object.Id 是否不为 null。
和之前一样,它遍历对象的内部字段,并根据它们的名称和值,使用 ContentValues
类来组合用于记录更新的 Sqlite 查询。
它返回更新的行数,这个数字应该总是 1 或 0,因为在我们的应用程序数据库上下文中,ID
字段是唯一的。
public int UpdateRow(DB_BASIC object)
{
int result = ConstantsCollection.INDEX_NOT_DEFINED;
if(object != null && object.ID != null)
{
SQLiteDatabase db = null;
db = this.getWritableDatabase();
String tableName = ReflectionUtils.GetClassName(object.getClass());
StringBuilder sbWhereClause = new StringBuilder();
sbWhereClause.append(ConstantsCollection.SQLITE_SPACE);
sbWhereClause.append(ConstantsCollection.ID);
sbWhereClause.append(ConstantsCollection.SQLITE_EQUAL_SIGN);
sbWhereClause.append(String.valueOf(object.ID));
ContentValues values = new ContentValues();
//iterates on fields
for(Field f : object.getClass().getFields())
{
String fieldValue = GetStringValue(f, object);
String name = f.getName();
if( fieldValue != null
&& !fieldValue.equals(StringUtils.EMPTY_STRING)
&& !name.equals(ConstantsCollection.ID) )
{
values.put(name, fieldValue);
}
}
result = db.update(tableName, values, sbWhereClause.toString(), null);
}
return result;
}
摘要
在本文中,我向你展示了附加的安卓项目中实现的基本功能。
DataBaseHandler
类中还有更多可以添加的功能,我们没有讨论。
DataBaseHandler
是一个基础架构类。实现它之后,你就不再需要担心 sqlite 集成的问题了,你所需要做的就是创建你的业务逻辑类,当将它们保存到数据库时,只需将它们转换为 DB_BASIC
类,并根据你的意图将它们传递给 DataBaseHandler
类中的相应方法。
使用这种基于 java 反射的动态方法,我节省了大量时间。希望它对你也有用。