ゴゴゴゴゴ
Skip to content
JOJO
首页文章项目知识库相册音乐
全站搜索
  • Navigate
  • Go to Page
  • escClose
© 2026 JOJOBuilt with Next.js

四种声音一个时代

这里收藏周杰伦、王力宏、陶喆与林俊杰的专辑,也把四位创作者放回完整唱片的语境。陶喆以松弛的律动重塑华语 R&B,周杰伦把说唱、古典与中国式旋律带进流行音乐,王力宏在抒情、摇滚和东方元素之间拓宽制作边界,林俊杰则用细密旋律与宽阔声线记录城市情绪。这里不打乱曲序,也不把专辑压缩成热门单曲;挑一张封面,从第一首听到最后一首,感受开场、中段与尾声如何共同完成一段叙事。

左右滑动浏览,轻触封面,从第一首开始听

正在播放的唱片

选择一首歌曲查看歌词
专辑封面

专辑

    请选择一张唱片等待选曲
    0:00
    0:00

    键盘快捷键

    搜索
    / 或 ⌘K
    回到首页
    G H
    博客列表
    G B
    切换主题
    T
    关闭弹窗
    Esc
    显示帮助
    ?

    按 Esc 关闭此窗口

    Blog550
    • 全部文章(550)
    • 包管理工具(8)
      • Bun 完全指南
      • fnm 完全指南
      • ni 完全指南
      • npm 完全指南
      • nrm 完全指南
      • nvm 完全指南
      • pnpm 完全指南
      • Yarn 完全指南
    • 并发异步(7)
      • 异步任务取消机制
      • Promise静态方法汇总
      • 封装超时工具方法
      • 并发与并行
      • 封装并发任务方法
      • 一道并发相关的面试题
      • 文件写入的原子化与并发操作
    • 打包工具(10)
      • Bun - 全栈 JavaScript 运行时与打包器
      • esbuild - 用 Go 写的极速打包器
      • Parcel - 零配置的极速打包工具
      • Rolldown - Vite 团队打造的 Rust 打包器
      • Rollup - 专注于库开发的打包工具
      • Rspack - Rust 驱动的 Webpack 替代品
      • SWC - Rust 驱动的超快编译器
      • Turbopack - Rust 驱动的下一代打包工具
      • Vite - 下一代前端构建工具
      • Webpack - 功能最强大的模块打包器
    • 算法(74)
      • 课程必看
      • 算法基本概念
      • 评判算法的好坏
      • 复习对数
      • 常见的复杂度
      • 常见算法思维
      • 递归
      • 数据结构
      • 数组结构
      • 顺序查找
      • 二分查找
      • 插值查找
      • 冒泡排序
      • 选择排序
      • 插入排序
      • 折半插入排序
      • 希尔排序
      • 归并排序
      • 快速排序
      • 链表结构
      • 双向链表
      • 单向循环链表
      • 双向循环链表
      • 栈结构
      • 队列结构
      • 循环队列
      • 击鼓传花游戏
      • 双端队列
      • 回文检查器
      • 集合结构
      • 字典结构
      • 树结构
      • 二叉树
      • 二叉树遍历
      • 前序中序还原二叉树
      • 后序中序还原二叉树
      • 二叉树深度优先搜索
      • 二叉树广度优先搜索
      • 二叉树的比较
      • 二叉树diff算法
      • 二叉搜索树
      • 二叉搜索树常见操作
      • 平衡二叉树
      • 二叉树左单旋
      • 二叉树右单旋
      • 二叉树双旋
      • 二叉堆
      • 堆排序
      • 哈夫曼树
      • 图结构
      • 邻接矩阵
      • 邻接表
      • 构建图
      • 图的深度优先搜索
      • 图的广度优先搜索
      • 普里姆算法
      • 克鲁斯卡尔算法
      • LRU算法
      • 动态规划
      • 青蛙跳台阶
      • 不同路径问题
      • 凑零钱问题
      • 打家劫舍
      • 背包问题
      • 回溯算法
      • 数字组合
      • 电话号码字母组合
      • 复原IP地址
      • 数组子集
      • 贪心算法
      • 柠檬水找零
      • 摆动序列
      • 分发糖果
      • 跳跃游戏
    • 文档协同(12)
      • 课程说明
      • 文档协同常见方案
      • OT冲突算法
      • CRDT冲突算法
      • CRDT数据结构
      • 文章翻译
      • Yjs基础介绍
      • Yjs快速上手
      • Yjs细节补充
      • 监听机制
      • 标签页同步
      • WebSocket基础知识
    • 项目实战(1)
      • ShipAny:1小时开发海外游客「中国攻略 + AI旅游助手」SaaS网站
    • AI工具(9)
      • Claude Code 完全指南
      • ComfyUI 完全指南
      • GitHub Copilot 入门指南
      • Coze 完全指南
      • Cursor vs GitHub Copilot 全面对比
      • Cursor-完整指南
      • Dify 完全指南
      • Droid 完全指南
      • n8n 完全指南
    • Astro(5)
      • Astro 入门指南
      • Astro 路由与页面
      • Astro 组件与样式
      • Astro 内容集合
      • Astro 集成与部署
    • CSS(25)
      • CSS 选择器基础
      • CSS 盒模型
      • CSS 定位与层叠
      • 浮动与 BFC
      • Flexbox 布局
      • Grid 布局
      • 响应式设计
      • 伪类与伪元素
      • 现代选择器
      • CSS 变量
      • 层叠层
      • 过渡效果
      • CSS 动画
      • 变换与滤镜
      • 容器查询
      • 文字与排版
      • 背景与渐变
      • 滚动与溢出
      • 裁剪与遮罩
      • 颜色系统
      • 边框与阴影
      • 逻辑属性
      • CSS 函数
      • 多列布局
      • 用户交互
    • ES6+(40)
      • let 和 const 命令
      • 解构赋值基础
      • 解构赋值进阶
      • 字符串的扩展
      • 字符串新增方法
      • 正则表达式扩展
      • 数值的扩展
      • BigInt 大整数
      • 函数默认参数
      • rest 参数与扩展运算符
      • 箭头函数详解
      • 尾调用优化
      • 数组扩展方法
      • 数组新增方法
      • 数组不可变方法
      • 对象的扩展
      • Object 新增方法
      • Symbol 基础
      • Symbol 进阶应用
      • Set 数据结构
      • Map 数据结构
      • TypedArray 类型化数组
      • Proxy 基础
      • Proxy 进阶应用
      • Reflect 元编程
      • 元编程实战案例
      • 装饰器 Decorator
      • Iterator 迭代器
      • Generator 基础
      • Generator 进阶
      • Promise 基础
      • Promise 进阶
      • async 函数
      • await 与错误处理
      • 异步迭代
      • Class 基础
      • Class 继承
      • Module 语法
      • Module 加载
      • ES 新特性汇总
    • HTML(8)
      • HTML 入门:构建 Web 的基石
      • HTML5 语义化:让页面更有意义
      • HTML 表单基础:用户交互的起点
      • HTML 表单进阶:验证与现代特性
      • HTML 多媒体:音视频与响应式图片
      • HTML APIs(上):客户端存储完全指南
      • HTML APIs(下):交互与设备能力
      • Canvas 与 SVG:Web 图形绘制入门
    • JavaScript(30)
      • JavaScript 简介
      • 变量与数据类型
      • 运算符
      • 类型转换
      • 控制流语句
      • 函数基础
      • 对象基础
      • 数组基础
      • 原型与原型链
      • this 关键字
      • 面向对象编程
      • 作用域
      • 闭包
      • 执行上下文
      • DOM 选择器
      • DOM 操作
      • 事件处理
      • 事件委托
      • 错误处理
      • 调试技巧
      • 正则表达式
      • Date 日期对象
      • Math 对象
      • 定时器
      • JSON 处理
      • BOM 基础
      • 本地存储
      • 表单处理
      • AJAX 与 Fetch
      • 严格模式
    • Langchain(37)
      • Langchain基本介绍
      • 模型接口
      • 缓存
      • 提示词模板
      • 结构化提示词
      • Message消息对象
      • 消息占位
      • LCEL
      • 并发流式返回结果
      • Runnable接口
      • 条件路由
      • 字符串解析器
      • 结构化输出解析器
      • 列表输出解析器
      • JSON解析器
      • 正则解析器
      • 本地资源Loader
      • Web资源Loader
      • 文本切割
      • 嵌入处理
      • 内存型向量库
      • 检索优化
      • RAG实践
      • Memory基础
      • 自动维护聊天记录
      • 实现长程记忆
      • 持久化记忆
      • 第三方存储
      • Tools基础
      • Tools配置
      • Zod基础
      • LangChain中使用工具
      • 重新认识Agent
      • LangChain中创建Agent
      • Agent实践
      • 回退机制
      • 回调机制
    • LLM(34)
      • LLM发展历史
      • N-Gram模型
      • 词袋模型
      • 词元
      • 嵌入
      • Transformer内部结构
      • Transformer解码策略
      • Transformer块
      • 本地部署大模型
      • Agent
      • 流式返回信息
      • 支持上下文
      • 获取实时消息
      • 外接DeepSeek
      • Function Calling
      • MCP理论知识
      • Resources
      • Schema
      • 注册资源模板
      • Prompts
      • 监听资源更新
      • 远程通信方式
      • MCP Client
      • 大模型幻觉
      • RCT构词法
      • 提示词常见技巧
      • 基于反馈迭代优化
      • 改善提示效果
      • 提示词评估工具
      • RAG基础知识
      • RAG实践
      • RAG架构演进
      • 大模型微调
      • LoRA微调
    • MCP(8)
      • MCP 入门:连接 AI 与外部世界的桥梁
      • 搭建第一个 MCP Server
      • MCP Tools 深入:让 AI 具备行动能力
      • MCP Resources:向 AI 暴露数据
      • MCP Prompts:预定义 AI 交互模板
      • MCP Transport 与部署
      • 构建 MCP Client:集成到你的应用
      • 实战:构建文档助手 MCP Server
    • Nest(31)
      • 快速上手Nest Cli
      • 三层架构与MVC
      • IoC
      • Nest中的装饰器
      • 模块的基本概念
      • 什么是AOP
      • 中间件与守卫
      • 拦截器
      • 文件上传
      • 自定义装饰器
      • mysql相关回顾
      • 对于ORM的理解
      • 快速上手Redis
      • Nest中使用Redis
      • 状态保存方式
      • Nest中实现Session与JWT
      • JWT登录注册后端处理
      • 使用Passport与循环引用问题
      • 通过环境变量获取配置信息
      • RBAC权限设计1
      • RBAC权限设计2
      • Nest中使用Websocket
      • 日志处理
      • 面向切面的日志处理
      • 快速入门GraphQL
      • Nest中使用GraphQL
      • Swagger API文档
      • 单元测试与E2E测试
      • 安全防护实践
      • 定时任务与任务调度
      • Bull队列与异步任务
    • Next(36)
      • Next.js 简介
      • 项目创建与配置
      • App Router 概述
      • 创建第一个页面
      • 开发工具与调试
      • TypeScript 配置
      • 文件系统路由
      • 动态路由
      • 嵌套路由与布局
      • 路由组
      • 并行路由
      • 拦截路由
      • 导航与链接
      • 中间件
      • Server Components
      • Client Components
      • 混合渲染模式
      • Streaming 与 Suspense
      • 静态与动态渲染
      • 数据获取基础
      • 缓存策略
      • Server Actions
      • Route Handlers
      • 数据库集成
      • 错误处理
      • 图片优化
      • 字体优化
      • 脚本优化
      • 元数据与 SEO
      • 性能分析
      • 认证基础
      • NextAuth 集成
      • 环境变量与配置
      • 测试策略
      • 部署方案
      • 项目实战
    • Nuxt(36)
      • web应用变迁史
      • 课程说明
      • SSR初体验
      • 水合操作
      • 改造CSR项目
      • 基本介绍
      • 配置概览
      • 组件类型
      • Nuxt模块
      • 嵌套路由
      • 动态路由
      • 请求数据
      • server目录
      • 应用部署
      • 中间件
      • 插件
      • layers
      • 过渡
      • 状态管理
      • 全匹配路由
      • 路由补充知识
      • 自定义路由
      • 自定义模块
      • 自定义指令
      • meta相关处理
      • 渲染模式
      • 错误处理
      • 不同阶段错误
      • 客户端渲染组件
      • 其余内置组件
      • 配置相关细节
      • 常用组合式函数
      • 服务器引擎
      • NuxtUI
      • SSR注意事项
      • 课程收官
    • React(55)
      • 序章:React课程介绍
      • React 基本介绍
      • JSX 基础语法
      • 组件与事件绑定
      • 组件状态与数据传递
      • 表单
      • 生命周期
      • Hooks
      • React-router介绍
      • React-router v版本学生管理系统课堂笔记
      • React-redux 介绍
      • Antd介绍
      • coder station前台系统笔记
      • coder station 后台系统笔记
      • Antd Pro介绍
      • Dva介绍
      • Umi.js 介绍
      • 属性默认值和类型验证
      • 高阶组件
      • Ref
      • Context
      • Render Props
      • Portals
      • 错误边界
      • 组件渲染性能优化
      • 前端框架的理解
      • Scheduler调度延时任务
      • 最小堆
      • React中的位运算
      • beginWork工作流程
      • completeWork工作流程
      • 图解diff算法
      • commit 工作流程
      • lane模型
      • React 中的事件
      • Hooks原理
      • React和Vue描述页面的区别
      • useState和useReducer
      • effect相关hook
      • useCallback和useMemo
      • useRef
      • Update
      • 性能优化策略之eagerState
      • 性能优化策略之bailout
      • bailout和ContextAPI
      • 性能优化对日常开发启示
      • 前端框架的分类
      • 虚拟 DOM
      • React 整体架构
      • React 渲染流程
      • Fiber双缓冲
      • MessageChannel
      • Scheduler调度普通任务
      • 课程介绍
      • 课程总结
    • ReactNative(33)
      • RN 基本介绍
      • 快速搭建 RN 开发环境
      • 复习 React 基础知识
      • RN 基础知识
      • 实战案例
      • RN 内置基础组件
      • 容器组件
      • Pressable 组件
      • 列表组件
      • 功能组件
      • 第三方组件库
      • 自定义组件案例:弹框组件
      • 自定义组件案例:单选组件
      • 基础API
      • 屏幕API
      • 设备API
      • 动画API
      • 手势API
      • React Navigation简介
      • 参数传递和标题栏信息配置
      • 嵌套路由与生命周期
      • 其他类型的导航
      • 状态管理介绍
      • Redux
      • 其他第三方库
      • Expo SDK
      • Fabric 架构
      • 渲染、提交和挂载
      • 实现跨平台
      • 扁平化视图
      • 线程模型
      • 如何打包发布应用
      • 通过 React Native CLI 搭建环境
    • TailwindCSS(7)
      • TailwindCSS基础介绍
      • 环境搭建与配置
      • 布局系统
      • 间距与尺寸系统
      • 颜色与主题
      • 响应式设计
      • 交互与动画
    • TypeScript(35)
      • 初识 TypeScript
      • 基础类型
      • 数组与元组
      • 对象类型
      • 函数类型
      • 类型推断与断言
      • 联合类型与交叉类型
      • 字面量类型与类型别名
      • 接口基础
      • 接口进阶
      • 接口继承与实现
      • 类基础
      • 类的访问修饰符
      • 类的继承
      • 类与接口对比
      • 泛型基础
      • 泛型接口与泛型类
      • 泛型约束
      • 内置工具类型(上)
      • 内置工具类型(下)
      • 泛型实战
      • 类型守卫
      • 类型收窄
      • 索引类型与索引签名
      • 映射类型
      • 条件类型
      • infer 关键字
      • 模板字面量类型
      • 模块系统
      • 命名空间
      • 声明文件基础
      • 声明文件进阶
      • tsconfig 配置详解
      • 类型体操入门
      • TypeScript 最佳实践
    • 杂谈(9)
      • Cloudflare 全栈部署指南
      • Creem 支付集成指南
      • Dokploy 自托管部署指南
      • GitHub OAuth 登录集成指南
      • Google OAuth 登录集成指南
      • Stripe 支付集成指南
      • Supabase 完全指南
      • Vercel 自动化部署指南
      • Zeabur 部署指南
    • 全部文章(550)
    • 包管理工具(8)
      • Bun 完全指南
      • fnm 完全指南
      • ni 完全指南
      • npm 完全指南
      • nrm 完全指南
      • nvm 完全指南
      • pnpm 完全指南
      • Yarn 完全指南
    • 并发异步(7)
      • 异步任务取消机制
      • Promise静态方法汇总
      • 封装超时工具方法
      • 并发与并行
      • 封装并发任务方法
      • 一道并发相关的面试题
      • 文件写入的原子化与并发操作
    • 打包工具(10)
      • Bun - 全栈 JavaScript 运行时与打包器
      • esbuild - 用 Go 写的极速打包器
      • Parcel - 零配置的极速打包工具
      • Rolldown - Vite 团队打造的 Rust 打包器
      • Rollup - 专注于库开发的打包工具
      • Rspack - Rust 驱动的 Webpack 替代品
      • SWC - Rust 驱动的超快编译器
      • Turbopack - Rust 驱动的下一代打包工具
      • Vite - 下一代前端构建工具
      • Webpack - 功能最强大的模块打包器
    • 算法(74)
      • 课程必看
      • 算法基本概念
      • 评判算法的好坏
      • 复习对数
      • 常见的复杂度
      • 常见算法思维
      • 递归
      • 数据结构
      • 数组结构
      • 顺序查找
      • 二分查找
      • 插值查找
      • 冒泡排序
      • 选择排序
      • 插入排序
      • 折半插入排序
      • 希尔排序
      • 归并排序
      • 快速排序
      • 链表结构
      • 双向链表
      • 单向循环链表
      • 双向循环链表
      • 栈结构
      • 队列结构
      • 循环队列
      • 击鼓传花游戏
      • 双端队列
      • 回文检查器
      • 集合结构
      • 字典结构
      • 树结构
      • 二叉树
      • 二叉树遍历
      • 前序中序还原二叉树
      • 后序中序还原二叉树
      • 二叉树深度优先搜索
      • 二叉树广度优先搜索
      • 二叉树的比较
      • 二叉树diff算法
      • 二叉搜索树
      • 二叉搜索树常见操作
      • 平衡二叉树
      • 二叉树左单旋
      • 二叉树右单旋
      • 二叉树双旋
      • 二叉堆
      • 堆排序
      • 哈夫曼树
      • 图结构
      • 邻接矩阵
      • 邻接表
      • 构建图
      • 图的深度优先搜索
      • 图的广度优先搜索
      • 普里姆算法
      • 克鲁斯卡尔算法
      • LRU算法
      • 动态规划
      • 青蛙跳台阶
      • 不同路径问题
      • 凑零钱问题
      • 打家劫舍
      • 背包问题
      • 回溯算法
      • 数字组合
      • 电话号码字母组合
      • 复原IP地址
      • 数组子集
      • 贪心算法
      • 柠檬水找零
      • 摆动序列
      • 分发糖果
      • 跳跃游戏
    • 文档协同(12)
      • 课程说明
      • 文档协同常见方案
      • OT冲突算法
      • CRDT冲突算法
      • CRDT数据结构
      • 文章翻译
      • Yjs基础介绍
      • Yjs快速上手
      • Yjs细节补充
      • 监听机制
      • 标签页同步
      • WebSocket基础知识
    • 项目实战(1)
      • ShipAny:1小时开发海外游客「中国攻略 + AI旅游助手」SaaS网站
    • AI工具(9)
      • Claude Code 完全指南
      • ComfyUI 完全指南
      • GitHub Copilot 入门指南
      • Coze 完全指南
      • Cursor vs GitHub Copilot 全面对比
      • Cursor-完整指南
      • Dify 完全指南
      • Droid 完全指南
      • n8n 完全指南
    • Astro(5)
      • Astro 入门指南
      • Astro 路由与页面
      • Astro 组件与样式
      • Astro 内容集合
      • Astro 集成与部署
    • CSS(25)
      • CSS 选择器基础
      • CSS 盒模型
      • CSS 定位与层叠
      • 浮动与 BFC
      • Flexbox 布局
      • Grid 布局
      • 响应式设计
      • 伪类与伪元素
      • 现代选择器
      • CSS 变量
      • 层叠层
      • 过渡效果
      • CSS 动画
      • 变换与滤镜
      • 容器查询
      • 文字与排版
      • 背景与渐变
      • 滚动与溢出
      • 裁剪与遮罩
      • 颜色系统
      • 边框与阴影
      • 逻辑属性
      • CSS 函数
      • 多列布局
      • 用户交互
    • ES6+(40)
      • let 和 const 命令
      • 解构赋值基础
      • 解构赋值进阶
      • 字符串的扩展
      • 字符串新增方法
      • 正则表达式扩展
      • 数值的扩展
      • BigInt 大整数
      • 函数默认参数
      • rest 参数与扩展运算符
      • 箭头函数详解
      • 尾调用优化
      • 数组扩展方法
      • 数组新增方法
      • 数组不可变方法
      • 对象的扩展
      • Object 新增方法
      • Symbol 基础
      • Symbol 进阶应用
      • Set 数据结构
      • Map 数据结构
      • TypedArray 类型化数组
      • Proxy 基础
      • Proxy 进阶应用
      • Reflect 元编程
      • 元编程实战案例
      • 装饰器 Decorator
      • Iterator 迭代器
      • Generator 基础
      • Generator 进阶
      • Promise 基础
      • Promise 进阶
      • async 函数
      • await 与错误处理
      • 异步迭代
      • Class 基础
      • Class 继承
      • Module 语法
      • Module 加载
      • ES 新特性汇总
    • HTML(8)
      • HTML 入门:构建 Web 的基石
      • HTML5 语义化:让页面更有意义
      • HTML 表单基础:用户交互的起点
      • HTML 表单进阶:验证与现代特性
      • HTML 多媒体:音视频与响应式图片
      • HTML APIs(上):客户端存储完全指南
      • HTML APIs(下):交互与设备能力
      • Canvas 与 SVG:Web 图形绘制入门
    • JavaScript(30)
      • JavaScript 简介
      • 变量与数据类型
      • 运算符
      • 类型转换
      • 控制流语句
      • 函数基础
      • 对象基础
      • 数组基础
      • 原型与原型链
      • this 关键字
      • 面向对象编程
      • 作用域
      • 闭包
      • 执行上下文
      • DOM 选择器
      • DOM 操作
      • 事件处理
      • 事件委托
      • 错误处理
      • 调试技巧
      • 正则表达式
      • Date 日期对象
      • Math 对象
      • 定时器
      • JSON 处理
      • BOM 基础
      • 本地存储
      • 表单处理
      • AJAX 与 Fetch
      • 严格模式
    • Langchain(37)
      • Langchain基本介绍
      • 模型接口
      • 缓存
      • 提示词模板
      • 结构化提示词
      • Message消息对象
      • 消息占位
      • LCEL
      • 并发流式返回结果
      • Runnable接口
      • 条件路由
      • 字符串解析器
      • 结构化输出解析器
      • 列表输出解析器
      • JSON解析器
      • 正则解析器
      • 本地资源Loader
      • Web资源Loader
      • 文本切割
      • 嵌入处理
      • 内存型向量库
      • 检索优化
      • RAG实践
      • Memory基础
      • 自动维护聊天记录
      • 实现长程记忆
      • 持久化记忆
      • 第三方存储
      • Tools基础
      • Tools配置
      • Zod基础
      • LangChain中使用工具
      • 重新认识Agent
      • LangChain中创建Agent
      • Agent实践
      • 回退机制
      • 回调机制
    • LLM(34)
      • LLM发展历史
      • N-Gram模型
      • 词袋模型
      • 词元
      • 嵌入
      • Transformer内部结构
      • Transformer解码策略
      • Transformer块
      • 本地部署大模型
      • Agent
      • 流式返回信息
      • 支持上下文
      • 获取实时消息
      • 外接DeepSeek
      • Function Calling
      • MCP理论知识
      • Resources
      • Schema
      • 注册资源模板
      • Prompts
      • 监听资源更新
      • 远程通信方式
      • MCP Client
      • 大模型幻觉
      • RCT构词法
      • 提示词常见技巧
      • 基于反馈迭代优化
      • 改善提示效果
      • 提示词评估工具
      • RAG基础知识
      • RAG实践
      • RAG架构演进
      • 大模型微调
      • LoRA微调
    • MCP(8)
      • MCP 入门:连接 AI 与外部世界的桥梁
      • 搭建第一个 MCP Server
      • MCP Tools 深入:让 AI 具备行动能力
      • MCP Resources:向 AI 暴露数据
      • MCP Prompts:预定义 AI 交互模板
      • MCP Transport 与部署
      • 构建 MCP Client:集成到你的应用
      • 实战:构建文档助手 MCP Server
    • Nest(31)
      • 快速上手Nest Cli
      • 三层架构与MVC
      • IoC
      • Nest中的装饰器
      • 模块的基本概念
      • 什么是AOP
      • 中间件与守卫
      • 拦截器
      • 文件上传
      • 自定义装饰器
      • mysql相关回顾
      • 对于ORM的理解
      • 快速上手Redis
      • Nest中使用Redis
      • 状态保存方式
      • Nest中实现Session与JWT
      • JWT登录注册后端处理
      • 使用Passport与循环引用问题
      • 通过环境变量获取配置信息
      • RBAC权限设计1
      • RBAC权限设计2
      • Nest中使用Websocket
      • 日志处理
      • 面向切面的日志处理
      • 快速入门GraphQL
      • Nest中使用GraphQL
      • Swagger API文档
      • 单元测试与E2E测试
      • 安全防护实践
      • 定时任务与任务调度
      • Bull队列与异步任务
    • Next(36)
      • Next.js 简介
      • 项目创建与配置
      • App Router 概述
      • 创建第一个页面
      • 开发工具与调试
      • TypeScript 配置
      • 文件系统路由
      • 动态路由
      • 嵌套路由与布局
      • 路由组
      • 并行路由
      • 拦截路由
      • 导航与链接
      • 中间件
      • Server Components
      • Client Components
      • 混合渲染模式
      • Streaming 与 Suspense
      • 静态与动态渲染
      • 数据获取基础
      • 缓存策略
      • Server Actions
      • Route Handlers
      • 数据库集成
      • 错误处理
      • 图片优化
      • 字体优化
      • 脚本优化
      • 元数据与 SEO
      • 性能分析
      • 认证基础
      • NextAuth 集成
      • 环境变量与配置
      • 测试策略
      • 部署方案
      • 项目实战
    • Nuxt(36)
      • web应用变迁史
      • 课程说明
      • SSR初体验
      • 水合操作
      • 改造CSR项目
      • 基本介绍
      • 配置概览
      • 组件类型
      • Nuxt模块
      • 嵌套路由
      • 动态路由
      • 请求数据
      • server目录
      • 应用部署
      • 中间件
      • 插件
      • layers
      • 过渡
      • 状态管理
      • 全匹配路由
      • 路由补充知识
      • 自定义路由
      • 自定义模块
      • 自定义指令
      • meta相关处理
      • 渲染模式
      • 错误处理
      • 不同阶段错误
      • 客户端渲染组件
      • 其余内置组件
      • 配置相关细节
      • 常用组合式函数
      • 服务器引擎
      • NuxtUI
      • SSR注意事项
      • 课程收官
    • React(55)
      • 序章:React课程介绍
      • React 基本介绍
      • JSX 基础语法
      • 组件与事件绑定
      • 组件状态与数据传递
      • 表单
      • 生命周期
      • Hooks
      • React-router介绍
      • React-router v版本学生管理系统课堂笔记
      • React-redux 介绍
      • Antd介绍
      • coder station前台系统笔记
      • coder station 后台系统笔记
      • Antd Pro介绍
      • Dva介绍
      • Umi.js 介绍
      • 属性默认值和类型验证
      • 高阶组件
      • Ref
      • Context
      • Render Props
      • Portals
      • 错误边界
      • 组件渲染性能优化
      • 前端框架的理解
      • Scheduler调度延时任务
      • 最小堆
      • React中的位运算
      • beginWork工作流程
      • completeWork工作流程
      • 图解diff算法
      • commit 工作流程
      • lane模型
      • React 中的事件
      • Hooks原理
      • React和Vue描述页面的区别
      • useState和useReducer
      • effect相关hook
      • useCallback和useMemo
      • useRef
      • Update
      • 性能优化策略之eagerState
      • 性能优化策略之bailout
      • bailout和ContextAPI
      • 性能优化对日常开发启示
      • 前端框架的分类
      • 虚拟 DOM
      • React 整体架构
      • React 渲染流程
      • Fiber双缓冲
      • MessageChannel
      • Scheduler调度普通任务
      • 课程介绍
      • 课程总结
    • ReactNative(33)
      • RN 基本介绍
      • 快速搭建 RN 开发环境
      • 复习 React 基础知识
      • RN 基础知识
      • 实战案例
      • RN 内置基础组件
      • 容器组件
      • Pressable 组件
      • 列表组件
      • 功能组件
      • 第三方组件库
      • 自定义组件案例:弹框组件
      • 自定义组件案例:单选组件
      • 基础API
      • 屏幕API
      • 设备API
      • 动画API
      • 手势API
      • React Navigation简介
      • 参数传递和标题栏信息配置
      • 嵌套路由与生命周期
      • 其他类型的导航
      • 状态管理介绍
      • Redux
      • 其他第三方库
      • Expo SDK
      • Fabric 架构
      • 渲染、提交和挂载
      • 实现跨平台
      • 扁平化视图
      • 线程模型
      • 如何打包发布应用
      • 通过 React Native CLI 搭建环境
    • TailwindCSS(7)
      • TailwindCSS基础介绍
      • 环境搭建与配置
      • 布局系统
      • 间距与尺寸系统
      • 颜色与主题
      • 响应式设计
      • 交互与动画
    • TypeScript(35)
      • 初识 TypeScript
      • 基础类型
      • 数组与元组
      • 对象类型
      • 函数类型
      • 类型推断与断言
      • 联合类型与交叉类型
      • 字面量类型与类型别名
      • 接口基础
      • 接口进阶
      • 接口继承与实现
      • 类基础
      • 类的访问修饰符
      • 类的继承
      • 类与接口对比
      • 泛型基础
      • 泛型接口与泛型类
      • 泛型约束
      • 内置工具类型(上)
      • 内置工具类型(下)
      • 泛型实战
      • 类型守卫
      • 类型收窄
      • 索引类型与索引签名
      • 映射类型
      • 条件类型
      • infer 关键字
      • 模板字面量类型
      • 模块系统
      • 命名空间
      • 声明文件基础
      • 声明文件进阶
      • tsconfig 配置详解
      • 类型体操入门
      • TypeScript 最佳实践
    • 杂谈(9)
      • Cloudflare 全栈部署指南
      • Creem 支付集成指南
      • Dokploy 自托管部署指南
      • GitHub OAuth 登录集成指南
      • Google OAuth 登录集成指南
      • Stripe 支付集成指南
      • Supabase 完全指南
      • Vercel 自动化部署指南
      • Zeabur 部署指南

    对于ORM的理解

    2025年10月14日 · 28 min
    ChatGPTClaude

    环境信息:Node.js 20.x、TypeORM 0.3.x、MySQL 8.x、@nestjs/typeorm 10.x

    一、对于ORM的理解

    面向对象编程和关系型数据库,都是目前最流行的技术,但是它们的模型是不一样的。

    面向对象编程把所有实体看成对象(object),关系型数据库则是采用实体之间的关系(relation)连接数据。很早就有人提出,关系也可以用对象表达,这样的话,就能使用面向对象编程,来操作关系型数据库。

    image-20250201152823282

    简单说,ORM 就是通过实例对象的语法,完成关系型数据库的操作的技术,是”对象-关系映射”(Object/Relational Mapping) 的缩写。

    ORM 把数据库映射成对象

    • 数据库的表(table) —> 类(class)
    • 记录(record,行数据)—> 对象(object)
    • 字段(field)—> 对象的属性(attribute)

    举例来说,下面是一行 SQL 语句。

    SELECT id, first_name, last_name, phone, birth_date, sex
    FROM persons
    WHERE id = 10

    如果程序直接操作SQL,大致语法如下:

    res = db.execSql(sql)
    name = res[0]['FIRST_NAME']

    如果改成 ORM 的写法大致如下:

    p = Person.get(10)
    name = p.first_name

    一比较就可以发现,ORM 使用对象,封装了数据库操作,因此可以不碰 SQL 语言。开发者只使用面向对象编程,与数据对象直接交互,不用关心底层数据库。

    ORM 有下面这些优点:

    • 数据模型都在一个地方定义,更容易更新和维护,也利于重用代码。
    • ORM 有现成的工具,很多功能都可以自动完成,比如数据消毒、预处理、事务等等。
    • 它迫使你使用 MVC 架构,ORM 就是天然的 Model,最终使代码更清晰。
    • 基于 ORM 的业务代码比较简单,代码量少,语义性好,容易理解。
    • 你不必编写性能不佳的 SQL。

    但是,ORM 也有很突出的缺点。

    • ORM 库不是轻量级工具,需要花精力学习和设置。
    • 对于复杂的查询,ORM 要么是无法表达,要么是性能不如原生的 SQL。
    • ORM 抽象掉了数据库层,开发者无法了解底层的数据库操作,也无法定制一些特殊的 SQL。

    二、TypeORM

    TypeORM(中文文档)是一个ORM,可以在 NodeJS、浏览器、Cordova、PhoneGap、Ionic、React Native、NativeScript、Expo 和 Electron 平台上运行,并且可以与 TypeScript 和 JavaScript(ES5、ES6、ES7、ES8)一起使用。

    TypeORM 受到其他 ORM 的很大影响,例如 Hibernate、Doctrine 和 Entity Framework。

    直接开始

    直接运行命令创建TypeORM项目

    npx typeorm init --name typeorm-test --database mysql2

    没有安装TypeORM包的话,使用npx命令时,系统会自动检测是否已经安装这个包。

    Terminal window
    npm http fetch GET 200 https://registry.npmjs.org/npm 729ms (cache updated)
    npm http fetch GET 200 https://registry.npmjs.org/typeorm 1538ms (cache miss)
    Need to install the following packages:
    typeorm@0.3.20
    Ok to proceed? (y) y

    选择“y”后,脚本将自动安装相关的依赖包

    不过项目并不会自动帮你安装mysql的依赖包,所以项目创建好之后,还需在项目中安装mysql2的相关依赖

    npm i mysql2 --save

    创建模型及实体

    要处理数据库,首先当然要创建表,在TypeORM中,我们通过定义实体来告诉他如何创建一个数据表,比如,我们要创建一个用户表,就需要定义用户的实体模型

    import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'
    @Entity()
    export class User {
    @PrimaryGeneratedColumn()
    id: number
    @Column({ length: 100 })
    name: string
    @Column({ length: 100 })
    nickname: string
    @Column()
    age: number
    @Column({ length: 11 })
    phone: string
    @Column('text')
    desc: string
    @Column('double', {
    default: 0,
    })
    other: number
    }

    我们仅仅定义一个User模型并不够,还需要使用装饰器@Entity将其定义为一个实体

    用于装饰列的装饰器包括 @Column、@PrimaryGeneratedColumn和@PrimaryColumn。@PrimaryColumn用于定义普通主键列,而@PrimaryGeneratedColumn用于定义自增主键列。

    string类型默认映射为数据库中的varchar(255),number类型默认映射为int整数类型。如果我们希望某个字段是文本(Text)类型或者(Double)类型,可以通过@Column装饰器指定

    连接数据库

    为了和数据库进行交互,我们需要设置一个数据源(DataSource)。数据源中包含了与数据库连接相关的配置。通过这些配置,我们可以建立与数据库的初始连接或者连接池。

    import 'reflect-metadata'
    import { DataSource } from 'typeorm'
    import { User } from './entity/User'
    export const AppDataSource = new DataSource({
    type: 'mysql',
    host: 'localhost',
    port: 3306,
    username: 'root',
    password: '123456',
    database: 'typeorm_test',
    synchronize: true,
    logging: true,
    entities: [User],
    migrations: [],
    subscribers: [],
    })
    • type :表示数据库的类型,TypeORM 不只支持 MySQL 还支持 postgres、oracle、sqllite 等数据库。
    • database:表示你需要连接的数据库名,mysql 是可以有多个 database (schema)的。**注意:**数据库并不能帮你创建,需要自己先创建数据库
    • host、port :表示数据库服务器的主机和端口号
    • user、password:表示登录数据库的用户名和密码
    • synchronize :表示根据同步建表,简单来说就是当 database 里没有和 Entity 对应的表的时候,会自动生成建表 sql 语句并执行
    • logging:启动日志记录,会打印生成的 sql 语句
    • entities :指定有哪些和数据库的表对应的 Entity,也可以通过路径的方式指定,也可以用通配符,比如:entities: ['./**/entity/*.ts']

    还有一些其他的配置:

    • migrations :修改表结构之类的 sql
    • subscribers:Entity 生命周期的订阅者,比如 insert、update、remove 前后,可以加入一些逻辑
    • poolSize:指定数据库连接池中连接的最大数量
    • connectorPackage:指定驱动包

    创建DataSource实例之后,需要调用该实例的initialize方法来创建数据库连接。

    使用实体管理器操作数据库

    一旦数据库连接成功,我们就可以使用实体管理器(manager)来执行数据库操作,在src/index.ts文件下,稍作修改

    import { AppDataSource } from './data-source'
    import { User } from './entity/User'
    AppDataSource.initialize()
    .then(async () => {
    const user1 = new User()
    user1.name = 'jack'
    user1.nickname = '张三'
    user1.phone = '13311111111'
    user1.age = 25
    user1.desc = 'This is a desc1'
    // 插入数据
    await AppDataSource.manager.save(user1)
    console.log('Saved a new user with id: ' + user1.id)
    const user2 = new User()
    user2.name = 'rose'
    user2.nickname = '李四'
    user2.phone = '13322222222'
    user2.age = 26
    user2.desc = 'This is a desc2'
    await AppDataSource.manager.save(user2)
    console.log('Saved a new user with id: ' + user2.id)
    // 查询数据
    const users = await AppDataSource.manager.find(User)
    console.log('查询所有用户信息: ', users)
    // 更新数据
    user1.name = 'tom'
    await AppDataSource.manager.save(user1)
    // 查询数据
    const user = await AppDataSource.manager.findOneBy(User, {
    id: 2,
    })
    console.log('用户信息:', user)
    })
    .catch((error) => console.log(error))

    使用Repository操作CRUD

    除了使用实体管理器(EntityManager)来管理实体外,我们还可以使用存储库(Repository)来管理每个实体,我们直接通过代码来看看

    import { AppDataSource } from './data-source'
    import { User } from './entity/User'
    async function main() {
    await AppDataSource.initialize()
    const userRepository = AppDataSource.getRepository(User)
    const user1 = new User()
    user1.name = 'jack'
    user1.nickname = '张三'
    user1.phone = '13311111111'
    user1.age = 25
    user1.desc = 'This is a desc1'
    // 新增 user1
    await userRepository.save(user1)
    console.log('新增成功...', user1)
    const user2 = new User()
    user2.name = 'rose'
    user2.nickname = '李四'
    user2.phone = '13322222222'
    user2.age = 26
    user2.desc = 'This is a desc2'
    // 新增 user2
    await userRepository.save(user2)
    console.log('新增成功...', user2)
    // 查询
    const users = await userRepository.find()
    console.log('查询所有用户信息:', users)
    // 根据条件查询
    const user = await userRepository.findOneBy({ id: 2 })
    console.log('查询id=2的用户信息:', user)
    // 更新
    user.name = 'tom'
    user.nickname = '王五'
    await userRepository.save(user)
    console.log('更新成功...', user)
    // 删除
    // const userDelete = await userRepository.findOneBy({ id: 2 });
    // await userRepository.delete(userDelete);
    // console.log("删除成功...");
    }
    main()

    使用QueryBuilder操作CRUD

    QueryBuilder是TypeORM中最强大的功能之一,它提供了更加底层的数据库查询操作,灵活度高,支持组合更复杂的SQL查询语句,比如多条件查询、多表查询等等。

    创建QueryBuilder的方式有很多,可以通过DataSource创建,也可以通过Repository创建,还能通过EntityManager创建

    // DataSource创建
    async function main() {
    await AppDataSource.initialize()
    const user = await AppDataSource.createQueryBuilder()
    .select('user')
    .from(User, 'user')
    .where('user.id = :id', { id: 1 })
    .getOne()
    console.log('查询id=1的用户信息:', user)
    }
    main()
    // Repository创建
    const user = await AppDataSource.getRepository(User)
    .createQueryBuilder('user')
    .where('user.id = :id', { id: 1 })
    .getOne()
    console.log('查询id=1的用户信息:', user)
    // EntityManager创建
    const user = await AppDataSource.manager
    .createQueryBuilder(User, 'user')
    .where('user.id = :id', { id: 1 })
    .getOne()
    console.log('查询id=1的用户信息:', user)

    使用哪种方式创建都可以。

    比如我们现在要新增一条数据

    async function main() {
    await AppDataSource.initialize()
    const user = new User()
    user.name = 'lily'
    user.nickname = '丽丽'
    user.phone = '13333333333'
    user.age = 18
    user.desc = '......'
    const queryBuilder = AppDataSource.createQueryBuilder()
    const result = await queryBuilder.insert().into(User).values(user).execute()
    console.log('插入数据:', result)
    }
    main()

    上面代码使用insert和into方法进行插入操作,语法上接近原始的SQL语句,value方法中可以传入对象或者数组,如果要插入多条记录,就可以选择传入数组。最后调用execute方法执行语句

    如果要执行查询:

    const saveUsers = await queryBuilder.select('u').from(User, 'u').getMany()
    console.log('查询所有用户信息:', saveUsers)

    同样查询操作类似于**“select from”的SQL语句,其实u是User**实体的别名,getMany方法可以把多条记录查询出来。注意查询出来的记录和我们代码的实体对象一一对应。

    可能有时候我们数据库的字段和实体字段并不一致,如果仅仅就只想查询数据库中的字段,可以使用getRawMany,比如我们有这样的实体:

    import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'
    @Entity()
    export class User {
    @PrimaryGeneratedColumn()
    id: number
    @Column({
    name: 'u_name',
    length: 100,
    })
    name: string
    @Column({
    name: 'u_nickname',
    length: 100,
    })
    nickname: string
    @Column()
    age: number
    @Column({ length: 11 })
    phone: string
    @Column('text')
    desc: string
    @Column('double', {
    default: 0,
    })
    other: number
    }

    查询的时候

    const saveUsers = await queryBuilder.select().from(User, 'u').getRawMany()
    console.log('查询所有用户信息:', saveUsers)

    就能获取数据库原始的字段。

    如果我们想查询单条信息并修改

    const userUpdate = await queryBuilder
    .select('u')
    .from(User, 'u')
    .where('u.id = :id', { id: 1 })
    .getOne()
    console.log('查询id=1的用户信息:', userUpdate)
    userUpdate.name = 'lucy'
    userUpdate.nickname = '露西'
    userUpdate.phone = '13344444444'
    userUpdate.age = 20
    userUpdate.desc = '123456'
    const resultUpdate = await queryBuilder
    .update(User)
    .set(userUpdate)
    .where('id = :id', { id: 1 })
    .execute()
    console.log('更新数据:', resultUpdate)

    最后是删除数据

    const resultDelete = await queryBuilder
    .delete()
    .from(User)
    .where('id = :id', { id: 1 })
    .execute()
    console.log('删除数据:', resultDelete)

    三、TypeORM处理多表关系

    一对一关系

    以用户表和身份证表为例,用户表上面已经创建过了,接下来创建一个身份证实例

    import {
    Entity,
    PrimaryGeneratedColumn,
    Column,
    OneToOne,
    JoinColumn,
    } from 'typeorm'
    import { User } from './User'
    @Entity()
    export class IdCard {
    @PrimaryGeneratedColumn()
    id: number
    @Column({ length: 18 })
    cardNo: string
    @Column()
    name: string
    @Column()
    address: string
    @Column()
    birthday: Date
    @Column()
    email: string
    @OneToOne(() => User)
    @JoinColumn()
    user: User
    }

    上面最关键的是,是 @OneToOne装饰器来建立与User实体之间的一对一关联关系。同时用 @JoinColumn()装饰器来定义user列,用来维护一个外键,TypeORM会自动帮我们生成外键id

    **注意:**添加了实体,就需要将实体添加到DataSource配置选择entites中

    export const AppDataSource = new DataSource({
    ......
    entities: [User, IdCard],
    });

    当然,如果你配置的是./**/entity/*.ts这样的通配符路径,则可以忽略

    接下来,我们在index.ts中分别为User和IdCard插入数据,关键点是要确保他们关联起来

    import { AppDataSource } from './data-source'
    import { IdCard } from './entity/IdCard'
    import { User } from './entity/User'
    async function main() {
    await AppDataSource.initialize()
    // 创建一个用户
    const user = new User()
    user.name = 'hmm'
    user.nickname = '韩梅梅'
    user.phone = '13355555555'
    user.age = 18
    user.desc = '......'
    // 创建一个身份证信息
    const idCard = new IdCard()
    idCard.cardNo = '123456789012345678'
    idCard.name = 'hmm'
    idCard.address = '北京市朝阳区'
    idCard.birthday = new Date('2000-01-01')
    idCard.email = 'hmm2000@163.com'
    // 关联两个实体
    idCard.user = user
    // 获取实体存储库
    const userRepository = AppDataSource.getRepository(User)
    const idCardRepository = AppDataSource.getRepository(IdCard)
    // 先保存用户信息
    await userRepository.save(user)
    console.log('保存用户信息成功...', user)
    // 再保存身份证信息
    await idCardRepository.save(idCard)
    console.log('保存身份证信息成功...', idCard)
    }
    main()

    在数据库中,我们已经可以看到两张表的关联关系,比如查看id_card表的相关DDL信息,就能看到自动生成的建表信息

    image-20250202200943113

    如果在数据库中,我们要查询两者信息,可以使用SQL语句

    select * from user join id_card on user.id = id_card.userid

    image-20250202201103808

    在代码中,我们可以使用Repository提供的find方法来查询。

    但是两个实体的关系是单向的,也就是我们只在IdCard实体中持有User实体的外键列,这就使得IdCard有权访问User,但是反过来User其实并不知道IdCard的存在。

    在这种情况下应该使用idCardRepository来查询,并且设置关联(relations)

    async function main() {
    await AppDataSource.initialize()
    // 获取idCard实体存储库
    const idCardRepository = AppDataSource.getRepository(IdCard)
    const idCards = await idCardRepository.find({
    relations: { user: true },
    })
    console.log(idCards)
    }
    main()

    双向关联

    所以,为了让User实体能够访问到IdCard实体,我们需要修改实体类建立双向关联

    @Entity()
    export class IdCard {
    //...... 省略
    @OneToOne(() => User, (user) => user.card)
    @JoinColumn()
    user: User
    }
    @Entity()
    export class User {
    // ...... 省略
    @OneToOne(() => IdCard, (idCard) => idCard.user)
    card: IdCard
    }

    此时,如果我们用userRepository同样也就可以加载关联表数据了

    import { AppDataSource } from './data-source'
    import { IdCard } from './entity/IdCard'
    import { User } from './entity/User'
    async function main() {
    await AppDataSource.initialize()
    // 获取idCard实体存储库
    const idCardRepository = AppDataSource.getRepository(IdCard)
    // 通过idCard实体查询关联表数据
    const idCards = await idCardRepository.find({
    relations: { user: true },
    })
    console.log('idCards---', idCards)
    // 获取user实体存储库
    const userRepository = AppDataSource.getRepository(User)
    const users = await userRepository.find({
    relations: { card: true },
    })
    console.log('users---', users)
    }
    main()

    级联处理

    前面我们保存User和IdCard其实是分别用了两个存储库去处理,但是这两个有关联的数据,我们完全可以进行级联更新或者删除。只需要在实体类中加上级联处理就行:

    @Entity('user')
    export class User {
    // ......省略
    @OneToOne(() => IdCard, (idCard) => idCard.user, {
    cascade: true,
    })
    card: IdCard
    }

    还是之前的代码,但是现在我们只需要使用User库就能直接进行保存

    async function main() {
    await AppDataSource.initialize()
    // 创建一个用户
    const user = new User()
    user.name = 'lilei'
    user.nickname = '李雷'
    user.phone = '13366666666'
    user.age = 18
    user.desc = '......'
    // 创建一个身份证信息
    const idCard = new IdCard()
    idCard.cardNo = '123456789012345678'
    idCard.name = 'lilei'
    idCard.address = '北京市朝阳区'
    idCard.birthday = new Date('2000-02-02')
    idCard.email = 'lilei2000@163.com'
    // 注意这里需要通过user来关联idCard
    user.card = idCard
    // 获取实体存储库
    const userRepository = AppDataSource.getRepository(User)
    // 直接保存用户信息即可关联保存idCard信息
    await userRepository.save(user)
    console.log('保存用户信息成功...', user)
    }
    main()

    那如果是更新呢?其实还是一样的。

    async function main() {
    await AppDataSource.initialize()
    // 获取user实体存储库
    const userRepository = AppDataSource.getRepository(User)
    const loadUser = await userRepository.findOne({
    relations: { card: true },
    where: { id: 1 },
    })
    console.log('user:', loadUser)
    loadUser.name = 'lucy'
    loadUser.nickname = '露西'
    loadUser.phone = '13377777777'
    loadUser.age = 20
    loadUser.card.name = 'lucy'
    loadUser.card.cardNo = '999999999999999'
    loadUser.card.address = '成都市武侯区'
    loadUser.card.birthday = new Date('2000-03-03')
    loadUser.card.email = 'lucy2000@163.com'
    const result = await userRepository.save(loadUser)
    console.log(result)
    }
    main()

    一对多/多对一关系

    一对多/多对一关系是我们最常见的关系,班级和学生,部门和员工,可以拿这些简单的内容来举例。比如部门可员工(一个部门可以有多个员工,一个员工只属于一个部门)

    import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm'
    import { Employee } from './Employee'
    @Entity()
    export class Department {
    @PrimaryGeneratedColumn()
    id: number
    @Column({ length: 100 })
    name: string
    @Column()
    desc: string
    @OneToMany(() => Employee, (employee) => employee.department, {
    cascade: true,
    })
    employees: Employee[]
    }

    使用@OneToMany装饰器来定义与员工表的关系,并设置级联(cascade)选项用来自动更新相关联的Employee记录。由于是多,所以employee肯定是一个数组。

    不过@OneToMany并不能单独使用,它必须与@ManyToOne装饰器配对使用,以确保关系的双向性。

    import {
    Entity,
    PrimaryGeneratedColumn,
    Column,
    ManyToOne,
    JoinColumn,
    } from 'typeorm'
    import { Department } from './Department'
    @Entity()
    export class Employee {
    @PrimaryGeneratedColumn()
    id: number
    @Column({ length: 100 })
    name: string
    @ManyToOne(() => Department, (department) => department.employees)
    @JoinColumn()
    department: Department
    }

    上面使用**@JoinColumn装饰器定义了外键列,在一对多/多对一关系中,外键列肯定会设置在”多”的一方**

    实体定义完成之后,在index.ts中插入数据:

    async function main() {
    await AppDataSource.initialize()
    const depart = new Department()
    depart.name = '技术部'
    depart.desc = '技术部门'
    const emp1 = new Employee()
    emp1.name = '员工1'
    const emp2 = new Employee()
    emp2.name = '员工2'
    // 设置部门和员工的关联关系
    depart.employees = [emp1, emp2]
    const departmentRepository = AppDataSource.getRepository(Department)
    const result = await departmentRepository.save(depart)
    console.log(result)
    }
    main()

    多对多关系

    这里我们使用订单和商品的关系,一个订单可以包含多件商品,一件商品也可以出现在多个订单中。首先,当然还是需要先定义实体。

    import { Entity, PrimaryGeneratedColumn, Column, ManyToMany } from 'typeorm'
    import { Product } from './Product'
    @Entity()
    export class Order {
    @PrimaryGeneratedColumn()
    id: number
    @Column({ length: 100 })
    name: string
    @ManyToMany(() => Product, (product) => product.orders, {
    cascade: true,
    })
    products: Product[]
    }
    import {
    Entity,
    PrimaryGeneratedColumn,
    Column,
    ManyToMany,
    JoinTable,
    } from 'typeorm'
    import { Order } from './Order'
    @Entity()
    export class Product {
    @PrimaryGeneratedColumn()
    id: number
    @Column({ length: 100 })
    name: string
    @ManyToMany(() => Order, (order) => order.products)
    @JoinTable()
    orders: Order[]
    }

    这里使用@JoinTable()来生成中间表,TypeORM会创建一个中间表来管理多对多关系。

    在index.ts中添加数据来看看效果

    async function main() {
    await AppDataSource.initialize()
    const order1 = new Order()
    order1.name = '订单1'
    const order2 = new Order()
    order2.name = '订单2'
    const product1 = new Product()
    product1.name = '产品1'
    const product2 = new Product()
    product2.name = '产品2'
    const product3 = new Product()
    product3.name = '产品3'
    const product4 = new Product()
    product4.name = '产品4'
    order1.products = [product1, product2, product3]
    order2.products = [product1, product2, product3, product4]
    const orderRepository = AppDataSource.getRepository(Order)
    await orderRepository.save(order1)
    await orderRepository.save(order2)
    }
    main()

    上面的代码会自动帮我们生成一张中间表product_orders_order,而且生成了productId和orderId作为复合主键,并插入数据:

    image-20250203113105685

    image-20250203113301771

    除此之外,我们还可以自行定义中间表的名称和列名

    @Entity()
    export class Product {
    @PrimaryGeneratedColumn()
    id: number
    @Column({ length: 100 })
    name: string
    @ManyToMany(() => Order, (order) => order.products)
    @JoinTable({
    name: 'order_products',
    joinColumn: {
    name: 'order_id',
    referencedColumnName: 'id',
    },
    inverseJoinColumn: {
    name: 'product_id',
    referencedColumnName: 'id',
    },
    })
    orders: Order[]
    }

    image-20250203182806956

    四、在Nest中使用TypeORM

    掌握了TypeORM的基本用法之后,在Nest中集成TypeORM就特别简单了。

    我们先重新创建一个nest工程

    Terminal window
    nest n nest-typeorm-mysql -p -pnpm

    为了方便,我们直接创建一个User模块

    Terminal window
    nest g res user

    然后导入我们需要的TypeORM相关的包

    Terminal window
    pnpm add typeorm mysql2 @nestjs/typeorm -s

    TypeORM提供了初始化数据库连接的方式,在app.module.ts文件中引入

    @Module({
    imports: [
    TypeOrmModule.forRoot({
    type: 'mysql',
    host: 'localhost',
    username: 'root',
    port: 3306,
    password: '123456',
    database: 'nest_typeorm',
    entities: [__dirname + '/**/*.entity{.ts,.js}'],
    synchronize: true,
    timezone: 'Z',
    }),
    UserModule,
    ],
    controllers: [AppController],
    providers: [AppService],
    })
    export class AppModule {}

    **注意:**这里是为了方便,打开了synchronize: true,会自动加载或更新数据库结构匹配实体定义。但是在生产环境中,我们肯定要禁用同步,不然随便就改变了数据库结构这是非常不安全的。

    由于docker中mysql数据库我们并没有处理时区问题,这样存储进去的时间类型,反馈给前端的时候就会出现东8区的时间差问题。

    上面的代码通过forRoot方法在根模块(AppModule)中注册了TypeORM模块。这样可以使得TypeORM服务在应用程序的各个模块中都可以共享使用,不过需要注意,这里需要node版本在20以上。

    完善一下User实体,新增一些字段

    import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'
    @Entity()
    export class User {
    @PrimaryGeneratedColumn()
    id: number
    @Column()
    name: string
    @Column()
    sex: string
    @Column({
    type: 'timestamp',
    transformer: {
    to: (value: string | Date): Date => {
    if (typeof value === 'string') {
    return new Date(`${value}T00:00:00.000Z`)
    }
    return value
    },
    from: (value: Date): Date => {
    return value
    },
    },
    })
    birthday: Date
    }

    使用命令pnpm run start:dev启动服务,就能帮我们创建User表(注意:数据库需要自己手动创建)

    image-20250203185212181

    EntityManager操作实体

    我们可以通过nest直接注入EntityManager来操作实体,这里唯一需要注意的一点是,TypeORM是天生的Repository,所以,我们可以直接在Service层中注入EntityManager

    user.service.ts

    import { Injectable } from '@nestjs/common'
    import { CreateUserDto } from './dto/create-user.dto'
    import { UpdateUserDto } from './dto/update-user.dto'
    import { InjectEntityManager } from '@nestjs/typeorm'
    import { EntityManager } from 'typeorm'
    import { User } from './entities/user.entity'
    @Injectable()
    export class UserService {
    @InjectEntityManager()
    private entityManager: EntityManager
    async create(createUserDto: CreateUserDto) {
    const user = await this.entityManager.save(User, createUserDto)
    console.log(user)
    return user
    }
    async findAll() {
    return await this.entityManager.find(User)
    }
    async findOne(id: number) {
    return await this.entityManager.findOneBy(User, { id })
    }
    async update(id: number, updateUserDto: UpdateUserDto) {
    return await this.entityManager.save(User, { id, ...updateUserDto })
    }
    async remove(id: number) {
    return await this.entityManager.delete(User, { id })
    }
    }

    user.controller.ts

    import {
    Controller,
    Get,
    Post,
    Body,
    Patch,
    Param,
    Delete,
    } from '@nestjs/common'
    import { UserService } from './user.service'
    import { CreateUserDto } from './dto/create-user.dto'
    import { UpdateUserDto } from './dto/update-user.dto'
    @Controller('user')
    export class UserController {
    constructor(private readonly userService: UserService) {}
    @Post()
    create(@Body() createUserDto: CreateUserDto) {
    return this.userService.create(createUserDto)
    }
    @Get()
    findAll() {
    return this.userService.findAll()
    }
    @Get(':id')
    findOne(@Param('id') id: string) {
    return this.userService.findOne(+id)
    }
    @Patch(':id')
    update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
    return this.userService.update(+id, updateUserDto)
    }
    @Delete(':id')
    remove(@Param('id') id: string) {
    return this.userService.remove(+id)
    }
    }

    Repository操作实体

    除了使用EntityManager操作实体之外,同样可以使用Repository操作实体,其实在功能和使用上和EntityManager几乎一致,唯一的区别是,使用Repository不需要每次操作都传入实体对象

    使用EntityManager的小问题:

    image-20250203194409778

    我们注入Repository,替换一下就行:

    import { Injectable } from '@nestjs/common'
    import { CreateUserDto } from './dto/create-user.dto'
    import { UpdateUserDto } from './dto/update-user.dto'
    import { InjectRepository } from '@nestjs/typeorm'
    import { Repository } from 'typeorm'
    import { User } from './entities/user.entity'
    @Injectable()
    export class UserService {
    @InjectRepository(User)
    private userRepository: Repository<User>
    async create(createUserDto: CreateUserDto) {
    const user = await this.userRepository.save(createUserDto)
    return user
    }
    async findAll() {
    return await this.userRepository.find()
    }
    async findOne(id: number) {
    return await this.userRepository.findOneBy({ id })
    }
    async update(id: number, updateUserDto: UpdateUserDto) {
    return await this.userRepository.save({ id, ...updateUserDto })
    }
    async remove(id: number) {
    return await this.userRepository.delete({ id })
    }
    }

    但是要注意的是:由于Nest的依赖注入作用域限定在模块级别,因此userRepository存储库还需要在User模块中引入并注册存储库,所以,在user.module.ts文件中,还需要添加如下的代码:

    @Module({
    imports: [TypeOrmModule.forFeature([User])],
    controllers: [UserController],
    providers: [UserService],
    })
    export class UserModule {}

    QueryBuilder操作实体

    当然,如果我们有比较复杂的SQL语句,还能使用QueryBuilder对实体进行操作。比如有下面的查询,需要根据姓名和性别进行查询,如果使用Repository我们可能会写成下面这个样子

    async create(createUserDto: CreateUserDto) {
    const user = await this.userRepository.save(createUserDto);
    return user;
    }
    async findAll(name: string, sex: string) {
    return await this.userRepository.find({
    where: {
    name,
    sex,
    },
    });
    }

    为了查询方便,controller中稍微做一下修改:

    @Post()
    create(@Body() createUserDto: CreateUserDto) {
    return this.userService.create(createUserDto);
    }
    @Get()
    findAll(@Query('name') name: string, @Query('sex') sex: string) {
    return this.userService.findAll(name, sex);
    }

    如果我们已经有下面的数据

    image-20250204124419712

    经过上面的查询,实际上where中的连接条件是and

    查询之后的结果如下:

    image-20250204124534146

    如果我们希望使用or进行连接,就可以使用QueryBuilder

    async create(createUserDto: CreateUserDto) {
    // const user = await this.userRepository.save(createUserDto);
    const user = await this.userRepository
    .createQueryBuilder()
    .insert()
    .into(User)
    .values(createUserDto)
    .execute();
    return user;
    }
    async findAll(name: string, sex: string) {
    // return await this.userRepository.find({
    // where: {
    // name,
    // sex,
    // },
    // });
    return this.userRepository
    .createQueryBuilder('u')
    .where('u.name = :name or u.sex = :sex', { name, sex })
    .getMany();
    }

    查询出来的结果:

    image-20250204125206831

    返回 BlogNest
    上一篇mysql相关回顾下一篇快速上手Redis
    Skip toc
    • 一、对于ORM的理解
    • 二、TypeORM
      • 直接开始
      • 创建模型及实体
      • 连接数据库
      • 使用实体管理器操作数据库
      • 使用Repository操作CRUD
      • 使用QueryBuilder操作CRUD
    • 三、TypeORM处理多表关系
      • 一对一关系
      • 一对多/多对一关系
      • 多对多关系
    • 四、在Nest中使用TypeORM
      • EntityManager操作实体
      • Repository操作实体
      • QueryBuilder操作实体
    Table of Contents
    • 一、对于ORM的理解
    • 二、TypeORM
      • 直接开始
      • 创建模型及实体
      • 连接数据库
      • 使用实体管理器操作数据库
      • 使用Repository操作CRUD
      • 使用QueryBuilder操作CRUD
    • 三、TypeORM处理多表关系
      • 一对一关系
      • 一对多/多对一关系
      • 多对多关系
    • 四、在Nest中使用TypeORM
      • EntityManager操作实体
      • Repository操作实体
      • QueryBuilder操作实体
    > share on twitter