-
Schema
- 列出reaction的枚举值
- 明确视频、用户、videoReactions之间的逻辑关系,用户和视频都可以对应多个reactions,但一个reaction只能由一个用户产生且只作用于一个视频
- 生成reaction变更时的数据校验格式
export const reactionType = pgEnum('reaction_type', [
'like',
'dislike'
])
export const videoReactions = pgTable("video_reactions", {
userId: uuid("user_id").references(() => users.id, {
onDelete: "cascade",
}).notNull(),
videoId: uuid("video_id").references(() => videos.id, {
onDelete: "cascade",
}).notNull(),
type: reactionType('type').notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("update_at").defaultNow().notNull(),
}, t => [ primaryKey({ name: "video_reactions_p_key", columns: [t.userId, t.videoId],
})
])
export const videoReactionsRelations = relations(videoReactions, ({ one }) => ({
users: one(users, {
fields: [videoReactions.userId],
references: [users.id],
}),
videos: one(videos, {
fields: [videoReactions.videoId],
references: [videos.id],
}),
}))
export const videoReactionsInsertSchema = createInsertSchema(videoReactions)
export const videoReactionsSelectSchema = createSelectSchema(videoReactions)
export const videoReactionsUpdateSchema = createUpdateSchema(videoReactions)
-
在获取视频详细信息的接口中添加视频reactions相关的内容
- 视频reactions相关的内容包含:当前视频的like数量、当前视频的dislike数量
.innerJoin(users, eq(videos.userId, users.id))
- 在这个接口中,我们不仅要返回视频信息,还要返回视频发布者的用户信息,比如用户头像等,因此需要关联users表
innreJoin() SQL中的连接操作,只保留两张表都能匹配上的数据
// src/modules/videos/server/procedure.ts
export const videosRouter = createTRPCRouter({
getOne: baseProcedure
.input(z.object({ videoId: z.uuid() }))
.query(async ({ input, ctx }) => {
const [video] = await db
.select({
...
// 计算关联的videoReactions中type为'like'的数量
videoLikesCount: db.$count(videoReactions, and(
eq(videoReactions.videoId, videos.id),
eq(videoReactions.type, 'like')
)),
// 计算关联的videoReactions中type为'dislike'的数量
videoDislikesCount: db.$count(videoReactions, and(
eq(videoReactions.videoId, videos.id),
eq(videoReactions.type, 'dislike')
)),
})
.from(videos)
.innerJoin(users, eq(videos.userId, users.id)) // 关联用户表,获取视频发布者的信息
.where(eq(videos.id, input.videoId))
})
})
-
在获取视频详细信息的接口中添加视频浏览者reaction相关的内容
- 判断浏览者是否登录:
getOne这个接口我们使用的是baseProcedure,是不要求登录的;但如果要在页面中显示当前浏览者是否对视频有reaction的操作,只有登录状态下才能获取到
const { clerkUserId } = ctx
- 在users表中找到当前浏览者
viewer: 这一步需要注意,可能存在用户未登录的情况,为了避免SQL报错,我们使用inArray()查询
inArray(column, array):相当于SQL的IN运算符,在我们这里的意思是选出users.clerkId等于clerkUserId的用户,如果clerkUserId为undefined则不匹配任何用户
let viewerId;
const [viewer] = await db
.select()
.from(users)
.where(inArray(users.clerkId, clerkUserId ? [clerkUserId] : []))
if (viewer) viewerId = viewer.id
- 获取当前浏览者对所有视频的reactions的数据并作为一张临时表
db.$with() Drizzle ORM里用来构建CTE(Common Table Expression,公用表表达式)的方法,我们这里CTE的名字是viewer_reactions,这个CTE只在这次查询中有效,不会真的建表,但后续的主查询中我们又可以像用表一样使用它
const viewerReactions = db.$with('viewer_reactions').as(
db
.select({
videoId: videoReactions.videoId,
type: videoReactions.type
})
.from(videoReactions)
.where(inArray(videoReactions.userId, viewerId ? [viewerId] : []))
)
- 使用数据库查询获取数据并返回
leftJoin() SQL中的连接操作,关联左边表(我们这里是videos表),保留videos表的所有行,根据eq()进行条件判断,如果viewerReactions表中有匹配上的就带上这部分数据,没匹配上的则用null填充
groupBy() 按条件进行数据分堆,我们这里的意思是“每个视频-视频作者-当前浏览者对该视频的reaction”
const [video] = await db
.with(viewerReactions) // 使用上面定义的子查询
.select({
...
viewerReaction: viewerReactions.type // 当前浏览视频的用户对该视频的reaction类型
})
.from(videos)
.innerJoin(users, eq(videos.userId, users.id)) // 关联用户表,获取视频发布者的信息
.leftJoin(viewerReactions, eq(videos.id, viewerReactions.videoId)) // 关联当前浏览视频的用户对视频的reactions
.where(eq(videos.id, input.videoId))
.groupBy(
videos.id,
users.id,
viewerReactions.type
)
-
VideoReactionsRouter
- 以like操作为例,基本逻辑是先检查用户对当前视频是否有过like操作,如果有则删除,如果没有like操作,则创建一条新的reaction,但需要注意的是用户虽然没有like操作,不代表没有dislike操作
onConflictDoUpdate({target: [...], set: {...}}) insert时避免唯一键冲突,根据target判断是否发生冲突,如果有冲突则更新set中要求更新的字段
// src/modules/video-reactions/server/procedure.ts
export const VideoReactionsRouter = createTRPCRouter({
like: protectedProcedure
.input(z.object({ videoId: z.uuid() }))
.mutation(async ({ ctx, input }) => {
const { videoId } = input
const { id: userId } = ctx.user
// 检查用户是否已经对该视频有like reaction
const [existingReaction] = await db
.select()
.from(videoReactions)
.where(and(
eq(videoReactions.userId, userId),
eq(videoReactions.videoId, videoId),
eq(videoReactions.type, 'like')
))
// 如果已经有like reaction,则删除它(即取消点赞)
if (existingReaction) {
const [deletedReaction] = await db
.delete(videoReactions)
.where(and(
eq(videoReactions.userId, userId),
eq(videoReactions.videoId, videoId),
eq(videoReactions.type, 'like')
))
.returning()
return deletedReaction
}
// 根据 userId + videoId 检查冲突,用户对该视频虽然没有like reaction,但可能有dislike reaction
// 如果存在dislike reaction,则更新为like,否则插入新的like reaction
const [createdReaction] = await db
.insert(videoReactions)
.values({ userId, videoId, type: 'like' })
.onConflictDoUpdate({
target: [videoReactions.userId, videoReactions.videoId], // 检查冲突的字段
set: { type: 'like' } // 如果有冲突需要更新的字段
})
.returning()
return createdReaction
}),
dislike: protectedProcedure...
})