11. 数据存储(下):SQLite数据库基础、SQLiteOpenHelper使用、Room持久化库入门、ContentProvider数据共享、文件存储最佳实践
好,咱们接着聊数据存储的下半部分。上一章我们把SharedPreferences和文件存储的基本操作讲完了,这一章要上点硬菜了——数据库。说实话,数据库这块是很多初学者的分水岭,跨过去,你的App就能处理复杂数据了。
本章核心脉络:从原生SQLite → SQLiteOpenHelper → Room → ContentProvider → 文件存储最佳实践,这是一个从底层到上层、从手动到自动的演进过程。
11.1 SQLite 数据库基础
SQLite 是什么?说白了,它是一个轻量级的嵌入式数据库引擎。Android 系统内置了它,不需要额外安装任何东西。我刚开始学的时候觉得这玩意儿挺神秘的,后来发现它就是个文件——一个 .db 文件,存在你的 App 私有目录下。
SQLite 支持标准的 SQL 语法,但有一些限制。比如它不支持 RIGHT JOIN 和 FULL OUTER JOIN,但常用的 CRUD 操作完全没问题。
💡 小提示:SQLite 的数据类型比较宽松,你可以把字符串存到 INTEGER 字段里,它不会报错。但千万别这么干——我见过有人因为这个导致排序结果完全乱掉。
创建一个数据库的基本步骤:
- 调用
openOrCreateDatabase()方法 - 执行 SQL 语句创建表
- 进行增删改查操作
- 操作完成后关闭数据库
// 直接操作 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 需要三步:
- 定义 Entity —— 对应数据库中的表
- 定义 DAO —— 数据访问对象,写 SQL 的地方
- 定义 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 需要:
- 继承
ContentProvider类 - 实现
onCreate()、query()、insert()、update()、delete()、getType()方法 - 在 AndroidManifest.xml 中注册
- 定义 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 到文件存储,每种方案都有它的用武之地。多动手写写,慢慢就熟练了。