ภาคต่อและซินาตร้าแก้ปัญหา API ของ Ruby ได้อย่างไร
เผยแพร่แล้ว: 2022-03-11บทนำ
ในช่วงไม่กี่ปีที่ผ่านมา จำนวนเฟรมเวิร์กแอปพลิเคชันหน้าเดียวของ JavaScript และแอปพลิเคชันมือถือเพิ่มขึ้นอย่างมาก สิ่งนี้ทำให้ความต้องการ API ฝั่งเซิร์ฟเวอร์เพิ่มขึ้นตามลำดับ เนื่องจาก Ruby on Rails เป็นหนึ่งในเฟรมเวิร์กการพัฒนาเว็บที่ได้รับความนิยมมากที่สุดในปัจจุบัน จึงเป็นตัวเลือกที่เป็นธรรมชาติสำหรับนักพัฒนาหลายๆ คนสำหรับการสร้างแอปพลิเคชัน API แบ็คเอนด์
แม้ว่ากระบวนทัศน์สถาปัตยกรรม Ruby on Rails ทำให้การสร้างแอปพลิเคชัน API แบ็คเอนด์ค่อนข้างง่าย การใช้ Rails สำหรับ API เพียงอย่างเดียว นั้นเกินความสามารถ อันที่จริง มันเกินความสามารถจนถึงจุดที่แม้แต่ทีม Rails ก็รู้เรื่องนี้และด้วยเหตุนี้จึงได้เปิดตัวโหมด API เฉพาะใหม่ในเวอร์ชัน 5 ด้วยคุณสมบัติใหม่นี้ใน Ruby on Rails การสร้างแอปพลิเคชันเฉพาะ API ใน Rails กลายเป็นเรื่องง่ายยิ่งขึ้น และทางเลือกที่ดีกว่า
แต่มีตัวเลือกอื่นด้วย สิ่งที่น่าสังเกตมากที่สุดคืออัญมณีที่เติบโตเต็มที่และทรงพลังสองชิ้น ซึ่งเมื่อรวมกันแล้วจะมอบเครื่องมืออันทรงพลังสำหรับการสร้าง API ฝั่งเซิร์ฟเวอร์ พวกเขาคือซินาตราและภาคต่อ
อัญมณีทั้งสองนี้มีชุดคุณลักษณะที่สมบูรณ์มาก: Sinatra ทำหน้าที่เป็นภาษาเฉพาะโดเมน (DSL) สำหรับเว็บแอปพลิเคชัน และ Sequel ทำหน้าที่เป็นเลเยอร์การทำแผนที่เชิงวัตถุ (ORM) ลองมาดูแต่ละอย่างคร่าวๆ
ซินาตรา
Sinatra เป็นเฟรมเวิร์กเว็บแอปพลิเคชันแบบแร็ค Rack เป็นอินเทอร์เฟซเว็บเซิร์ฟเวอร์ Ruby ที่รู้จักกันดี มันถูกใช้โดยเฟรมเวิร์กมากมาย เช่น Ruby on Rails และรองรับเว็บเซิร์ฟเวอร์จำนวนมาก เช่น WEBrick, Thin หรือ Puma Sinatra มีอินเทอร์เฟซขั้นต่ำสำหรับการเขียนเว็บแอปพลิเคชันใน Ruby และหนึ่งในคุณสมบัติที่น่าสนใจที่สุดคือการรองรับส่วนประกอบมิดเดิลแวร์ ส่วนประกอบเหล่านี้อยู่ระหว่างแอปพลิเคชันและเว็บเซิร์ฟเวอร์ และสามารถตรวจสอบและจัดการคำขอและการตอบสนองได้
สำหรับการใช้งานคุณลักษณะ Rack นี้ Sinatra กำหนด DSL ภายในสำหรับการสร้างเว็บแอปพลิเคชัน ปรัชญานั้นง่ายมาก: เส้นทางถูกแสดงโดยวิธี HTTP ตามด้วยเส้นทางที่ตรงกับรูปแบบ บล็อก Ruby ที่ประมวลผลคำขอและตอบกลับ
get '/' do 'Hello from sinatra' end
รูปแบบการจับคู่เส้นทางสามารถรวมพารามิเตอร์ที่มีชื่อได้ เมื่อดำเนินการบล็อกเส้นทาง ค่าพารามิเตอร์จะถูกส่งไปยังบล็อกผ่านตัวแปร params
get '/players/:sport_id' do # Parameter value accessible through params[:sport_id] end
รูปแบบการจับคู่สามารถใช้ตัวดำเนินการ splat *
ซึ่งทำให้ค่าพารามิเตอร์พร้อมใช้งานผ่าน params[:splat]
get '/players/*/:year' do # /players/performances/2016 # Parameters - params['splat'] -> ['performances'], params[:year] -> 2016 end
นี่ไม่ใช่จุดสิ้นสุดของความเป็นไปได้ของซินาตราที่เกี่ยวข้องกับการจับคู่เส้นทาง สามารถใช้ตรรกะการจับคู่ที่ซับซ้อนมากขึ้นผ่านนิพจน์ทั่วไป เช่นเดียวกับตัวจับคู่แบบกำหนดเอง
Sinatra เข้าใจกริยา HTTP มาตรฐานทั้งหมดที่จำเป็นสำหรับการสร้าง REST API: Get, Post, Put, Patch, Delete และ Options ลำดับความสำคัญของเส้นทางถูกกำหนดโดยลำดับที่กำหนดไว้ และเส้นทางแรกที่ตรงกับคำขอคือเส้นทางที่ให้บริการคำขอนั้น
แอปพลิเคชัน Sinatra สามารถเขียนได้สองวิธี โดยใช้สไตล์คลาสสิกหรือโมดูลาร์ ความแตกต่างหลักระหว่างพวกเขาคือ ด้วยรูปแบบคลาสสิก เราสามารถมีซินาตร้าแอปพลิเคชั่นเดียวต่อกระบวนการรูบี้ ความแตกต่างอื่นๆ นั้นเล็กน้อยพอที่ ในกรณีส่วนใหญ่ พวกเขาสามารถละเลยได้ และสามารถใช้การตั้งค่าเริ่มต้นได้
วิธีการแบบคลาสสิก
การใช้งานแอปพลิเคชันแบบคลาสสิกนั้นตรงไปตรงมา เราแค่ต้องโหลด Sinatra และใช้ตัวจัดการเส้นทาง:
require 'sinatra' get '/' do 'Hello from Sinatra' end
โดยการบันทึกโค้ดนี้ลงในไฟล์ demo_api_classic.rb
เราสามารถเริ่มแอปพลิเคชันได้โดยตรงโดยดำเนินการคำสั่งต่อไปนี้:
ruby demo_api_classic.rb
อย่างไรก็ตาม หากแอปพลิเคชันถูกปรับใช้กับตัวจัดการแร็ค เช่น Passenger จะเป็นการดีกว่าที่จะเริ่มต้นด้วยไฟล์ config.ru
คอนฟิกูเรชันของ Rack
require './demo_api_classic' run Sinatra::Application
ด้วยไฟล์ config.ru
แอปพลิเคชันจะเริ่มต้นด้วยคำสั่งต่อไปนี้:
rackup config.ru
วิธีการแบบแยกส่วน
แอปพลิเคชัน Modular Sinatra ถูกสร้างขึ้นโดยการจัดประเภทย่อย Sinatra::Base
หรือ Sinatra::Application
:
require 'sinatra' class DemoApi < Sinatra::Application # Application code run! if app_file == $0 end
คำสั่งเริ่มต้นด้วย run!
ใช้สำหรับเริ่มต้นแอปพลิเคชันโดยตรงด้วย ruby demo_api.rb
เช่นเดียวกับแอปพลิเคชันคลาสสิก ในทางกลับกัน หากแอปพลิเคชันถูกปรับใช้กับ Rack เนื้อหาตัวจัดการของ rackup.ru
จะต้อง:
require './demo_api' run DemoApi
ภาคต่อ
ผลสืบเนื่องเป็นเครื่องมือที่สองในชุดนี้ ตรงกันข้ามกับ ActiveRecord ซึ่งเป็นส่วนหนึ่งของ Ruby on Rails การพึ่งพาของ Sequel นั้นน้อยมาก ในขณะเดียวกันก็มีคุณสมบัติที่หลากหลายและสามารถใช้สำหรับการจัดการฐานข้อมูลทุกประเภท ด้วยภาษาเฉพาะโดเมนที่เรียบง่าย Sequel ช่วยนักพัฒนาจากปัญหาทั้งหมดเกี่ยวกับการรักษาการเชื่อมต่อ การสร้างการสืบค้น SQL การดึงข้อมูลจาก (และการส่งข้อมูลกลับไปยัง) ฐานข้อมูล
ตัวอย่างเช่น การสร้างการเชื่อมต่อกับฐานข้อมูลนั้นง่ายมาก:
DB = Sequel.connect(adapter: :postgres, database: 'my_db', host: 'localhost', user: 'db_user')
วิธีการเชื่อมต่อจะส่งคืนวัตถุฐานข้อมูล ในกรณีนี้คือ Sequel::Postgres::Database
ซึ่งสามารถใช้เพิ่มเติมเพื่อดำเนินการ SQL แบบดิบได้
DB['select count(*) from players']
หรือเพื่อสร้างออบเจ็กต์ชุดข้อมูลใหม่:
DB[:players]
คำสั่งทั้งสองนี้สร้างอ็อบเจ็กต์ชุดข้อมูล ซึ่งเป็นเอนทิตี Sequel พื้นฐาน
คุณลักษณะชุดข้อมูล Sequel ที่สำคัญที่สุดอย่างหนึ่งคือจะไม่ดำเนินการสืบค้นข้อมูลในทันที ทำให้สามารถจัดเก็บชุดข้อมูลเพื่อใช้ในภายหลัง และในกรณีส่วนใหญ่ จะเชื่อมโยงชุดข้อมูลเหล่านี้
users = DB[:players].where(sport: 'tennis')
ดังนั้นถ้าชุดข้อมูลไม่เข้าฐานข้อมูลทันที คำถามคือ เมื่อไหร่จะถึง? ผลสืบเนื่องรัน SQL บนฐานข้อมูลเมื่อมีการใช้ "วิธีปฏิบัติการ" ที่เรียกว่า วิธีการเหล่านี้คือ all
each
รายการ map
first
และ last
ผลสืบเนื่องสามารถขยายได้และความสามารถในการขยายเป็นผลมาจากการตัดสินใจทางสถาปัตยกรรมขั้นพื้นฐานเพื่อสร้างแกนกลางขนาดเล็กที่เสริมด้วยระบบปลั๊กอิน เพิ่มคุณสมบัติอย่างง่ายดายผ่านปลั๊กอินซึ่งอันที่จริงแล้วคือโมดูล Ruby ปลั๊กอินที่สำคัญที่สุดคือปลั๊กอิน Model
เป็นปลั๊กอินเปล่าที่ไม่ได้กำหนดวิธีการของคลาสหรืออินสแตนซ์ใด ๆ ด้วยตัวเอง แต่จะรวมปลั๊กอินอื่น ๆ (โมดูลย่อย) ซึ่งกำหนดคลาส อินสแตนซ์ หรือเมธอดชุดข้อมูลของโมเดล ปลั๊กอิน Model ช่วยให้สามารถใช้ Sequel เป็นเครื่องมือในการแมปเชิงวัตถุ (ORM) และมักถูกเรียกว่า "ปลั๊กอินฐาน"
class Player < Sequel::Model end
โมเดล Sequel จะแยกวิเคราะห์สคีมาฐานข้อมูลโดยอัตโนมัติ และตั้งค่าเมธอด accessor ที่จำเป็นทั้งหมดสำหรับทุกคอลัมน์ ถือว่าชื่อตารางเป็นพหูพจน์และเป็นเวอร์ชันที่ขีดเส้นใต้ของชื่อรุ่น ในกรณีที่มีความจำเป็นต้องทำงานกับฐานข้อมูลที่ไม่เป็นไปตามหลักการตั้งชื่อนี้ สามารถตั้งชื่อตารางได้อย่างชัดเจนเมื่อกำหนดแบบจำลอง
class Player < Sequel::Model(:player) end
ตอนนี้เรามีทุกอย่างที่จำเป็นในการเริ่มสร้าง back-end API
การสร้าง API
โครงสร้างรหัส
ตรงกันข้ามกับ Rails ซินาตราไม่ได้กำหนดโครงสร้างโครงการใดๆ อย่างไรก็ตาม เนื่องจากเป็นแนวปฏิบัติที่ดีเสมอที่จะจัดระเบียบโค้ดเพื่อการบำรุงรักษาและการพัฒนาที่ง่ายขึ้น เราจะทำที่นี่ด้วยโครงสร้างไดเร็กทอรีต่อไปนี้:

project root |-config |-helpers |-models |-routes
การกำหนดค่าแอปพลิเคชันจะถูกโหลดจากไฟล์การกำหนดค่า YAML สำหรับสภาพแวดล้อมปัจจุบันด้วย:
Sinatra::Application.config_file File.join(File.dirname(__FILE__), 'config', "#{Sinatra::Application.settings.environment}_config.yml")
โดยค่าเริ่มต้น ค่า Sinatra::Applicationsettings.environment
คือ development,
และมีการเปลี่ยนแปลงโดยการตั้งค่าตัวแปรสภาพแวดล้อม RACK_ENV
นอกจากนี้ แอปพลิเคชันของเราต้องโหลดไฟล์ทั้งหมดจากอีกสามไดเร็กทอรี เราสามารถทำได้ง่ายๆ โดยการวิ่ง:
%w{helpers models routes}.each {|dir| Dir.glob("#{dir}/*.rb", &method(:require))}
เมื่อมองแวบแรก วิธีการโหลดนี้อาจดูสะดวก อย่างไรก็ตาม ด้วยโค้ดบรรทัดเดียวนี้ เราไม่สามารถข้ามไฟล์ได้ง่ายๆ เพราะโค้ดนี้จะโหลดไฟล์ทั้งหมดจากไดเร็กทอรีในอาร์เรย์ นั่นเป็นเหตุผลที่เราจะใช้วิธีโหลดไฟล์เดียวที่มีประสิทธิภาพมากขึ้น ซึ่งถือว่าในแต่ละโฟลเดอร์เรามีไฟล์รายการ init.rb
ซึ่งโหลดไฟล์อื่นๆ ทั้งหมดจากไดเร็กทอรี นอกจากนี้ เราจะเพิ่มไดเร็กทอรีเป้าหมายไปยังเส้นทางการโหลด Ruby:
%w{helpers models routes}.each do |dir| $LOAD_PATH << File.expand_path('.', File.join(File.dirname(__FILE__), dir)) require File.join(dir, 'init') end
วิธีนี้ต้องใช้การทำงานเพิ่มขึ้นเล็กน้อย เนื่องจากเราต้องรักษาคำสั่ง require ในไฟล์ init.rb
แต่ละไฟล์ แต่ในทางกลับกัน เราสามารถควบคุมได้มากขึ้น และเราสามารถปล่อยไฟล์หนึ่งไฟล์ขึ้นไปได้อย่างง่ายดายโดยลบไฟล์ออกจากไฟล์ manifest init.rb
ในไดเรกทอรีเป้าหมาย
การตรวจสอบ API
สิ่งแรกที่เราต้องการในแต่ละ API คือการรับรองความถูกต้อง เราจะนำไปใช้เป็นโมดูลตัวช่วย ตรรกะการตรวจสอบสิทธิ์ที่สมบูรณ์จะอยู่ในไฟล์ helpers/authentication.rb
require 'multi_json' module Sinatra module Authentication def authenticate! client_id = request['client_id'] client_secret = request['client_secret'] # Authenticate client here halt 401, MultiJson.dump({message: "You are not authorized to access this resource"}) unless authenticated? end def current_client @current_client end def authenticated? !current_client.nil? end end helpers Authentication end
สิ่งที่เราต้องทำตอนนี้คือโหลดไฟล์นี้โดยเพิ่มคำสั่ง require ในไฟล์รายการตัวช่วย ( helpers/init.rb
) และเรียกการ authenticate!
วิธีใน Sinatra's before
hook ซึ่งจะถูกดำเนินการก่อนดำเนินการตามคำขอใด ๆ
before do authenticate! end
ฐานข้อมูล
ต่อไปเราต้องเตรียมฐานข้อมูลของเราสำหรับการสมัคร มีหลายวิธีในการเตรียมฐานข้อมูล แต่เนื่องจากเราใช้ Sequel จึงเป็นเรื่องปกติที่จะทำโดยใช้ผู้ย้ายข้อมูล ผลสืบเนื่องมาพร้อมกับผู้ย้ายข้อมูลสองประเภท - ตามจำนวนเต็มและการประทับเวลา แต่ละคนมีข้อดีและข้อเสีย ในตัวอย่างของเรา เราตัดสินใจใช้ตัวย้ายการประทับเวลาของ Sequel ซึ่งกำหนดให้ไฟล์การย้ายต้องมีการประทับเวลานำหน้า ตัวย้ายการประทับเวลามีความยืดหยุ่นสูงและยอมรับรูปแบบการประทับเวลาต่างๆ ได้ แต่เราจะใช้เฉพาะรูปแบบที่ประกอบด้วยปี เดือน วัน ชั่วโมง นาที และวินาทีเท่านั้น นี่คือไฟล์การโยกย้ายสองไฟล์ของเรา:
# db/migrations/20160710094000_sports.rb Sequel.migration do change do create_table(:sports) do primary_key :id String :name, :null => false end end end # db/migrations/20160710094100_players.rb Sequel.migration do change do create_table(:players) do primary_key :id String :name, :null => false foreign_key :sport_id, :sports end end end
ตอนนี้เราพร้อมที่จะสร้างฐานข้อมูลพร้อมตารางทั้งหมดแล้ว
bundle exec sequel -m db/migrations sqlite://db/development.sqlite3
สุดท้าย เรามีไฟล์ model sport.rb
และ player.rb
ในไดเร็กทอรี models
# models/sport.rb class Sport < Sequel::Model one_to_many :players def to_api { id: id, name: name } end end # models/player.rb class Player < Sequel::Model many_to_one :sport def to_api { id: id, name: name, sport_id: sport_id } end end
ในที่นี้ เรากำลังใช้วิธีที่เป็นผลสืบเนื่องในการกำหนดความสัมพันธ์ของแบบจำลอง โดยที่วัตถุ Sport
มีผู้เล่นหลายคน และ Player
สามารถมีกีฬาได้เพียงประเภทเดียว นอกจากนี้ แต่ละโมเดลยังกำหนดเมธอด to_api
ซึ่งส่งคืนแฮชพร้อมแอตทริบิวต์ที่จำเป็นต้องทำให้เป็นอนุกรม นี่เป็นแนวทางทั่วไปที่เราสามารถใช้ได้กับรูปแบบต่างๆ อย่างไรก็ตาม หากเราจะใช้รูปแบบ JSON ใน API ของเราเท่านั้น เราสามารถใช้ to_json
ของ Ruby โดยมีอาร์กิวเมนต์ only
เพื่อจำกัดการทำให้เป็นอนุกรมสำหรับแอตทริบิวต์ที่จำเป็น เช่น player.to_json(only: [:id, :name, :sport_i])
แน่นอน เราสามารถกำหนด BaseModel
ที่สืบทอดจาก Sequel::Model
และกำหนดวิธีการ to_api
ที่เป็นค่าเริ่มต้น ซึ่งสืบทอดโมเดลทั้งหมดจากนั้นจึงสืบทอดได้
ตอนนี้ เราสามารถเริ่มใช้งานปลายทาง API จริงได้แล้ว
ปลายทาง API
เราจะเก็บคำจำกัดความของปลายทางทั้งหมดไว้ในไฟล์ภายในไดเร็กทอรี routes
เนื่องจากเราใช้ไฟล์ Manifest ในการโหลดไฟล์ เราจะจัดกลุ่มเส้นทางตามทรัพยากร (เช่น เก็บเส้นทางที่เกี่ยวข้องกับกีฬาไว้ในไฟล์ sports.rb
, เส้นทางของผู้เล่นทั้งหมดใน routes.rb
เป็นต้น)
# routes/sports.rb class DemoApi < Sinatra::Application get "/sports/?" do MultiJson.dump(Sport.all.map { |s| s.to_api }) end get "/sports/:id" do sport = Sport.where(id: params[:id]).first MultiJson.dump(sport ? sport.to_api : {}) end get "/sports/:id/players/?" do sport = Sport.where(id: params[:id]).first MultiJson.dump(sport ? sport.players.map { |p| p.to_api } : []) end end # routes/players.rb class DemoApi < Sinatra::Application get "/players/?" do MultiJson.dump(Player.all.map { |p| s.to_api }) end get "/players/:id/?" do player = Player.where(id: params[:id]).first MultiJson.dump(player ? player.to_api : {}) end end
เส้นทางที่ซ้อนกัน เช่นเดียวกับการรับผู้เล่นทั้งหมดภายในหนึ่ง sport /sports/:id/players
สามารถกำหนดได้โดยการวางร่วมกับเส้นทางอื่น หรือโดยการสร้างไฟล์ทรัพยากรแยกต่างหากที่จะมีเฉพาะเส้นทางที่ซ้อนกัน
ด้วยเส้นทางที่กำหนด แอปพลิเคชันพร้อมที่จะรับคำขอ:
curl -i -XGET 'http://localhost:9292/sports?client_id=<client_id>&client_secret=<client_secret>'
โปรดทราบว่าตามที่ระบบการตรวจสอบสิทธิ์ของแอปพลิเคชันกำหนดไว้ในไฟล์ helpers/authentication.rb
เรากำลังส่งข้อมูลรับรองโดยตรงในพารามิเตอร์คำขอ
บทสรุป
หลักการที่แสดงให้เห็นในแอปพลิเคชันตัวอย่างง่ายๆ นี้นำไปใช้กับแอปพลิเคชันส่วนหลังของ API มันไม่ได้ขึ้นอยู่กับสถาปัตยกรรม model-view-controller (MVC) แต่ยังคงแยกความรับผิดชอบอย่างชัดเจนในลักษณะเดียวกัน ตรรกะทางธุรกิจที่สมบูรณ์จะถูกเก็บไว้ในไฟล์แบบจำลองในขณะที่การจัดการคำขอจะทำในวิธีเส้นทางของซินาตรา ตรงกันข้ามกับสถาปัตยกรรม MVC ซึ่งใช้มุมมองเพื่อแสดงการตอบสนอง แอปพลิเคชันนี้ทำสิ่งนั้นในที่เดียวกับที่จัดการคำขอ - ในวิธีการกำหนดเส้นทาง ด้วยไฟล์ตัวช่วยใหม่ แอปพลิเคชันสามารถขยายได้อย่างง่ายดายเพื่อส่งการแบ่งหน้า หรือหากจำเป็น ให้จำกัดข้อมูลกลับไปยังผู้ใช้ในส่วนหัวของการตอบกลับ
ในท้ายที่สุด เราได้สร้าง API ที่สมบูรณ์ด้วยชุดเครื่องมือที่เรียบง่ายและไม่สูญเสียฟังก์ชันการทำงานใดๆ การพึ่งพาในจำนวนที่จำกัดช่วยให้มั่นใจได้ว่าแอปพลิเคชันจะโหลดและเริ่มทำงานได้เร็วกว่ามาก และมีหน่วยความจำน้อยกว่า Rails มาก ดังนั้น ครั้งต่อไปที่คุณเริ่มทำงานกับ API ใหม่ใน Ruby ให้พิจารณาใช้ Sinatra และ Sequel เนื่องจากเป็นเครื่องมือที่ทรงพลังมากสำหรับกรณีการใช้งานดังกล่าว