在移动端照片类应用中,相册管理是核心功能之一,需要实现相册创建、照片归类、相册信息修改、照片批量操作等一系列需求。SQLite作为嵌入式关系型数据库,不需要额外的数据库服务进程,整个数据库存储在一个文件中,非常适合照片应用这类本地数据存储场景,能够高效支撑相册管理相关的所有数据操作需求。

相册管理核心表结构设计
要实现稳定的相册管理功能,首先需要设计合理的数据库表结构,核心需要两张表分别存储相册基础信息和照片信息,同时通过外键关联实现两者的从属关系。相册表需要包含相册的唯一标识、相册名称、创建时间、相册封面照片ID、相册描述等字段,其中相册ID作为主键,保证每个相册的唯一性,创建时间字段用于后续按时间排序相册列表,封面照片ID关联照片表的主键,方便快速获取相册封面展示。
照片表需要存储照片的唯一标识、所属相册ID、照片本地存储路径、拍摄时间、照片宽度、照片高度、文件大小、添加时间等字段。所属相册ID作为外键关联相册表的主键,当相册被删除时可以通过外键约束同步处理关联的照片记录,避免出现孤立的照片数据。拍摄时间字段用于照片按时间排序,本地存储路径记录照片在设备中的实际存储位置,不需要把照片二进制数据直接存入数据库,避免数据库文件过大影响读写性能。
以下是两张核心表的创建SQL语句,我们在创建时为常用查询字段添加索引,提升后续查询效率:
-- 创建相册表
CREATE TABLE IF NOT EXISTS album (
album_id INTEGER PRIMARY KEY AUTOINCREMENT,
album_name TEXT NOT NULL,
create_time INTEGER NOT NULL,
cover_photo_id INTEGER,
album_desc TEXT,
UNIQUE(album_name)
);
-- 为相册表创建时间字段添加索引,优化按时间排序的查询
CREATE INDEX IF NOT EXISTS idx_album_create_time ON album(create_time DESC);
-- 创建照片表
CREATE TABLE IF NOT EXISTS photo (
photo_id INTEGER PRIMARY KEY AUTOINCREMENT,
album_id INTEGER NOT NULL,
photo_path TEXT NOT NULL,
shoot_time INTEGER,
width INTEGER,
height INTEGER,
file_size INTEGER,
add_time INTEGER NOT NULL,
FOREIGN KEY(album_id) REFERENCES album(album_id) ON DELETE CASCADE
);
-- 为照片表的所属相册ID和添加时间添加索引,优化按相册查询照片的排序操作
CREATE INDEX IF NOT EXISTS idx_photo_album_id ON photo(album_id);
CREATE INDEX IF NOT EXISTS idx_photo_add_time ON photo(add_time DESC);
相册基础功能实现逻辑
相册的基础功能包括创建新相册、查询相册列表、修改相册信息、删除相册四个核心操作。创建新相册时,需要校验相册名称是否重复,避免同一个名称创建多个相册,同时记录当前时间戳作为创建时间,初始状态下封面照片ID可以设置为NULL,等相册中添加第一张照片后再更新对应的值。插入数据时使用事务操作,保证插入过程的原子性,避免插入失败导致数据不一致。
查询相册列表时,通常会按照创建时间倒序排列,让最新创建的相册排在前面,同时如果需要展示每个相册的照片数量,可以通过LEFT JOIN关联照片表统计每个相册的照片总数,不需要额外在相册表中维护照片数量字段,避免后续添加删除照片时需要同步更新计数,减少数据不一致的风险。如果相册数量较多,还可以添加分页查询逻辑,避免一次性加载全部相册导致性能问题。
修改相册信息时,主要支持修改相册名称和相册描述,同时如果需要更换相册封面,可以更新cover_photo_id字段为对应照片的主键ID。删除相册时,由于我们在创建照片表时设置了外键的ON DELETE CASCADE约束,所以只需要删除相册表中的对应记录,数据库会自动删除所有关联的照片记录,不需要手动写批量删除照片的SQL语句,减少代码复杂度。以下是相册基础操作的示例代码:
import android.database.sqlite.SQLiteDatabase;
import java.util.ArrayList;
import java.util.List;
public class AlbumDao {
private SQLiteDatabase db;
public AlbumDao(SQLiteDatabase db) {
this.db = db;
}
// 创建新相册
public long createAlbum(String albumName, String albumDesc) {
db.beginTransaction();
try {
// 校验相册名称是否已存在
String checkSql = "SELECT COUNT(*) FROM album WHERE album_name = ?";
var cursor = db.rawQuery(checkSql, new String[]{albumName});
cursor.moveToFirst();
int count = cursor.getInt(0);
cursor.close();
if (count > 0) {
return -1; // 相册名称重复
}
// 插入新相册
String insertSql = "INSERT INTO album (album_name, create_time, album_desc) VALUES (?, ?, ?)";
long createTime = System.currentTimeMillis();
db.execSQL(insertSql, new String[]{albumName, String.valueOf(createTime), albumDesc});
db.setTransactionSuccessful();
// 获取新插入的相册ID
var idCursor = db.rawQuery("SELECT last_insert_rowid()", null);
idCursor.moveToFirst();
long albumId = idCursor.getLong(0);
idCursor.close();
return albumId;
} catch (Exception e) {
e.printStackTrace();
return -1;
} finally {
db.endTransaction();
}
}
// 查询所有相册,包含每个相册的照片数量
public List<Album> getAllAlbums() {
List<Album> albumList = new ArrayList<>();
String sql = "SELECT a.album_id, a.album_name, a.create_time, a.cover_photo_id, a.album_desc, " +
"COUNT(p.photo_id) AS photo_count " +
"FROM album a LEFT JOIN photo p ON a.album_id = p.album_id " +
"GROUP BY a.album_id ORDER BY a.create_time DESC";
var cursor = db.rawQuery(sql, null);
while (cursor.moveToNext()) {
Album album = new Album();
album.setAlbumId(cursor.getLong(cursor.getColumnIndexOrThrow("album_id")));
album.setAlbumName(cursor.getString(cursor.getColumnIndexOrThrow("album_name")));
album.setCreateTime(cursor.getLong(cursor.getColumnIndexOrThrow("create_time")));
album.setCoverPhotoId(cursor.getLong(cursor.getColumnIndexOrThrow("cover_photo_id")));
album.setAlbumDesc(cursor.getString(cursor.getColumnIndexOrThrow("album_desc")));
album.setPhotoCount(cursor.getInt(cursor.getColumnIndexOrThrow("photo_count")));
albumList.add(album);
}
cursor.close();
return albumList;
}
// 删除相册
public boolean deleteAlbum(long albumId) {
try {
String sql = "DELETE FROM album WHERE album_id = ?";
db.execSQL(sql, new String[]{String.valueOf(albumId)});
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
}
照片与相册的关联操作实现
照片与相册的关联操作包括往相册中添加照片、从相册中移除照片、批量移动照片到其他相册、查询相册内的所有照片等场景。往相册中添加照片时,需要记录照片的本地路径、拍摄时间、尺寸等信息,同时如果当前相册的封面照片ID为NULL,自动将新添加的第一张照片设置为相册封面,提升用户体验。添加照片时同样使用事务操作,保证照片插入和相册封面更新的原子性。
从相册中移除照片时,只需要删除照片表中对应相册ID和照片ID的记录即可,如果删除的是当前相册的封面照片,需要重新从相册剩余的照片中选择一张作为新的封面,或者将封面ID设置为NULL。批量移动照片到其他相册时,只需要更新这些照片记录的album_id字段为新的相册ID,同时更新原相册和新相册的封面信息,避免封面照片指向不存在的相册。查询相册内的照片时,通常按照添加时间倒序排列,让最新添加的照片排在前面,同时支持按拍摄时间、照片尺寸等字段排序。
以下是照片与相册关联操作的示例代码,包含添加照片和批量移动照片的逻辑:
import android.database.sqlite.SQLiteDatabase;
import java.util.List;
public class PhotoDao {
private SQLiteDatabase db;
private AlbumDao albumDao;
public PhotoDao(SQLiteDatabase db) {
this.db = db;
this.albumDao = new AlbumDao(db);
}
// 往相册中添加单张照片
public long addPhotoToAlbum(long albumId, String photoPath, long shootTime, int width, int height, long fileSize) {
db.beginTransaction();
try {
long addTime = System.currentTimeMillis();
String insertSql = "INSERT INTO photo (album_id, photo_path, shoot_time, width, height, file_size, add_time) " +
"VALUES (?, ?, ?, ?, ?, ?, ?)";
db.execSQL(insertSql, new String[]{
String.valueOf(albumId),
photoPath,
String.valueOf(shootTime),
String.valueOf(width),
String.valueOf(height),
String.valueOf(fileSize),
String.valueOf(addTime)
});
// 获取新插入的照片ID
var cursor = db.rawQuery("SELECT last_insert_rowid()", null);
cursor.moveToFirst();
long photoId = cursor.getLong(0);
cursor.close();
// 检查当前相册是否有封面,没有则设置新照片为封面
String checkCoverSql = "SELECT cover_photo_id FROM album WHERE album_id = ?";
var coverCursor = db.rawQuery(checkCoverSql, new String[]{String.valueOf(albumId)});
coverCursor.moveToFirst();
long coverId = coverCursor.getLong(coverCursor.getColumnIndexOrThrow("cover_photo_id"));
coverCursor.close();
if (coverId == 0) {
String updateCoverSql = "UPDATE album SET cover_photo_id = ? WHERE album_id = ?";
db.execSQL(updateCoverSql, new String[]{String.valueOf(photoId), String.valueOf(albumId)});
}
db.setTransactionSuccessful();
return photoId;
} catch (Exception e) {
e.printStackTrace();
return -1;
} finally {
db.endTransaction();
}
}
// 批量移动照片到其他相册
public boolean movePhotosToAlbum(List<Long> photoIds, long targetAlbumId) {
if (photoIds == null || photoIds.isEmpty()) {
return false;
}
db.beginTransaction();
try {
// 拼接照片ID参数
StringBuilder idStr = new StringBuilder();
for (int i = 0; i < photoIds.size(); i++) {
idStr.append(photoIds.get(i));
if (i != photoIds.size() - 1) {
idStr.append(",");
}
}
// 更新照片的所属相册ID
String updateSql = "UPDATE photo SET album_id = ? WHERE photo_id IN (" + idStr.toString() + ")";
db.execSQL(updateSql, new String[]{String.valueOf(targetAlbumId)});
// 更新原相册的封面,如果原封面在被移动的照片中,重置原相册封面
String resetCoverSql = "UPDATE album SET cover_photo_id = NULL WHERE cover_photo_id IN (" + idStr.toString() + ")";
db.execSQL(resetCoverSql);
// 如果目标相册没有封面,设置移动的第一张照片为封面
String checkTargetCoverSql = "SELECT cover_photo_id FROM album WHERE album_id = ?";
var cursor = db.rawQuery(checkTargetCoverSql, new String[]{String.valueOf(targetAlbumId)});
cursor.moveToFirst();
long targetCoverId = cursor.getLong(cursor.getColumnIndexOrThrow("cover_photo_id"));
cursor.close();
if (targetCoverId == 0) {
String updateTargetCoverSql = "UPDATE album SET cover_photo_id = ? WHERE album_id = ?";
db.execSQL(updateTargetCoverSql, new String[]{String.valueOf(photoIds.get(0)), String.valueOf(targetAlbumId)});
}
db.setTransactionSuccessful();
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
} finally {
db.endTransaction();
}
}
// 查询指定相册内的所有照片,按添加时间倒序
public List<Photo> getPhotosByAlbumId(long albumId) {
List<Photo> photoList = new ArrayList<>();
String sql = "SELECT * FROM photo WHERE album_id = ? ORDER BY add_time DESC";
var cursor = db.rawQuery(sql, new String[]{String.valueOf(albumId)});
while (cursor.moveToNext()) {
Photo photo = new Photo();
photo.setPhotoId(cursor.getLong(cursor.getColumnIndexOrThrow("photo_id")));
photo.setAlbumId(cursor.getLong(cursor.getColumnIndexOrThrow("album_id")));
photo.setPhotoPath(cursor.getString(cursor.getColumnIndexOrThrow("photo_path")));
photo.setShootTime(cursor.getLong(cursor.getColumnIndexOrThrow("shoot_time")));
photo.setWidth(cursor.getInt(cursor.getColumnIndexOrThrow("width")));
photo.setHeight(cursor.getInt(cursor.getColumnIndexOrThrow("height")));
photo.setFileSize(cursor.getLong(cursor.getColumnIndexOrThrow("file_size")));
photo.setAddTime(cursor.getLong(cursor.getColumnIndexOrThrow("add_time")));
photoList.add(photo);
}
cursor.close();
return photoList;
}
}
性能优化与注意事项
在使用SQLite实现相册管理功能时,需要注意性能优化,避免相册和照片数量增多后出现查询卡顿的问题。首先所有的查询条件字段尽量添加索引,比如相册表的创建时间字段、照片表的所属相册ID和添加时间字段,我们已经在表创建时添加了对应的索引,能够有效提升查询效率。其次避免频繁的短事务操作,比如批量添加100张照片时,不要每张照片开启一个事务,而是把所有插入操作放在一个事务中,减少事务开启和提交的开销。
另外不要把照片的二进制数据直接存入数据库,只需要存储照片的本地路径即可,照片文件存储在设备的文件系统中,这样可以避免数据库文件过大,影响读写性能。如果照片的拍摄时间、尺寸等信息不需要频繁修改,可以在插入时一次性写入,避免后续更新操作。同时定期清理无关联的相册和照片数据,比如用户删除照片后,对应的本地文件也需要同步删除,避免占用设备存储空间。
还要注意SQLite的线程安全问题,如果应用在多线程场景下操作数据库,建议使用单例模式管理SQLiteDatabase实例,或者使用SQLiteOpenHelper的getWritableDatabase方法获取实例,避免多个线程同时操作数据库导致异常。如果需要支持大容量的相册管理,比如单个相册包含上万张照片,查询时建议添加分页逻辑,每次只加载20-50张照片,用户滑动到底部时再加载更多,避免一次性加载大量数据导致内存占用过高和界面卡顿。