如何在SQL中建模并更新带顺序的多对多关联?以歌单歌曲场景为例
歌单与歌曲关联关系建模方案(适配PostgreSQL + Supabase场景)
方案1:多对多关联表(生产环境首选,数据一致性高)
表结构设计
-- 歌单表 CREATE TABLE playlists ( id INT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, name VARCHAR(255) NOT NULL, created_at TIMESTAMPTZ DEFAULT NOW() ); -- 歌曲表 CREATE TABLE songs ( id INT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, name VARCHAR(255) NOT NULL, artist VARCHAR(255) NOT NULL, album VARCHAR(255), duration INT NOT NULL, created_at TIMESTAMPTZ DEFAULT NOW() ); -- 歌单歌曲关联表,用position字段存储顺序,避免和SQL关键字index重名 CREATE TABLE playlist_songs ( id INT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, playlist_id INT NOT NULL REFERENCES playlists(id) ON DELETE CASCADE, song_id INT NOT NULL REFERENCES songs(id) ON DELETE CASCADE, position INT NOT NULL, -- 保证同一个歌单下不会出现重复的排序位 UNIQUE(playlist_id, position) ); -- 加索引加速查询 CREATE INDEX idx_playlist_songs_playlist ON playlist_songs(playlist_id);
顺序更新SQL函数
直接在Supabase的SQL编辑器中执行以下代码创建函数,即可通过SDK直接调用完成顺序更新,不需要逐行修改:
CREATE OR REPLACE FUNCTION update_playlist_order(p_playlist_id INT, p_song_ids INT[]) RETURNS VOID AS $$ BEGIN -- 先删除该歌单下原有的关联记录 DELETE FROM playlist_songs WHERE playlist_id = p_playlist_id; -- 批量插入新的关联记录,自动生成排序位 INSERT INTO playlist_songs (playlist_id, song_id, position) SELECT p_playlist_id, song_id, idx FROM UNNEST(p_song_ids) WITH ORDINALITY AS t(song_id, idx); END; $$ LANGUAGE plpgsql SECURITY DEFINER;
Supabase JS SDK使用示例
// 更新歌单顺序,传入歌单ID和新的歌曲ID数组 const { error } = await supabase.rpc('update_playlist_order', { p_playlist_id: 1, p_song_ids: [24, 50, 21] }) // 查询歌单下的所有歌曲,按顺序返回 const { data: playlistSongs } = await supabase .from('playlist_songs') .select('songs(*)') .eq('playlist_id', 1) .order('position', { ascending: true })
方案2:歌单表直接存储歌曲ID数组(开发效率优先,适合小项目)
虽然PostgreSQL没有原生支持外键数组,但可以通过触发器实现数据有效性校验,避免出现无效的歌曲ID。
表结构设计
CREATE TABLE playlists ( id INT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, name VARCHAR(255) NOT NULL, song_ids INT[] NOT NULL DEFAULT '{}', -- 存储有序的歌曲ID数组 created_at TIMESTAMPTZ DEFAULT NOW() ); -- 可选:添加触发器校验song_ids中的ID都存在于songs表中 CREATE OR REPLACE FUNCTION check_song_ids_valid() RETURNS TRIGGER AS $$ BEGIN IF EXISTS ( SELECT 1 FROM unnest(NEW.song_ids) AS sid LEFT JOIN songs ON songs.id = sid WHERE songs.id IS NULL ) THEN RAISE EXCEPTION '歌单中包含不存在的歌曲ID'; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER trigger_playlists_check_song_ids BEFORE INSERT OR UPDATE OF song_ids ON playlists FOR EACH ROW EXECUTE FUNCTION check_song_ids_valid();
操作示例
// 更新歌单顺序,直接传新的数组即可 const { error } = await supabase .from('playlists') .update({ song_ids: [24, 50, 21] }) .eq('id', 1) // 查询歌单下的所有歌曲,保留数组顺序 const { data: playlist } = await supabase .from('playlists') .select(` id, name, songs: songs(id, name, artist, album) `) .eq('id', 1) // 如需在数据库层面直接关联返回有序结果,可以用视图或RPC函数处理
方案选型建议
- 关联表方案适合用户量较大、歌单歌曲数量多、频繁对单首歌曲进行增删操作的场景,数据一致性高,查询性能更稳定。
- 数组方案适合个人项目、小体量应用,开发速度快,顺序更新逻辑简单,维护成本低。
内容的提问来源于stack exchange,提问作者Jacob Haugen
相关产品推荐
相关产品推荐

