ภาคต่อและซินาตร้าแก้ปัญหา 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) ลองมาดูแต่ละอย่างคร่าวๆ

API กับ Sinatra และภาคต่อ: Ruby Tutorial

Ruby API ในการควบคุมอาหาร: แนะนำ Sequel และ Sinatra
ทวีต

ซินาตรา

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 เรากำลังส่งข้อมูลรับรองโดยตรงในพารามิเตอร์คำขอ

ที่เกี่ยวข้อง: บทช่วยสอน Grape Gem: วิธีสร้าง REST-Like API ใน Ruby

บทสรุป

หลักการที่แสดงให้เห็นในแอปพลิเคชันตัวอย่างง่ายๆ นี้นำไปใช้กับแอปพลิเคชันส่วนหลังของ API มันไม่ได้ขึ้นอยู่กับสถาปัตยกรรม model-view-controller (MVC) แต่ยังคงแยกความรับผิดชอบอย่างชัดเจนในลักษณะเดียวกัน ตรรกะทางธุรกิจที่สมบูรณ์จะถูกเก็บไว้ในไฟล์แบบจำลองในขณะที่การจัดการคำขอจะทำในวิธีเส้นทางของซินาตรา ตรงกันข้ามกับสถาปัตยกรรม MVC ซึ่งใช้มุมมองเพื่อแสดงการตอบสนอง แอปพลิเคชันนี้ทำสิ่งนั้นในที่เดียวกับที่จัดการคำขอ - ในวิธีการกำหนดเส้นทาง ด้วยไฟล์ตัวช่วยใหม่ แอปพลิเคชันสามารถขยายได้อย่างง่ายดายเพื่อส่งการแบ่งหน้า หรือหากจำเป็น ให้จำกัดข้อมูลกลับไปยังผู้ใช้ในส่วนหัวของการตอบกลับ

ในท้ายที่สุด เราได้สร้าง API ที่สมบูรณ์ด้วยชุดเครื่องมือที่เรียบง่ายและไม่สูญเสียฟังก์ชันการทำงานใดๆ การพึ่งพาในจำนวนที่จำกัดช่วยให้มั่นใจได้ว่าแอปพลิเคชันจะโหลดและเริ่มทำงานได้เร็วกว่ามาก และมีหน่วยความจำน้อยกว่า Rails มาก ดังนั้น ครั้งต่อไปที่คุณเริ่มทำงานกับ API ใหม่ใน Ruby ให้พิจารณาใช้ Sinatra และ Sequel เนื่องจากเป็นเครื่องมือที่ทรงพลังมากสำหรับกรณีการใช้งานดังกล่าว