前言
我想了半天,該做什麼項目,基於筆者的數據庫知識羸弱,怕一方面做前端一方面做後端會搞得四不像,又累時間又長。所以就想以做純 API 為目的,只做接口會不會更快一些呢
正文
筆者打算做一個全唐詩的 API 項目,此項目只為學習 ruby on rails web 開發並部署至服務器,會逐步從零開始到部署上線,部署手段會有些原始,不過沒事,下個項目筆者會升級部署手段
先新建一個 API 項目
rails new --api --database=postgresql --skip-test tangpoetry
意思是新建一個唐詩的 API 項目,數據庫為 postgresql,跳過測試
新建後進入項目,並更新 gem 下載源
cd tangpoetry
bundle config mirror.https://rubygems.org https://gems.ruby-china.com
重新下載依賴
bundle install --verbose # verbose:打印下載依賴過程
再去 config/database.yml 中修改開發環境時的數據庫
development:
<<: *default
database: tangpoetry_dev
username: tangpoetry
password: 123456
host: localhost
在此之前,需要在 pgAdmin4(postgresql圖形界面) 中創建 database、username、password 等,這裏不做贅述
我們在本地啓動服務
rails s
如此, rails 應用就啓動了
建立數據庫
筆者要做的是全唐詩的 API 接口,要有什麼功能先不説,起碼不會自己做數據,在網上找了一個 唐詩的數據庫,先導入 mysql 中,能看到它有兩個表
我們先根據表中的字段創建倆模型(Model)
rails g model poetry poet_id:integer content:text title:string
rails g model poet name:string
PS:模型(Model)需要是單數。id、created_at、updated_at 會自己創建
此時,就有個問題了,這個項目的 sql 是以 mysql 為語法而寫的,怎麼將它轉換為 postgresql 呢?
先不要管這個問題,來設計一下要做的 API
- 隨機獲取一首詩:/poetry/random
- 用詩的題目查詢:/poetry/title/靜夜思
- 列出這個詩人的所有詩:/poetry/author/李白
- 列出這個詩人的這首詩:/poetry/author/張若虛/title/春江花月夜
- 通過創作數量排名:/poet/list/createnum(沒做)
確定好要做的 API 後,我們就去實現,先在命令行中執行以下代碼來創建控制器(Controller)
rails g controller poetry random
# rails g controller 名字 動作
會生成這樣的文件:
以上命令的意思是説在創建一個名為 poetry 的類,它的方法為 random。rails 會幫你創建好類和方法以及在路由處創建一個 poetry/random 的路由
修改 poetry_controller.rb 中的內容:
class PoetryController < ApplicationController
def random
render json: { resource: 'hello, world'}
end
end
而後訪問 http://127.0.0.1:3000/poetry/random ,就能看到 json 格式的返回值了
訪問 url,應用匹配 route,routes 匹配 controller,controller 操作 model,並返回對應的數據給路由
現在我們要回到最開始的疑問,怎麼把全唐詩中的 sql 轉化為 postgresql?
筆者經過一些嘗試,發現可以轉換,數據是有的,數據結構也一致,無非原本是用 mysql 寫的,現在將其改成 postgresql。而現在我們已經有數據庫的兩張表了,只要插入數據即可,用 pgAdmin 也好,用其他工具也罷,用 postgresql 語法把數據插入數據庫中
在倉庫中分別提供 mysql 的數據tang_poetry.sql 和 postgresql 的數據 tangpoetry.sql
INSERT INTO poets (id,name,created_at,updated_at) VALUES (1,'李世民','2014-06-02 11:47:52','2014-06-02 11:47:52'),(...)
INSERT INTO poetries (id,poet_id,content,title, created_at, updated_at) VALUES (2,1,'塞外悲風切,交河冰已結。瀚海百重波,陰山千里雪。迥戍危烽火,層巒引高節。悠悠卷旆旌,飲馬出長城。寒沙連騎跡,朔吹斷邊聲。胡塵清玉塞,羌笛韻金鉦。絕漠干戈戢,車徒振原隰。都尉反龍堆,將軍旋馬邑。揚麾氛霧靜,紀石功名立。荒裔一戎衣,靈台凱歌入。','飲馬長城窟行','2014-06-02 11:47:52','2014-06-02 11:47:52'),(...)
如何導入數據:使用 pgAdmin ,可以選擇要導入數據的數據庫、右鍵單擊該數據庫並選擇“Restore...”選項,之後選擇要導入的數據文件並執行導入操作
實現第一個接口
這個時候,數據庫中已有全唐詩的數據,我們現在要做第一個接口,即獲取一首隨機詩
首先是獲取隨機數,其次是根據這個 id 找到這一項
...
def random
random = Poetry.find(rand(1...43030))
render json: { resource: random}
end
...
ruby on rails 就是這麼簡單,這樣就完成了 random 接口
其餘API
除了 random 接口,我們還有四個接口,做好後再部署唐詩 API 項目就完成了,可謂是半小時完成一項目
- 用詩的題目查詢:/poetry/title/靜夜思
- 列出這個詩人的所有詩:/poetry/author/李白
- 列出這個詩人的這首詩:/poetry/author/張若虛/title/春江花月夜
- 通過創作數量排名:/poet/list/createnum
前三者都是在 poetry 路由下的訪問,我們先新建 routes
get 'poetry/random'
+get 'poetry/title/:title', to: 'poetry#title'
+get 'poetry/author/:author', to: 'poetry#author'
+get 'poetry/author/:author/title/:title', to: 'poetry#author_title'
意思是訪問 poetry/title/:title 路由,就是去 poetry_controller 中找 title 方法,並且有個 title 變量(其餘兩者相同道理)。再去 poetry_controller 文件,新建title、author、author_title 方法
class PoetryController < ApplicationController
def random
random = Poetry.find(rand(1...43030))
render json: { data: random }
end
# /poetry/title/靜夜思
def title
result = Poetry.find_by(title: params[:title])
render json: { data: result }
end
# /poetry/author/李白
def author
author = Poet.find_by(name: params[:author])
result = Poetry.where({poet_id: author[:id]})
render json: { data: result }
end
# /poetry/author/張若虛/title/春江花月夜
def author_title
author = Poet.find_by(name: params[:author])
result = Poetry.where({poet_id: author[:id], title: params[:title]})
render json: { data: result }
end
end
又搞定了三個接口,你就説快不快
其中,對 ORM 有所不瞭解,筆者是在 RailsGuides 查詢
Poet.find_by(name: params[:title])
# 只能找滿足條件的第一條
Poetry.where({poet_id: @author[:id]})
# where 條件查詢
# 找到滿足條件的所有項
接着偶們做最後一個接口,即通過創作數量排名:/poet/list/createnum
rails g controller poet list
或者不用命令行,直接在 routes.rb 上修改,並新建 poet_controller.rb 文件進行更新
——————————————
淦,卡住了
筆者這裏搜到一個相關的教程,奈何 sql 基礎太差,還是不會弄。這個接口就不做了
部署
本地開發結束了,現部署上線
之前我們能在 Heroku 快速部署,但現在它已經要收費了,所以筆者決定部署到服務器上,思路是:
先使用本地 docker 部署服務,本地跑通後,再上傳源碼,通過 Dockerfile 構建運行環境,在運行環境中運行源代碼
初試 tangpoetry 鏡像
我們先構建 Dockerfile,下面命令很好理解,就不過都解釋
FROM ruby:3.1.3
WORKDIR /app
COPY . .
RUN bundle config mirror.https://rubygems.org https://gems.ruby-china.com
ENV RAILS_ENV=production
RUN bundle config set --local without 'development test'
RUN bundle install
ENTRYPOINT bundle exec puma
然後將它打包成鏡像
docker build -t tangpoetry .
基於 tangpoetry 鏡像,生成容器
docker run -d --name tangpoetry_container -p 3000:3000 tangpoetry
# -d 後台啓動容器
# --name 容器名
# -p 端口映射
我們訪問(http://localhost:3000)首頁,是能看到 Hi 的
為了測試方便,我們新建一個根路由,返回一個 json:{ message: 'Hi' }
但是如果訪問所寫的任意接口,都會訪問不了,原因很簡單,因為 production 環境下的數據庫未配置,所以我們需要再建一個容器,並將唐詩數據導入到此容器中,再通過 docker network 連接兩個容器
也就是説我們的服務由兩個容器組成(後續可以的話可以通過 docker-compose 改造)
現在本地環境用的數據庫是本地下載了 postgreSQL,所以我們需要用 docker 啓動 postgresSQL 鏡像數據庫
創建基於 postgres 的容器
docker run -d --name db-for-tangpoetry -e POSTGRES_USER=tangpoetry -e POSTGRES_PASSWORD=123456 -e POSTGRES_DB=tangpoetry_production -e PGDATA=/var/lib/postgresql/data/pgdata -v tangpoetry-data:/var/lib/postgresql/data --network=network1 postgres:14
# -d 後台運行
# --name 容器名字叫 db-for-tangpoetry
# -e 環境命令
# -v 數據卷
# --network 使用網絡
這裏的數據卷和網絡都要事先建好
docker volume create tangpoetry-data:創建 tangpoetry-data 數據卷
docker volume ls可查看數據卷列表
docker network create network1創建 network1 網絡
docker network ls可查看網絡列表
進入(postgresSQL)數據庫容器
docker exec -it db-for-tangpoetry bash
連接 tangpoetry_production 數據庫
psql -h localhost -p 5432 -U tangpoetry tangpoetry_production
命令 \l 查看數據庫中的表
説明我們的數據庫創建成功,現在我們需要導出本地數據庫,並導入到 docker 鏡像數據庫中
先將本地的數據庫導出
pg_dump -U tangpoetry -d tangpoetry_dev > tangpoetry.sql
# pg_dump 導出數據
# -U 用户名
# -d 數據庫
再導入到db-for-tangpoetry 容器中
docker exec -i db-for-tangpoetry pg_restore -U tangpoetry -d tangpoetry_production < tangpoetry.sql
# pg_restore 導入數據
筆者輸入後顯示的如下:
重新編譯 tangpoetry 鏡像
我們需要將 tangpoetry 的源碼在修改下,配置 config/database.yml 中的 production:
production:
<<: *default
database: tangpoetry_production
username: tangpoetry
password: <%= ENV["DB_PASSWORD"] %>
host: <%= ENV["DB_HOST"] %>
再重新打包(再此之前先刪除原來的容器和鏡像)
docker build -t tangpoetry .
基於 tangpoetry 鏡像打包 tangpoetry 容器
docker run -d --name tangpoetry_container -p 3000:3000 -e DB_HOST=db-for-tangpoetry -e DB_PASSWORD=123456 --network=network1 tangpoetry
在容器中創建數據庫,並 migrate
docker exec -it tangpoetry_container bin/rails db:create db:migrate
這樣再訪問 3000 端口時,我們就能看到數據了
線上部署
以上我們已經測試成功了本地 docker 部署,先將它推到遠程 git 倉庫,後續我們會登錄服務器,並 git pull 它,然後構建 tangpoetry 鏡像,由此創建 tangpoetry_container 容器
這裏遇到的坑讓我白了四根頭髮,第三個問題困擾了我兩天並白了兩根為數不多的頭髮
問題一:ruby-china 443 證書過期
bundle install 時,gems 源會 443,提示類似這樣:
Retrying download gem from https://gems.ruby-china.com/ due to error (2/4): Gem::RemoteFetcher::FetchError Net::OpenTimeout: Failed to open TCP connection to gems.ruby-china.com:443 (execution expired) (https://gems.ruby-china.com/gems/racc-1.6.2.gem)
換成阿里源、清華源都不行,筆者第一次部署時「使用不換源」來解決,等了好久才下載好,後來看到這篇文章,先將本地的依賴下載好成緩存,再 bundle 時就從本地拿就好
簡單來説就兩步:
先在項目根目錄下執行以下命令
bundle cache
bundle lock --add-platform x86_64-linux
bundle package --all-platforms
(以上皆為部分截圖)
再修改 Dockerfile
...
# 添加緩存到app中,這裏其實是做了 docker 打包的優化,不做過多介紹
ADD vendor/cache /app/vendor/cache
...
# 通過本地下載依賴
RUN bundle install --local
...
當我們很快打包後 tangpoetry 鏡像後,我們就以它為依據來構建服務,這裏我們複製本地部署時的代碼docker run -d --name tangpoetry_container -p 3000:3000 -e DB_HOST=db-for-tangpoetry -e DB_PASSWORD=123456 --network=network1 tangpoetry,先做測試
此時,我們的服務器上的 postgres 容器還沒創建,我們先把 ruby on rails 服務部署成功了,再連接數據庫
問題二:secret_key_base 的報錯
但訪問服務器ip+端口,發現訪問不了
通過 docker logs tangpoetry_container 查看報錯日誌
説 production 環境下的 secret_key_base 不存在
淦,又有個知識點
什麼是secret_key_base?為什麼需要這個?為什麼本地部署時沒出現這個?
Rails 在項目初始化的時候就會在根目錄config 下生成 master.key 和 credentials.yml.enc 兩個文件,前者可以理解為核心密鑰,後者是通過 Rails 自帶的加密方法生成的加密後的數據文件
關係為:
master.key + keys => encrypted
encrypted + master.key => keys
keys 是什麼,你需要加密的數據,例如 secret_key_base
我們在臨時文件中的寫入我們的 keys,保存關閉後會生成一個新的 master.key 和 credentials.yml.enc ,並且臨時文件會自動刪除,把.enc 存在 git 中,master.key 排除在 git 外,這樣,別人即使拿到源碼,拿不到你的 keys(缺少 master.key 解不了)
如何讀取 keys 呢?
rails c
# 在命令行中輸入 rails c 或者 rails console
# 輸入代碼
Rails.application.credentials.config # 查看所有的 keys
Rails.application.credentials.secret_key_base #查到 secret_key_base
如何修改 keys 呢?
筆者使用的是 window,使用 window 自帶的 PowerShell,它能臨時寫進參數
$env:EDITOR="code --wait"
rails credentials:edit
此時會生成一個臨時文件,我們將 demo:12345 修改為 demo:123456,保存並刪除臨時文件,會發現文件 credentials.yml.enc 發生了變化
也就是説 master.key + keys 會生成一個新的credentials.yml.enc
同理,我們不能在本地和生成使用一套 keys,Rails 支持多環境密鑰
$env:EDITOR="code --wait"
rails credentials:edit --environment production
會得到兩個文件:
config/credentials/production.key (被加入 .gitignore)
config/credentials/production.yml.enc
我們只需要把 production.key 寫進服務器環境變量中,就能解決問題二的問題了
$env:RAILS_ENV="production"
rails c
Rails.application.credentials.secret_key_base
最佳實踐是什麼?
- 先刪除
master.key和credentials.yml.enc,通過rails credentials:edit生成一個新的 key 和 enc,複製臨時文件中的secret_key_base - 再
rails credentials:edit --environment production生成生產環境的臨時文件,粘貼上一步的secret_key_base - 再刪除
master.key和credentials.yml.enc,再生成一個新的 key 和 enc
如此,再服務器上將RAILS_MASTER_KEY 寫進環境變量中,
vim ~/.bash_profile
echo DB_HOST=db-for-tangpoetry
echo DB_PASSWORD=123456
echo RAILS_MASTER_KEY=f78c0868148ca3b1aa64ee9e82c66ef4
執行 source ~/.bash_profile 立即生效
再次啓動容器,此時將 DB_HOST 等用變量形式寫入
docker run -d --name tangpoetry_container --network network1 -p 3000:3000 -e DB_HOST=$DB_HOST -e DB_PASSWORD=$DB_PASSWORD -e RAILS_MASTER_KEY=$RAILS_MASTER_KEY tangpoetry
問題三:應用容器連接不上數據庫容器
在此之前,我們已經能在服務器ip+端口上能訪問到首頁,但是此時我們還沒啓動數據庫,所以還訪問不到數據庫
我們先啓動數據庫容器
docker run -d --name $DB_HOST --network network1 -p 5432:5432 -e POSTGRES_USER=tangpoetry -e POSTGRES_PASSWORD=$DB_PASSWORD -e POSTGRES_DB=tangpoetry_production -e PGDATA=/var/lib/postgresql/data/pgdata -v tangpoetry-data:/var/lib/postgresql/data postgres:14
並導入數據
docker exec -i db-for-tangpoetry pg_restore -U tangpoetry -d tangpoetry_production < tangpoetry.sql
回到應用容器,進入容器中,初始化數據庫
docker exec -it tangpoetry_container bash # 進入唐詩容器
# 進入容器後
rails db:create
發現訪問不了
也就是説容器之間的訪問成了問題
筆者找了很多資料,找了兩天還是沒有解決問題,也在 ruby china 上提問,終於在在這篇問答中找到了靈感,我升級了 docker 版本,從 19 升級到了 23,就解決了
以上的命令用以下命令就能實現
docker exec tangpoetry_container bash db:create db:migrate
如此,我們的項目就算成功上線了
如有問題可訪問項目地址:https://github.com/johanazhu/tangpoetry
後續
如果,我是説如果,我們希望加上隨機一頁的效果,或者説每天更新一首詩,本地開發,成功,推到 git 倉庫,並在服務器上刪除原有鏡像,生成新鏡像,再根據新鏡像打包
要是項目迭代頻繁,會不會覺得,好麻煩
這篇文章花了筆者很多時間,好不容易才上線
如現在2023年6月份,距離筆者完成初稿已經3個月,筆者也找到了新的免費的部署方式——fly.io
參考資料
- Active Record 基礎
- 山竹記賬全棧版Vue 3 + Rails 7+TSX
- 山竹記賬免費版
- How to run 'rails credentials:edit' on Windows 10 without installing a Linux Subsystem
- Why can't I connect to Postgres in Docker?
- Postgresql9.1 suddenly could not connect to server: No route to host
- Capistrano: ArgumentError: Missing secret_key_base for 'production' environment, set this string with bin/rails credentials:edit
- How to get a Docker container's IP address from the host
- Rails + PostgreSQL + Docker
- caching-all-native-gem-platforms
系列文章
- 前端學Ruby:前言
- 前端學 Ruby:安裝Ruby、Rails
- 前端學 Ruby:熟悉 Ruby 語法
- 前端學 Ruby:熟悉Rails