Stories

Detail Return Return

前端學 Ruby:唐詩API項目 - Stories Detail

前言

我想了半天,該做什麼項目,基於筆者的數據庫知識羸弱,怕一方面做前端一方面做後端會搞得四不像,又累時間又長。所以就想以做純 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 應用就啓動了

ruby on 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 名字  動作

會生成這樣的文件:

生成的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 文件,新建titleauthorauthor_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鏡像

基於 tangpoetry 鏡像,生成容器

docker run -d --name tangpoetry_container -p 3000:3000 tangpoetry
# -d 後台啓動容器
# --name 容器名
# -p 端口映射

tangpoetry 容器

我們訪問(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 查看數據庫中的表

image-20230205141917349

説明我們的數據庫創建成功,現在我們需要導出本地數據庫,並導入到 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 

bundle cache

bundle lock

bundle package

(以上皆為部分截圖)

再修改 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 查看報錯日誌

docker logs tangpoetry_container 報錯

説 production 環境下的 secret_key_base 不存在

淦,又有個知識點

什麼是secret_key_base?為什麼需要這個?為什麼本地部署時沒出現這個?

Rails 在項目初始化的時候就會在根目錄config 下生成 master.keycredentials.yml.enc 兩個文件,前者可以理解為核心密鑰,後者是通過 Rails 自帶的加密方法生成的加密後的數據文件

關係為:

master.key + keys => encrypted
encrypted + master.key => keys

keys 是什麼,你需要加密的數據,例如 secret_key_base

我們在臨時文件中的寫入我們的 keys,保存關閉後會生成一個新的 master.keycredentials.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 

查看 credentials

如何修改 keys 呢?

筆者使用的是 window,使用 window 自帶的 PowerShell,它能臨時寫進參數

$env:EDITOR="code --wait"
rails credentials:edit

編寫credentials

此時會生成一個臨時文件,我們將 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

查看production下的密鑰

最佳實踐是什麼?

  • 先刪除 master.keycredentials.yml.enc,通過rails credentials:edit生成一個新的 key 和 enc,複製臨時文件中的 secret_key_base
  • rails credentials:edit --environment production 生成生產環境的臨時文件,粘貼上一步的 secret_key_base
  • 再刪除 master.keycredentials.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

發現訪問不了

rails db:create

也就是説容器之間的訪問成了問題

筆者找了很多資料,找了兩天還是沒有解決問題,也在 ruby china 上提問,終於在在這篇問答中找到了靈感,我升級了 docker 版本,從 19 升級到了 23,就解決了

rails db:create成功

以上的命令用以下命令就能實現

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
user avatar toopoo Avatar alibabawenyujishu Avatar vanve Avatar razyliang Avatar anchen_5c17815319fb5 Avatar Dream-new Avatar guixiangyyds Avatar yixiyidong Avatar kongsq Avatar milton Avatar jmix Avatar nzbin Avatar
Favorites 62 users favorite the story!
Favorites

Add a new Comments

Some HTML is okay.