Tube - Studio Videos

  • Database - Foreign keys 外键
    • 外键就是在一张表里,存放另一张表主键的字段,用来建立表与表之间的约束和联系
    • 外键是数据库级别的约束,当前例子中,如果用户注销,也就是users.id被删除,那么该用户下的发布的视频都会被删除
    • 外键与 relations 功能类似,但作用级别不同
export const videos = pgTable("videos", {
  ...
  userId: uuid("user_id").references(() => users.id, { // 外键 - 关联到users表的id字段
    onDelete: "cascade", // 级联删除,如果用户被删除,则删除该用户的所有视频
  }).notNull(),
  categoryId: uuid("category_id").references(() => categories.id, {
    onDelete: "set null", // 级联删除,如果分类被删除,则将视频的分类设置为null,但不删除视频
  }),
  ...
})
  • Database - Relations 关系
    • 关于relations与foreign key之间的解释,请参考 Drizzle soft relations
    • 表与表之间的逻辑联系,是业务逻辑层面的概念
    • 关系型数据库中常见的关系:One-to-One,One-to-Many,Many-to-Many
    • 视频与用户一对一,视频对应视频分类也是一对一
    export const videoRelations = relations(videos, ({ one }) => ({
      user: one(users, {
        fields: [videos.userId],
        references: [users.id],
      }),
      category: one(categories, {
        fields: [videos.categoryId],
        references: [categories.id],
      }),
    }))
    
    • 用户与视频一对多,或者用户视频为空(不传fields、references)
    export const usersRelations = relations(users, ({ many }) => ({
      videos: many(videos), // 一对多关系,用户可以有多个视频,或者为空
    }));
    
  • Cursor-based pagination 游标分页
    • 我们在获取videos数据的时候,考虑到页面是无限加载滚动的,因此这个tRPC路由使用游标分页
    • 用一个锚点(例如上一条记录的id或时间戳)作为起点,往后取数据;需要记住最后一条记录来精准获取下一页数据
    • 输入参数有2个:cursorlimit
    getAll: protectedProcedure
      .input(
        z.object({
          // 可选游标参数,上一次的最后一条数据,首次请求可不传
          cursor: z.object({ 
              id: z.uuid(),
              updatedAt: z.date(),
          }).nullish(), 
          // 每页数据条数限制
          limit: z.number().min(1).max(100), 
        })
    )
    
    • 使用 prefetchInfinite() 预取数据:预取只加载第一页的数据,因此不用传 cursor
    void trpc.studio.getAll.prefetchInfinite({
      limit: DEFAULT_LIMIT
    });
    
    • 获取缓存数据时使用 useSuspenseInfiniteQuery()
    const [videos, query] = trpc.studio.getAll.useSuspenseInfiniteQuery(
      { limit: DEFAULT_LIMIT },
      { getNextPageParam: lastPage => lastPage.nextCursor }
    )
    
    • tRPC路由中的Sql查询条件:
      • eq(field, value) 等于,field = value
      • and(cond1, cond2, ...) 且,多个条件同时满足,cond1 and cond2 and ...
      • or(cond1, cond2, ...) 或,多个条件只要一个满足,cond1 or cond2 or ...
      • lt(field, value) 小于,Less Than,field < value
      • desc(field) 倒序排序,DESC,根据field字段倒序排序
    import { eq, and, or, lt, desc } from 'drizzle-orm'
    
    getAll: protectedProcedure
      .input(...)
      .query(async ({ ctx, input }) => {
        const { cursor, limit } = input
        const { id: userId } = ctx.user
    
        const data = await db
          .select()
          .from(videos)
          .where(
            and(
              eq( videos.userId, userId ),  // 只查询当前用户的视频
              cursor ? or(
                lt(videos.updatedAt, cursor.updatedAt),  // 查询更新时间小于游标参数的记录
                and(
                  eq(videos.updatedAt, cursor.updatedAt),  // 如果更新时间相同,则查询id小于游标参数的记录
                  lt(videos.id, cursor.id)
                )
              ) : undefined
            )
          )
          .orderBy(desc(videos.updatedAt), desc(videos.id))  // 按更新时间和ID降序排列,也就是最新的视频在前面
          .limit(limit + 1)  // 多查询一条数据用于判断是否还有下一页
    
          const hasMore = data.length > limit // 判断是否还有下一页
          const videosData = hasMore ? data.slice(0, -1) : data // 如果还有下一页,则去掉最后一条数据
          const lastVideo = videosData[videosData.length - 1] // 获取最后一条数据,用于生成下一页的游标
          // 生成下一页的cursor
          const nextCursor = hasMore ? { 
            id: lastVideo.id,
            updatedAt: lastVideo.updatedAt,
          } : null
    
        return {
          videosData,
          nextCursor, // 是一个对象,包含了id和updatedAt两个字段
        }
      })
    
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容