65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.91/5 (6投票s)

2014年10月16日

CPOL

8分钟阅读

viewsIcon

45074

downloadIcon

1258

关于在安卓平台上使用 Java 类反射进行动态 Sqlite 数据库管理的文章。

来源

GitHub : https://github.com/Pavel-Durov/CodeProject---Android-dynamic-Sqlite-database-management-v.2
直接

目录

引言

在下文中,我将举例说明如何通过 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;
}

我们的 DataBaseHandlerGetSqliteType(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);
        }
    }
}

正如你所见,AddNewObjectCreateTable 方法的核心并没有太大区别,它们都接受一个 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 反射的动态方法,我节省了大量时间。希望它对你也有用。

 

 

© . All rights reserved.