MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Android开发:结合SQLiteOpenHelper和SQLiteDatabase

2022-04-134.9k 阅读

1. SQLite 简介

SQLite 是一款轻型的数据库,它是遵守 ACID 的关系型数据库管理系统,它的设计目标是嵌入式的,而且目前已经在很多嵌入式产品中使用了它,它占用资源非常的低,在嵌入式设备中,可能只需要几百K的内存就够了。它能够支持 Windows/Linux/Unix 等等主流的操作系统,同时能够跟很多程序语言相结合,比如 Tcl、C#、PHP、Java 等,还有 Android 系统中同样也集成了 SQLite 数据库。

SQLite 最大的特点就是它不是一个客户端/服务器结构的数据库,而是把整个数据库包含在一个单独的文件中,这使得它非常适合嵌入式设备。而且 SQLite 处理事务非常快速,能够满足大部分移动应用对于数据存储的需求。

2. SQLiteOpenHelper 类解析

2.1 作用

SQLiteOpenHelper 类是 Android 提供的一个帮助类,用于管理数据库的创建和版本管理。它极大地简化了数据库操作的流程,开发者只需要继承这个类并实现几个关键的方法,就可以轻松地管理数据库的创建、升级和降级等操作。

2.2 重要方法

  • 构造函数

    public SQLiteOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version)
    
    • context:上下文对象,通常为 ActivityApplication 的实例,用于获取应用程序的一些环境信息,比如文件存储路径等。
    • name:数据库的名称,如果传入 null,则会创建一个内存中的数据库,当应用关闭时,这个数据库也会消失。
    • factory:游标工厂,一般传入 null,使用默认的游标工厂即可。
    • version:数据库的版本号,当数据库结构发生变化时,需要更新这个版本号,以便触发 onUpgrade 方法进行数据库升级操作。
  • onCreate(SQLiteDatabase db) 这个方法在数据库第一次创建时被调用。在这个方法中,我们可以执行创建表、插入初始数据等操作。

    @Override
    public void onCreate(SQLiteDatabase db) {
        String createTable = "CREATE TABLE IF NOT EXISTS users (" +
                "_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
                "name TEXT, " +
                "age INTEGER)";
        db.execSQL(createTable);
    }
    

    上述代码创建了一个名为 users 的表,包含三个列:_id(自增长的主键)、name(文本类型)和 age(整数类型)。

  • onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) 当数据库的版本号发生变化时,这个方法会被调用。我们可以在这个方法中实现数据库的升级逻辑,比如添加新表、修改现有表结构等。

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        if (oldVersion < 2) {
            String addColumn = "ALTER TABLE users ADD COLUMN email TEXT";
            db.execSQL(addColumn);
        }
    }
    

    上述代码检查旧版本号是否小于 2,如果是,则向 users 表中添加一个名为 email 的新列。

  • onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) 这个方法与 onUpgrade 相反,当数据库版本号降低时被调用。在实际开发中,数据库降级操作相对较少,但如果需要支持降级,就可以在这个方法中实现相应逻辑。

    @Override
    public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        if (oldVersion >= 2) {
            String dropColumn = "ALTER TABLE users DROP COLUMN email";
            db.execSQL(dropColumn);
        }
    }
    

    上述代码检查旧版本号是否大于等于 2,如果是,则从 users 表中删除 email 列。

2.3 示例代码

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;

public class MyDatabaseHelper extends SQLiteOpenHelper {
    private static final String DATABASE_NAME = "my_database.db";
    private static final int DATABASE_VERSION = 2;

    public MyDatabaseHelper(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        String createTable = "CREATE TABLE IF NOT EXISTS users (" +
                "_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
                "name TEXT, " +
                "age INTEGER)";
        db.execSQL(createTable);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        if (oldVersion < 2) {
            String addColumn = "ALTER TABLE users ADD COLUMN email TEXT";
            db.execSQL(addColumn);
        }
    }

    @Override
    public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        if (oldVersion >= 2) {
            String dropColumn = "ALTER TABLE users DROP COLUMN email";
            db.execSQL(dropColumn);
        }
    }
}

3. SQLiteDatabase 类解析

3.1 作用

SQLiteDatabase 类代表一个 SQLite 数据库实例,通过它我们可以执行各种数据库操作,比如插入数据、查询数据、更新数据和删除数据等。它提供了丰富的方法来满足不同的数据库操作需求。

3.2 获取实例

通过 SQLiteOpenHelper 类的 getReadableDatabase()getWritableDatabase() 方法可以获取 SQLiteDatabase 实例。

MyDatabaseHelper helper = new MyDatabaseHelper(context);
SQLiteDatabase db = helper.getWritableDatabase();

getReadableDatabase() 方法在数据库可写时返回一个可读写的数据库实例,当数据库不可写(比如磁盘空间不足)时,返回一个只读的数据库实例。getWritableDatabase() 方法则始终尝试返回一个可读写的数据库实例,如果数据库不可写,可能会抛出异常。

3.3 基本操作方法

  • 插入数据 可以使用 insert() 方法插入数据。

    ContentValues values = new ContentValues();
    values.put("name", "John");
    values.put("age", 25);
    long newRowId = db.insert("users", null, values);
    

    insert() 方法的第一个参数是表名,第二个参数是 nullColumnHack,当 ContentValues 为空时,这个参数指定一个列名,数据库会将这个列的值设为 null,一般传入 null 即可。第三个参数是 ContentValues 对象,它类似一个键值对集合,用于存储要插入的数据。

  • 查询数据 使用 query() 方法进行查询。

    String[] projection = {"_id", "name", "age"};
    String selection = "age >?";
    String[] selectionArgs = {"20"};
    Cursor cursor = db.query(
            "users",
            projection,
            selection,
            selectionArgs,
            null,
            null,
            null
    );
    
    • projection:指定要查询的列,如果传入 null,则查询所有列。
    • selection:查询条件,类似 SQL 语句中的 WHERE 子句。
    • selectionArgs:用于替换 selection 中的占位符(?)的值。

    也可以使用 rawQuery() 方法执行原生的 SQL 查询语句。

    Cursor cursor = db.rawQuery("SELECT * FROM users WHERE age > 20", null);
    
  • 更新数据 通过 update() 方法更新数据。

    ContentValues values = new ContentValues();
    values.put("age", 26);
    String selection = "name =?";
    String[] selectionArgs = {"John"};
    int count = db.update(
            "users",
            values,
            selection,
            selectionArgs
    );
    

    update() 方法的第一个参数是表名,第二个参数是 ContentValues 对象,包含要更新的数据。第三个参数是更新条件,第四个参数是用于替换条件中占位符的值。

  • 删除数据 利用 delete() 方法删除数据。

    String selection = "name =?";
    String[] selectionArgs = {"John"};
    int count = db.delete("users", selection, selectionArgs);
    

    delete() 方法的第一个参数是表名,第二个参数是删除条件,第三个参数是用于替换条件中占位符的值。

3.4 事务处理

SQLite 支持事务,通过 SQLiteDatabase 类可以方便地进行事务操作。事务可以确保一组数据库操作要么全部成功执行,要么全部失败回滚,从而保证数据的一致性。

db.beginTransaction();
try {
    ContentValues values1 = new ContentValues();
    values1.put("name", "Alice");
    values1.put("age", 30);
    db.insert("users", null, values1);

    ContentValues values2 = new ContentValues();
    values2.put("name", "Bob");
    values2.put("age", 35);
    db.insert("users", null, values2);

    db.setTransactionSuccessful();
} finally {
    db.endTransaction();
}

在上述代码中,先调用 beginTransaction() 开始一个事务,然后执行一系列数据库操作,当所有操作都成功完成后,调用 setTransactionSuccessful() 标记事务成功,最后在 finally 块中调用 endTransaction() 结束事务。如果在事务执行过程中发生异常,没有调用 setTransactionSuccessful(),则事务会回滚,之前执行的数据库操作都不会生效。

4. 结合使用 SQLiteOpenHelper 和 SQLiteDatabase

4.1 数据操作示例

下面以一个完整的示例展示如何结合 SQLiteOpenHelperSQLiteDatabase 进行数据的增删改查操作。

import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;

public class UserDatabaseHelper extends SQLiteOpenHelper {
    private static final String DATABASE_NAME = "user_database.db";
    private static final int DATABASE_VERSION = 1;
    private static final String TABLE_NAME = "users";
    private static final String COLUMN_ID = "_id";
    private static final String COLUMN_NAME = "name";
    private static final String COLUMN_AGE = "age";

    public UserDatabaseHelper(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        String createTable = "CREATE TABLE " + TABLE_NAME + " (" +
                COLUMN_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
                COLUMN_NAME + " TEXT, " +
                COLUMN_AGE + " INTEGER)";
        db.execSQL(createTable);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        // 目前版本无需升级逻辑
    }

    // 插入用户
    public long insertUser(String name, int age) {
        SQLiteDatabase db = this.getWritableDatabase();
        ContentValues values = new ContentValues();
        values.put(COLUMN_NAME, name);
        values.put(COLUMN_AGE, age);
        long newRowId = db.insert(TABLE_NAME, null, values);
        db.close();
        return newRowId;
    }

    // 查询所有用户
    public Cursor getAllUsers() {
        SQLiteDatabase db = this.getReadableDatabase();
        String[] projection = {COLUMN_ID, COLUMN_NAME, COLUMN_AGE};
        Cursor cursor = db.query(
                TABLE_NAME,
                projection,
                null,
                null,
                null,
                null,
                null
        );
        return cursor;
    }

    // 更新用户年龄
    public int updateUserAge(String name, int newAge) {
        SQLiteDatabase db = this.getWritableDatabase();
        ContentValues values = new ContentValues();
        values.put(COLUMN_AGE, newAge);
        String selection = COLUMN_NAME + " =?";
        String[] selectionArgs = {name};
        int count = db.update(
                TABLE_NAME,
                values,
                selection,
                selectionArgs
        );
        db.close();
        return count;
    }

    // 删除用户
    public int deleteUser(String name) {
        SQLiteDatabase db = this.getWritableDatabase();
        String selection = COLUMN_NAME + " =?";
        String[] selectionArgs = {name};
        int count = db.delete(TABLE_NAME, selection, selectionArgs);
        db.close();
        return count;
    }
}

Activity 中使用这个数据库帮助类:

import android.database.Cursor;
import android.os.Bundle;
import android.widget.TextView;
import android.widget.Toast;

import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {
    private UserDatabaseHelper userDatabaseHelper;

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

        userDatabaseHelper = new UserDatabaseHelper(this);

        // 插入用户
        long newRowId = userDatabaseHelper.insertUser("Tom", 22);
        if (newRowId != -1) {
            Toast.makeText(this, "User inserted successfully with ID: " + newRowId, Toast.LENGTH_SHORT).show();
        } else {
            Toast.makeText(this, "Failed to insert user", Toast.LENGTH_SHORT).show();
        }

        // 查询所有用户
        Cursor cursor = userDatabaseHelper.getAllUsers();
        if (cursor.moveToFirst()) {
            TextView textView = findViewById(R.id.textView);
            StringBuilder result = new StringBuilder("All Users:\n");
            do {
                int id = cursor.getInt(cursor.getColumnIndex(UserDatabaseHelper.COLUMN_ID));
                String name = cursor.getString(cursor.getColumnIndex(UserDatabaseHelper.COLUMN_NAME));
                int age = cursor.getInt(cursor.getColumnIndex(UserDatabaseHelper.COLUMN_AGE));
                result.append("ID: ").append(id).append(", Name: ").append(name).append(", Age: ").append(age).append("\n");
            } while (cursor.moveToNext());
            textView.setText(result.toString());
        } else {
            Toast.makeText(this, "No users found", Toast.LENGTH_SHORT).show();
        }
        cursor.close();

        // 更新用户年龄
        int updateCount = userDatabaseHelper.updateUserAge("Tom", 23);
        if (updateCount > 0) {
            Toast.makeText(this, "User age updated successfully", Toast.LENGTH_SHORT).show();
        } else {
            Toast.makeText(this, "Failed to update user age", Toast.LENGTH_SHORT).show();
        }

        // 删除用户
        int deleteCount = userDatabaseHelper.deleteUser("Tom");
        if (deleteCount > 0) {
            Toast.makeText(this, "User deleted successfully", Toast.LENGTH_SHORT).show();
        } else {
            Toast.makeText(this, "Failed to delete user", Toast.LENGTH_SHORT).show();
        }
    }
}

4.2 实际应用场景

在实际的 Android 应用开发中,结合 SQLiteOpenHelperSQLiteDatabase 可以实现各种数据存储需求。比如一个笔记应用,可以使用 SQLiteOpenHelper 创建和管理数据库,用 SQLiteDatabase 来插入新笔记、查询已有的笔记、更新笔记内容以及删除不再需要的笔记。

又比如一个电商应用的购物车功能,用户添加商品到购物车时,可以通过 SQLiteDatabase 的插入操作将商品信息存储到本地数据库,在用户浏览购物车时,通过查询操作获取购物车中的商品列表,当用户修改商品数量或者删除商品时,使用更新和删除操作来同步数据库中的数据。

通过合理地运用这两个类,能够有效地管理本地数据,提高应用的性能和用户体验。同时,需要注意在进行数据库操作时,要及时关闭数据库连接,避免资源泄漏等问题。在多线程环境下,也要注意数据库操作的线程安全,确保数据的一致性和完整性。例如,可以使用同步块或者 SQLiteDatabase 提供的线程安全机制来处理多线程访问数据库的情况。

5. 优化与注意事项

5.1 性能优化

  • 批量操作:在进行插入、更新或删除操作时,如果有多个数据需要处理,尽量采用批量操作。例如,在插入多条数据时,可以使用 SQLiteDatabaseinsertWithOnConflict() 方法结合事务进行批量插入,这样可以减少数据库操作的次数,提高效率。
    db.beginTransaction();
    try {
        for (User user : userList) {
            ContentValues values = new ContentValues();
            values.put("name", user.getName());
            values.put("age", user.getAge());
            db.insertWithOnConflict("users", null, values, SQLiteDatabase.CONFLICT_REPLACE);
        }
        db.setTransactionSuccessful();
    } finally {
        db.endTransaction();
    }
    
  • 索引优化:为经常用于查询条件的列创建索引。例如,如果经常根据 name 列查询用户,那么可以在创建表时为 name 列添加索引。
    String createTable = "CREATE TABLE users (" +
            "_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
            "name TEXT, " +
            "age INTEGER);" +
            "CREATE INDEX idx_name ON users (name);";
    db.execSQL(createTable);
    
    索引可以加快查询速度,但也会增加插入、更新和删除操作的时间,因为数据库需要同时维护索引。所以要根据实际的业务需求合理创建索引。

5.2 注意事项

  • 数据库版本管理:在应用开发过程中,随着功能的不断迭代,数据库结构可能会发生变化。正确管理数据库版本非常重要,每次数据库结构变动时,都要更新 SQLiteOpenHelper 中的版本号,并在 onUpgradeonDowngrade 方法中实现相应的升级和降级逻辑。否则,可能会导致数据丢失或应用崩溃。
  • 内存管理:虽然 SQLite 是轻型数据库,但在频繁进行数据库操作时,也要注意内存管理。及时关闭游标(Cursor)和数据库连接,避免内存泄漏。例如,在查询操作结束后,要调用 cursor.close() 关闭游标。
    Cursor cursor = db.query("users", null, null, null, null, null, null);
    try {
        if (cursor.moveToFirst()) {
            // 处理数据
        }
    } finally {
        if (cursor != null) {
            cursor.close();
        }
    }
    
  • 并发访问:在多线程环境下访问 SQLite 数据库时,要注意并发问题。SQLite 本身支持多线程访问,但如果多个线程同时进行写操作,可能会导致数据不一致。可以使用事务结合同步机制来保证数据的一致性。例如,使用 synchronized 关键字来同步对数据库的写操作。
    private static final Object lock = new Object();
    
    public void updateUser(User user) {
        synchronized (lock) {
            SQLiteDatabase db = this.getWritableDatabase();
            ContentValues values = new ContentValues();
            values.put("name", user.getName());
            values.put("age", user.getAge());
            String selection = "_id =?";
            String[] selectionArgs = {String.valueOf(user.getId())};
            db.update("users", values, selection, selectionArgs);
            db.close();
        }
    }
    

通过合理运用 SQLiteOpenHelperSQLiteDatabase,并注意性能优化和相关注意事项,能够在 Android 开发中高效地管理本地数据库,为应用提供稳定的数据存储支持。无论是小型的个人应用还是大型的商业应用,都可以借助 SQLite 的强大功能满足数据存储和管理的需求。同时,随着 Android 开发技术的不断发展,也可以结合 Room 等更高层次的数据库框架,进一步简化数据库操作并提升开发效率,但理解 SQLiteOpenHelperSQLiteDatabase 的底层原理,对于优化数据库操作和解决问题仍然具有重要意义。在实际开发中,要根据项目的具体需求和规模,选择合适的数据库管理方式,以达到最佳的性能和用户体验。例如,对于简单的应用,直接使用 SQLiteOpenHelperSQLiteDatabase 可以更灵活地控制数据库操作;而对于大型项目,Room 框架提供的更多功能和抽象可以提高代码的可维护性和开发效率。在使用任何一种方式时,都要深入理解其原理和特性,确保数据库操作的正确性和高效性。

此外,在处理复杂的数据库关系和业务逻辑时,要合理设计数据库结构。例如,如果应用涉及到多表关联,要确保表之间的关系设计合理,避免出现数据冗余和不一致的情况。可以使用 ER 图(实体 - 关系图)等工具来辅助数据库设计,清晰地展示表之间的关系和数据流向。同时,在进行数据库操作时,要注意数据的合法性检查,避免插入非法数据导致数据库出现错误。比如在插入年龄时,要检查年龄是否在合理的范围内。

在应用发布后,也要关注数据库的性能和稳定性。可以通过日志记录数据库操作的相关信息,以便在出现问题时能够快速定位和解决。同时,根据用户的使用情况和数据分析,适时对数据库进行优化,比如调整索引、清理无用数据等,以保持应用的良好性能。总之,在 Android 开发中,结合 SQLiteOpenHelperSQLiteDatabase 进行数据库管理是一个关键环节,需要开发者深入理解和掌握相关知识,不断优化和改进,以打造出高质量的应用。

另外,随着移动设备存储容量的不断增加和应用功能的日益复杂,数据库的规模也可能逐渐增大。在这种情况下,要注意数据库的备份和恢复机制。可以定期将数据库文件备份到外部存储设备或者云存储,以防止数据丢失。在实现备份和恢复功能时,要考虑数据的安全性和兼容性,确保备份的数据能够在不同设备和版本的应用中正确恢复。同时,对于一些敏感数据,如用户的登录密码等,要进行加密存储,即使数据库文件被获取,也能保证数据的安全性。可以使用 Android 提供的加密 API 或者第三方加密库来实现数据加密功能。

在开发过程中,还可以考虑使用数据库迁移工具来辅助管理数据库版本。一些开源的数据库迁移工具可以帮助开发者更方便地记录和执行数据库结构的变更,减少手动编写 onUpgradeonDowngrade 方法的工作量,同时提高版本管理的准确性和可维护性。这些工具通常支持版本控制,能够记录每次数据库变更的详细信息,方便团队协作开发和问题排查。

最后,要关注 SQLite 数据库的更新和新特性。SQLite 团队会不断对 SQLite 进行改进和优化,添加新的功能和性能提升。及时了解这些信息,并将相关的新特性应用到项目中,可以进一步提升应用的性能和用户体验。例如,新的 SQLite 版本可能对事务处理有更好的优化,或者支持更多的数据类型,开发者可以根据项目需求合理采用这些新特性。总之,在 Android 开发中,对 SQLite 数据库的管理和优化是一个持续的过程,需要开发者不断学习和实践,以适应不断变化的需求和技术环境。