Supabase Flutter多UUID查询优化:粉丝数据获取效率提升问询
Supabase + Flutter 粉丝数据查询优化方案
一、表结构重构(核心优化)
你当前用数组存储粉丝/关注UUID的方式不符合SQL设计范式,不仅无法创建有效外键约束,还会导致查询效率低下、分页困难。推荐重构为关联表结构:
1. 废弃原followers和following表,创建统一的user_follows表
CREATE TABLE user_follows ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), user_id UUID REFERENCES users(uuid) ON DELETE CASCADE, -- 被关注者ID follower_id UUID REFERENCES users(uuid) ON DELETE CASCADE, -- 粉丝ID created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() );
- 通过
user_id可查询某用户的所有粉丝,通过follower_id可查询某用户关注的所有人 - 天然支持外键约束,避免无效UUID数据
- 后续分页、排序操作更灵活
二、查询逻辑优化
1. 重构后的查询(一次请求获取所有粉丝元数据)
无需先拿UUID再循环查询,直接用JOIN关联users表一次性获取数据:
/// 存储粉丝Profile数据 List<ProfileModel> followersProfiles = []; /// 一次性获取当前用户的所有粉丝元数据 Future<void> fetchFollowersProfiles() async { followersProfiles.clear(); try { final data = await supabase .from('user_follows') .select('follower_id, users!inner(*)') // 关联查询粉丝的用户信息 .eq('user_id', _currentUuid) .order('created_at', ascending: false); // 按关注时间倒序 if (data != null && data.isNotEmpty) { followersProfiles = data.map((item) => ProfileModel.fromMap(item['users'])).toList(); } } catch (e) { if (kDebugMode) { print(e); } } update(); }
2. 过渡方案(暂时不改表结构)
用IN操作符一次性查询所有UUID对应的用户数据,避免循环请求:
Future<void> fetchFollowersProfiles() async { followersProfiles.clear(); try { // 先获取粉丝UUID数组 final followerData = await supabase .from('followers') .select('followers_list') .eq('user_id', _currentUuid) .single(); if (followerData != null && followerData['followers_list'].isNotEmpty) { final uuids = followerData['followers_list']; // 一次性查询所有UUID对应的用户 final userData = await supabase .from('users') .select('*') .in_('uuid', uuids); followersProfiles = userData.map((item) => ProfileModel.fromMap(item)).toList(); } } catch (e) { if (kDebugMode) { print(e); } } update(); }
三、高效分页实现
1. Offset-Limit分页(适合小数据量)
int _currentPage = 0; final int _pageSize = 20; Future<void> fetchFollowersPage() async { try { final data = await supabase .from('user_follows') .select('follower_id, users!inner(*)') .eq('user_id', _currentUuid) .order('created_at', ascending: false) .range(_currentPage * _pageSize, (_currentPage + 1) * _pageSize - 1); if (data.isNotEmpty) { followersProfiles.addAll(data.map((item) => ProfileModel.fromMap(item['users'])).toList()); _currentPage++; } } catch (e) { if (kDebugMode) { print(e); } } update(); }
在ListView中监听滚动到底部时调用fetchFollowersPage()加载下一页。
2. Keyset分页(适合大数据量,性能更优)
利用created_at或uuid作为游标,避免Offset分页的性能损耗:
DateTime? _lastFollowerCreatedAt; final int _pageSize = 20; Future<void> fetchFollowersPage() async { try { var query = supabase .from('user_follows') .select('follower_id, users!inner(*), created_at') .eq('user_id', _currentUuid) .order('created_at', ascending: false) .limit(_pageSize); // 非第一页时添加游标条件 if (_lastFollowerCreatedAt != null) { query = query.lt('created_at', _lastFollowerCreatedAt); } final data = await query; if (data.isNotEmpty) { followersProfiles.addAll(data.map((item) => ProfileModel.fromMap(item['users'])).toList()); // 更新游标为最后一条数据的创建时间 _lastFollowerCreatedAt = DateTime.parse(data.last['created_at']); } } catch (e) { if (kDebugMode) { print(e); } } update(); }
四、Widget代码适配
原Widget无需大幅修改,确保传入的usersList为优化后的followersProfiles即可:
class FollowersAndFollowingTab extends StatelessWidget { final List<ProfileModel> usersList; const FollowersAndFollowingTab({super.key, required this.usersList}); @override Widget build(BuildContext context) { return GetBuilder<DBController>( builder: (_) => ListView.builder( shrinkWrap: true, itemCount: usersList.length, itemBuilder: (context, index) { final userProfile = usersList[index]; return Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: Card( child: ListTile( onTap: () => Navigator.push( context, MaterialPageRoute( fullscreenDialog: true, builder: (context) => ProfileViewScreen(profileModel: userProfile), ), ), leading: Container( width: 50, decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), image: DecorationImage( fit: BoxFit.cover, image: CachedNetworkImageProvider(userProfile.profilePhoto!), ), ), ), title: Text( '${_bioWidgets.fullNameNoFlag(profileModel: userProfile)} ${_bioWidgets.returnFlagEmoji(profileModel: userProfile)}', ), subtitle: Text('@${userProfile.username!}'), trailing: IconButton( onPressed: () {}, icon: const Icon(Icons.more_vert, color: Colors.grey), ), ), ), ); }, ), ); } }
内容的提问来源于stack exchange,提问作者Raj A
相关产品推荐
相关产品推荐

