11. 数据存储(下):SQLite数据库基础、SQLiteOpenHelper使用、Room持久化库入门、ContentProvider数据共享、文件存储最佳实践

好,咱们接着聊数据存储的下半部分。上一章我们把SharedPreferences和文件存储的基本操作讲完了,这一章要上点硬菜了——数据库。说实话,数据库这块是很多初学者的分水岭,跨过去,你的App就能处理复杂数据了。

本章核心脉络:从原生SQLite → SQLiteOpenHelper → Room → ContentProvider → 文件存储最佳实践,这是一个从底层到上层、从手动到自动的演进过程。

📱 Android 数据存储体系(本章重点) 数据持久化 SQLite 原生数据库 SQLiteOpenHelper Room 持久化库 ContentProvider 文件存储最佳实践 从手动管理 → 自动ORM → 跨进程共享

11.1 SQLite 数据库基础

SQLite 是什么?说白了,它是一个轻量级的嵌入式数据库引擎。Android 系统内置了它,不需要额外安装任何东西。我刚开始学的时候觉得这玩意儿挺神秘的,后来发现它就是个文件——一个 .db 文件,存在你的 App 私有目录下。

SQLite 支持标准的 SQL 语法,但有一些限制。比如它不支持 RIGHT JOIN 和 FULL OUTER JOIN,但常用的 CRUD 操作完全没问题。

💡 小提示:SQLite 的数据类型比较宽松,你可以把字符串存到 INTEGER 字段里,它不会报错。但千万别这么干——我见过有人因为这个导致排序结果完全乱掉。

创建一个数据库的基本步骤:

  1. 调用 openOrCreateDatabase() 方法
  2. 执行 SQL 语句创建表
  3. 进行增删改查操作
  4. 操作完成后关闭数据库
// 直接操作 SQLiteDatabase 的方式(不推荐,但了解原理)
SQLiteDatabase db = openOrCreateDatabase(
    "myapp.db", 
    MODE_PRIVATE, 
    null
);

// 创建表
db.execSQL("CREATE TABLE IF NOT EXISTS users (" +
    "id INTEGER PRIMARY KEY AUTOINCREMENT, " +
    "name TEXT NOT NULL, " +
    "age INTEGER, " +
    "created_at DATETIME DEFAULT CURRENT_TIMESTAMP" +
    ")");

// 插入数据
ContentValues values = new ContentValues();
values.put("name", "张三");
values.put("age", 25);
long newId = db.insert("users", null, values);

// 查询
Cursor cursor = db.rawQuery("SELECT * FROM users WHERE age > ?", new String[]{"20"});
while (cursor.moveToNext()) {
    String name = cursor.getString(cursor.getColumnIndex("name"));
    int age = cursor.getInt(cursor.getColumnIndex("age"));
}
cursor.close();
db.close();

⚠️ 注意:直接使用 execSQL() 执行拼接的 SQL 语句存在 SQL 注入风险。永远使用参数化查询(问号占位符)。我曾经在项目里接手过一段代码,直接用字符串拼接用户输入,差点没把我送走。

11.2 SQLiteOpenHelper 使用

直接操作 SQLiteDatabase 太原始了,对吧?每次都要手动创建数据库、管理版本、关闭连接。Android 提供了 SQLiteOpenHelper 这个辅助类,帮我们处理这些脏活累活。

我个人习惯把 SQLiteOpenHelper 写成单例,避免多次创建实例导致资源浪费。

public class MyDatabaseHelper extends SQLiteOpenHelper {
    
    private static final String DATABASE_NAME = "myapp.db";
    private static final int DATABASE_VERSION = 2;
    
    private static MyDatabaseHelper instance;
    
    public static synchronized MyDatabaseHelper getInstance(Context context) {
        if (instance == null) {
            instance = new MyDatabaseHelper(context.getApplicationContext());
        }
        return instance;
    }
    
    private MyDatabaseHelper(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }
    
    @Override
    public void onCreate(SQLiteDatabase db) {
        String createTable = "CREATE TABLE users (" +
            "id INTEGER PRIMARY KEY AUTOINCREMENT, " +
            "name TEXT, " +
            "age INTEGER, " +
            "email TEXT UNIQUE)";
        db.execSQL(createTable);
    }
    
    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        // 版本升级逻辑
        if (oldVersion < 2) {
            db.execSQL("ALTER TABLE users ADD COLUMN phone TEXT");
        }
    }
}

这里有个关键点——onUpgrade() 方法。很多新手直接在这里写 DROP TABLE IF EXISTS 然后重建,这会导致用户数据全部丢失。正确的做法是做增量升级,就像上面那样,只添加新字段。

核心方法说明:

方法调用时机作用
onCreate()数据库第一次创建时建表、初始化数据
onUpgrade()数据库版本号升高时做表结构变更
onDowngrade()数据库版本号降低时默认抛异常,需重写
getReadableDatabase()任何时候获取可读数据库实例
getWritableDatabase()任何时候获取可写数据库实例

11.3 Room 持久化库入门

好了,SQLiteOpenHelper 虽然比原生好一些,但还是要写很多模板代码。Google 官方推出了 Room,它是 Jetpack 家族的一员,专门用来简化数据库操作。

Room 的核心思想是 ORM(对象关系映射)。你想想看,我们操作 Java 对象多方便,为什么还要写 SQL 语句?Room 让你用注解定义实体类,然后自动生成实现代码。

使用 Room 需要三步:

  1. 定义 Entity —— 对应数据库中的表
  2. 定义 DAO —— 数据访问对象,写 SQL 的地方
  3. 定义 Database —— 数据库持有者
// 1. Entity
@Entity(tableName = "users")
public class User {
    @PrimaryKey(autoGenerate = true)
    public int id;
    
    @ColumnInfo(name = "full_name")
    public String name;
    
    public int age;
    
    @Ignore  // 这个字段不存数据库
    public String tempField;
}

// 2. DAO
@Dao
public interface UserDao {
    @Query("SELECT * FROM users")
    List<User> getAll();
    
    @Query("SELECT * FROM users WHERE id = :userId")
    User getById(int userId);
    
    @Insert
    void insert(User user);
    
    @Update
    void update(User user);
    
    @Delete
    void delete(User user);
}

// 3. Database
@Database(entities = {User.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {
    public abstract UserDao userDao();
    
    private static AppDatabase instance;
    
    public static synchronized AppDatabase getInstance(Context context) {
        if (instance == null) {
            instance = Room.databaseBuilder(
                context.getApplicationContext(),
                AppDatabase.class,
                "myapp_room.db"
            ).build();
        }
        return instance;
    }
}

💡 使用建议:Room 默认不允许在主线程操作数据库,会抛异常。这是好事,防止你写出卡 UI 的代码。可以用 .allowMainThreadQueries() 绕过,但只建议在测试代码中用。

我在项目中遇到过一个问题:Room 的数据库升级比 SQLiteOpenHelper 复杂一些。如果只是加字段,可以用 Migration 类;如果改动大,建议用 fallbackToDestructiveMigration() 配合测试。

// 数据库迁移示例
Migration MIGRATION_1_2 = new Migration(1, 2) {
    @Override
    public void migrate(@NonNull SupportSQLiteDatabase database) {
        database.execSQL("ALTER TABLE users ADD COLUMN phone TEXT");
    }
};

AppDatabase db = Room.databaseBuilder(context, AppDatabase.class, "myapp.db")
    .addMigrations(MIGRATION_1_2)
    .build();

11.4 ContentProvider 数据共享

ContentProvider 是 Android 四大组件之一,它的作用是让不同 App 之间共享数据。说白了,你的 App 里有一堆数据,别的 App 想访问,怎么办?ContentProvider 就是那个安全通道。

我刚开始学的时候觉得 ContentProvider 很鸡肋——自己的 App 内部用 Room 不香吗?后来做联系人同步功能时才明白,没有 ContentProvider,第三方 App 根本拿不到你的数据。

实现一个 ContentProvider 需要:

  1. 继承 ContentProvider
  2. 实现 onCreate()query()insert()update()delete()getType() 方法
  3. 在 AndroidManifest.xml 中注册
  4. 定义 URI 匹配规则
public class MyContentProvider extends ContentProvider {
    
    private static final String AUTHORITY = "com.example.myapp.provider";
    private static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
    
    private static final int USERS = 1;
    private static final int USER_ID = 2;
    
    static {
        URI_MATCHER.addURI(AUTHORITY, "users", USERS);
        URI_MATCHER.addURI(AUTHORITY, "users/#", USER_ID);
    }
    
    private MyDatabaseHelper dbHelper;
    
    @Override
    public boolean onCreate() {
        dbHelper = MyDatabaseHelper.getInstance(getContext());
        return true;
    }
    
    @Nullable
    @Override
    public Cursor query(@NonNull Uri uri, @Nullable String[] projection,
                        @Nullable String selection, @Nullable String[] selectionArgs,
                        @Nullable String sortOrder) {
        SQLiteDatabase db = dbHelper.getReadableDatabase();
        Cursor cursor;
        
        switch (URI_MATCHER.match(uri)) {
            case USERS:
                cursor = db.query("users", projection, selection, 
                    selectionArgs, null, null, sortOrder);
                break;
            case USER_ID:
                String id = uri.getLastPathSegment();
                cursor = db.query("users", projection, "id=?",
                    new String[]{id}, null, null, sortOrder);
                break;
            default:
                throw new IllegalArgumentException("Unknown URI: " + uri);
        }
        
        cursor.setNotificationUri(getContext().getContentResolver(), uri);
        return cursor;
    }
    
    // insert, update, delete 方法类似,省略...
}

⚠️ 安全提醒:ContentProvider 默认是导出的(exported=true),任何 App 都能访问。如果你的数据敏感,记得设置 android:exported="false" 或者加上权限控制。我曾经见过一个记账 App 把用户账单数据通过 ContentProvider 暴露出去,谁都能查,太危险了。

11.5 文件存储最佳实践

最后聊聊文件存储。虽然数据库能解决大部分结构化数据的问题,但有些场景还是得用文件——比如存图片、音频、日志文件等。

Android 的文件存储分为内部存储和外部存储,我整理了一个对比表:

存储类型路径特点适用场景
内部存储/data/data/包名/私有,其他 App 无法访问敏感数据、数据库文件
外部存储(私有)/storage/emulated/0/Android/data/包名/App 卸载时自动删除大文件、缓存文件
外部存储(公共)/storage/emulated/0/DCIM/ 等用户可见,需权限用户生成的内容(照片、下载文件)

最佳实践我总结了几条:

  • 能用内部存储就别用外部存储 —— 省去权限申请的麻烦
  • 缓存文件用 getCacheDir() —— 系统空间不足时会自动清理
  • 大文件用 getExternalFilesDir() —— 避免占用系统空间
  • 不要硬编码路径 —— 不同厂商的路径可能不一样
// 内部存储写入
String fileName = "notes.txt";
String content = "这是笔记内容";
FileOutputStream fos = openFileOutput(fileName, Context.MODE_PRIVATE);
fos.write(content.getBytes());
fos.close();

// 内部存储读取
FileInputStream fis = openFileInput(fileName);
BufferedReader reader = new BufferedReader(new InputStreamReader(fis));
String line = reader.readLine();
reader.close();

// 缓存目录使用
File cacheFile = new File(getCacheDir(), "temp_data.json");
// 写入缓存...

// 外部私有目录
File externalFile = new File(getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS), "report.pdf");

💡 我的习惯:对于需要持久保存的用户数据,我一般用 Room 数据库。对于图片、文件等二进制数据,存到外部私有目录,数据库里只存文件路径。这样既保证了数据安全,又方便管理。

嗯,这一章的内容就到这里。数据存储是 App 的根基,选对方案能让你的代码少踩很多坑。从 SQLite 到 Room,从 ContentProvider 到文件存储,每种方案都有它的用武之地。多动手写写,慢慢就熟练了。


公众号:蓝海资料掘金营,微信deep3321