数据库适配
PostgreSQL/MySQL/SQLite。
数据库适配
这一课你将能做到
- 用
database.yml 配置三套环境,并把敏感信息挪到环境变量
- 把应用从 SQLite 切换到 MySQL/PostgreSQL,理解适配器差异
- 配置连接池、读写分离与多数据库,说清楚各自的适用场景
为什么要学
Rails 默认用 SQLite 起步,零配置很爽,但真实项目几乎必然上 MySQL 或 PostgreSQL(本站就是 MySQL)。数据库适配层的知识决定了你能否顺利完成「本地开发 → 团队 staging → 生产」的迁移,也决定了流量翻倍时你的应用是平滑扩容还是半夜宕机。连接池配置错误是生产事故的高发区。
先建立图像
Rails 连接数据库的层次:
你的代码 Article.find(1)
│
▼
Active Record(生成 SQL)
│
▼
适配器 Adapter(mysql2 / pg / sqlite3 gem)
│
▼
连接池(pool: 5,复用 TCP 连接)
│
▼
数据库服务器(MySQL 8 / PostgreSQL 16 / SQLite 文件)
三套环境各自独立:
| 环境 | 用途 | 数据 |
|---|
| development | 本地开发 | 真实感种子数据 |
| test | 跑测试 | 每次测试前清空,事务回滚 |
| production | 线上 | 真实用户数据 |
跟着做
步骤 1:读懂默认 database.yml
config/database.yml(SQLite 默认版):
default: &default
adapter: sqlite3
pool: <%= ENV.fetch("RAILS_MAX_THREADS", 5) %>
timeout: 5000
development:
<<: *default
database: storage/development.sqlite3
test:
<<: *default
database: storage/test.sqlite3
production:
<<: *default
database: storage/production.sqlite3
要点:default: &default 定义锚点,<<: *default 在各环境复用——DRY 的 YAML 技巧。pool 是连接池大小,<%= ENV.fetch(...) %> 说明 yml 里可以嵌 ERB 读环境变量。
步骤 2:切换到 MySQL
新项目直接指定:
rails new blog --database=mysql
已有项目改造——Gemfile 换驱动:
gem "mysql2", "~> 0.5" # 删掉 sqlite3
config/database.yml:
default: &default
adapter: mysql2
encoding: utf8mb4
pool: <%= ENV.fetch("RAILS_MAX_THREADS", 5) %>
username: <%= ENV.fetch("DB_USERNAME", "root") %>
password: <%= ENV.fetch("DB_PASSWORD", "") %>
host: <%= ENV.fetch("DB_HOST", "localhost") %>
development:
<<: *default
database: blog_development
test:
<<: *default
database: blog_test
production:
<<: *default
database: blog_production
bundle install
bin/rails db:create # 建库
bin/rails db:migrate # 建表
bin/rails db:seed # 灌种子数据
注意 utf8mb4:MySQL 的老 utf8 只支持 3 字节字符,存 emoji 会报错,utf8mb4 才是真 UTF-8。这是本站(Spring Boot + MySQL)同样踩过的坑。
步骤 3:生产环境用环境变量保密码
永远不要把生产密码提交进 Git。Rails 7 推荐两种方式:
方式一,环境变量直读(上面 yml 已演示),部署时在服务器上设:
export DB_PASSWORD="真实密码"
export RAILS_MASTER_KEY="config/master.key 的内容"
方式二,Rails 加密凭据(credentials):
bin/rails credentials:edit
# 编辑器里写入,保存后自动加密存到 config/credentials.yml.enc
db:
password: 真实密码
password: <%= Rails.application.credentials.dig(:db, :password) %>
credentials.yml.enc 可以安全提交 Git,解密钥匙 config/master.key 绝不提交。
步骤 4:连接池与 Puma 线程的关系
threads_count = ENV.fetch("RAILS_MAX_THREADS", 5)
threads threads_count, threads_count
workers ENV.fetch("WEB_CONCURRENCY", 2)
规则:连接池大小 ≥ 每个 worker 的线程数。每个线程处理请求时要独占一条数据库连接,池子比线程小,请求就得排队等连接,表现为「偶发超时」。总连接数 = workers × pool,还要给数据库 max_connections 留余量。算式:2 workers × 5 pool = 10 条连接,MySQL 默认 151 上限,绰绰有余。
步骤 5:多数据库与读写分离(了解即可)
production:
primary:
database: blog_production
host: master.db.internal
primary_replica:
database: blog_production
host: replica.db.internal
replica: true
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
connects_to database: { writing: :primary, reading: :primary_replica }
end
读多写少的场景(内容站、报表),从库扛查询流量,主库专心写。中小项目用不到,但面试常考。
讲清楚要点
- 适配器是 gem 不是框架内置:
mysql2、pg、sqlite3 都是独立 gem,Active Record 通过它们翻译 SQL 方言。换库 = 换 gem + 改 yml + 迁数据。
- test 环境必须独立库:测试框架每次运行清空数据(事务回滚),共用开发库会删掉你的调试数据。
- 密码不进 Git:用 ENV 或 credentials。检查仓库历史,
git log -p config/database.yml 确认从没提交过明文密码;提交过就立刻改密码。
- 池大小跟着线程走:Puma 线程数、Sidekiq 并发数都要 ≤ 对应进程的连接池,否则并发一高就
ConnectionTimeoutError。
- utf8mb4 是 MySQL 唯一正确的 UTF-8:建库时指定,Rails 的 mysql2 默认已帮你配好,但接手老库时要检查。
你可能会懵的地方
-
MySQL 和 PostgreSQL 怎么选?
都能打。PostgreSQL 功能更强(JSONB、全文搜索、地理 PostGIS、更严的 SQL 标准);MySQL 生态广、运维熟人多、托管服务便宜。团队已有 MySQL 经验(如本站)就用 MySQL,别为技术新鲜感换库。
-
SQLite 真的不能上生产吗?
Rails 官方近年态度转变:单服务器、读多写少的中小应用,SQLite 上生产完全可行(Litestack 方案),省掉运维成本。多机部署、高并发写入才必须上 MySQL/PG。
-
db:create 和 db:migrate 区别?
db:create 只建空库(CREATE DATABASE);db:migrate 在库里按迁移文件建表改表。新环境一把梭:bin/rails db:setup(create + schema 加载 + seed)。
-
schema.rb 和 structure.sql 区别?
默认 schema.rb 是 Ruby 方言的表结构快照,跨数据库通用。用到数据库特有功能(PG 的物化视图、触发器)时 schema.rb 表达不了,需切 config.active_record.schema_format = :sql,生成 structure.sql。
-
开发环境连远程数据库好吗?
不好。延迟高、误操作伤真实数据。开发用本地库,需要真实数据时做脱敏导出再导入。
错法对照
| 错法 | 问题 | 正解 |
|---|
| 生产密码明文写进 yml 并提交 | 密码随仓库泄露 | ENV 或 credentials,历史泄露立刻换密 |
| pool 默认 5 但 Puma 开 16 线程 | 高并发下连接等待超时 | pool ≥ 每进程线程数 |
| test 和 development 共用库 | 跑测试清空开发数据 | 三套环境三个库 |
| MySQL 用 utf8 编码 | emoji 写入报错 | utf8mb4 |
| 在 yml 里硬编码生产 host | 换服务器要改代码 | ENV.fetch("DB_HOST") |
卡点急救
Access denied for user:用户名密码错,或该用户没有对应库的权限。MySQL 里执行 GRANT ALL ON blog_development.* TO 'rails'@'localhost'; 并 FLUSH PRIVILEGES;。
Can't connect to MySQL server:MySQL 服务没启动;或 host 写了 localhost 但走了 socket 而 socket 路径不对——把 host 改成 127.0.0.1 强制走 TCP 试试。
- 测试报
database is locked(SQLite):测试并行/事务冲突,确认 test 库独立;MySQL 下常见原因是连接池耗尽,检查 pool 配置。
面试常问
-
Rails 三套环境的数据库如何隔离?为什么?
答:database.yml 里 development/test/production 各配独立 database。隔离原因:测试每次清空数据,混用会毁开发数据;生产更是物理隔离的真实数据。这是「环境即数据边界」的基本工程纪律。
-
连接池是干什么的?怎么定大小?
答:TCP 连接建立昂贵,池子预先建好一批连接复用。大小定式:≥ 单进程最大并发线程数(Puma 线程 / Sidekiq 并发),同时所有进程总连接数 < 数据库 max_connections。过大浪费数据库内存,过小请求排队。
-
明文密码提交过 Git 怎么办?
答:立刻改密码(改密是唯一能补救的动作);用 git filter-repo 或 BFG 清洗历史;通知团队强制更新仓库。预防:凭据一律走 ENV/credentials,code review 把 yml 变更列为重点审查项。
自测三问
pool: 5 和 Puma threads 16 同时存在会发生什么?怎么改?
- 为什么
credentials.yml.enc 能提交而 master.key 不能?
- MySQL 的
utf8 和 utf8mb4 差异是什么?
小结一句话
数据库适配的核心纪律:三套环境三个库、密码永不进 Git、连接池跟着线程走、MySQL 认准 utf8mb4。
延伸