Bagaimana Sekuel dan Sinatra Memecahkan Masalah API Ruby
Diterbitkan: 2022-03-11pengantar
Dalam beberapa tahun terakhir, jumlah kerangka kerja aplikasi satu halaman JavaScript dan aplikasi seluler telah meningkat secara substansial. Hal ini menyebabkan peningkatan permintaan untuk API sisi server. Dengan Ruby on Rails menjadi salah satu kerangka kerja pengembangan web paling populer saat ini, ini adalah pilihan alami di antara banyak pengembang untuk membuat aplikasi API back-end.
Namun sementara paradigma arsitektur Ruby on Rails membuatnya cukup mudah untuk membuat aplikasi API back-end, menggunakan Rails hanya untuk API itu berlebihan. Faktanya, ini terlalu berlebihan sampai-sampai tim Rails menyadari hal ini dan oleh karena itu memperkenalkan mode hanya-API baru di versi 5. Dengan fitur baru di Ruby on Rails ini, membuat aplikasi hanya-API di Rails menjadi lebih mudah. dan pilihan yang lebih layak.
Tapi ada juga pilihan lain. Yang paling menonjol adalah dua permata yang sangat matang dan kuat, yang dalam kombinasi menyediakan alat yang kuat untuk membuat API sisi server. Mereka adalah Sinatra dan Sekuel.
Kedua permata ini memiliki kumpulan fitur yang sangat kaya: Sinatra berfungsi sebagai bahasa khusus domain (DSL) untuk aplikasi web, dan Sekuel berfungsi sebagai lapisan pemetaan relasional objek (ORM). Jadi, mari kita lihat secara singkat masing-masing.
Sinatra
Sinatra adalah kerangka aplikasi web berbasis Rak. The Rack adalah antarmuka server web Ruby yang terkenal. Ini digunakan oleh banyak kerangka kerja, seperti Ruby on Rails, misalnya, dan mendukung banyak server web, seperti WEBrick, Thin, atau Puma. Sinatra menyediakan antarmuka minimal untuk menulis aplikasi web di Ruby, dan salah satu fiturnya yang paling menarik adalah dukungan untuk komponen middleware. Komponen-komponen ini terletak di antara aplikasi dan server web, dan dapat memantau dan memanipulasi permintaan dan tanggapan.
Untuk memanfaatkan fitur Rak ini, Sinatra mendefinisikan DSL internal untuk membuat aplikasi web. Filosofinya sangat sederhana: Rute diwakili oleh metode HTTP, diikuti oleh rute yang cocok dengan pola. Blok Ruby di mana permintaan diproses dan respons terbentuk.
get '/' do 'Hello from sinatra' end
Pola pencocokan rute juga dapat menyertakan parameter bernama. Ketika blok rute dieksekusi, nilai parameter dilewatkan ke blok melalui variabel params
.
get '/players/:sport_id' do # Parameter value accessible through params[:sport_id] end
Pencocokan pola dapat menggunakan operator percikan *
yang membuat nilai parameter tersedia melalui params[:splat]
.
get '/players/*/:year' do # /players/performances/2016 # Parameters - params['splat'] -> ['performances'], params[:year] -> 2016 end
Ini bukan akhir dari kemungkinan Sinatra terkait dengan pencocokan rute. Itu dapat menggunakan logika pencocokan yang lebih kompleks melalui ekspresi reguler, serta pencocokan khusus.
Sinatra memahami semua kata kerja HTTP standar yang diperlukan untuk membuat REST API: Get, Post, Put, Patch, Delete, dan Options. Prioritas rute ditentukan oleh urutan penetapannya, dan rute pertama yang cocok dengan permintaan adalah rute yang melayani permintaan tersebut.
Aplikasi Sinatra dapat ditulis dalam dua cara; menggunakan gaya klasik atau modular. Perbedaan utama di antara keduanya adalah, dengan gaya klasik, kita hanya dapat memiliki satu aplikasi Sinatra per proses Ruby. Perbedaan lainnya cukup kecil sehingga, dalam banyak kasus, dapat diabaikan, dan pengaturan default dapat digunakan.
Pendekatan Klasik
Menerapkan aplikasi klasik sangatlah mudah. Kami hanya perlu memuat Sinatra dan mengimplementasikan penangan rute:
require 'sinatra' get '/' do 'Hello from Sinatra' end
Dengan menyimpan kode ini ke file demo_api_classic.rb
, kita dapat memulai aplikasi secara langsung dengan menjalankan perintah berikut:
ruby demo_api_classic.rb
Namun, jika aplikasi akan digunakan dengan penangan Rak, seperti Penumpang, lebih baik memulainya dengan file config.ru
konfigurasi Rak.
require './demo_api_classic' run Sinatra::Application
Dengan file config.ru
di tempat, aplikasi dimulai dengan perintah berikut:
rackup config.ru
Pendekatan Modular
Aplikasi Modular Sinatra dibuat dengan mensubklasifikasikan Sinatra::Base
atau Sinatra::Application
:
require 'sinatra' class DemoApi < Sinatra::Application # Application code run! if app_file == $0 end
Pernyataan yang dimulai dengan run!
digunakan untuk memulai aplikasi secara langsung, dengan ruby demo_api.rb
, seperti halnya dengan aplikasi klasik. Di sisi lain, jika aplikasi akan di-deploy dengan Rack, konten handler dari rackup.ru
harus:
require './demo_api' run DemoApi
Sekuel
Sekuel adalah alat kedua di set ini. Berbeda dengan ActiveRecord, yang merupakan bagian dari Ruby on Rails, dependensi Sequel sangat kecil. Pada saat yang sama, ini cukup kaya fitur dan dapat digunakan untuk semua jenis tugas manipulasi basis data. Dengan bahasa spesifik domain yang sederhana, Sequel membebaskan pengembang dari semua masalah dengan memelihara koneksi, membangun kueri SQL, mengambil data dari (dan mengirim data kembali ke) database.
Misalnya, membuat koneksi dengan database sangat sederhana:
DB = Sequel.connect(adapter: :postgres, database: 'my_db', host: 'localhost', user: 'db_user')
Metode connect mengembalikan objek database, dalam hal ini, Sequel::Postgres::Database
, yang selanjutnya dapat digunakan untuk mengeksekusi SQL mentah.
DB['select count(*) from players']
Atau, untuk membuat objek kumpulan data baru:
DB[:players]
Kedua pernyataan ini membuat objek dataset, yang merupakan entitas Sekuel dasar.
Salah satu fitur kumpulan data Sekuel yang paling penting adalah ia tidak segera mengeksekusi kueri. Hal ini memungkinkan untuk menyimpan kumpulan data untuk digunakan nanti dan, dalam banyak kasus, untuk merangkainya.
users = DB[:players].where(sport: 'tennis')
Jadi, jika dataset tidak langsung masuk ke database, pertanyaannya adalah kapan? Sekuel mengeksekusi SQL pada database ketika apa yang disebut "metode yang dapat dieksekusi" digunakan. Metode ini adalah, untuk beberapa nama, all
, each
, map
, first
, dan last
.
Sekuel dapat diperpanjang, dan dapat diperpanjang adalah hasil dari keputusan arsitektur mendasar untuk membangun inti kecil yang dilengkapi dengan sistem plugin. Fitur mudah ditambahkan melalui plugin yang sebenarnya adalah modul Ruby. Plugin yang paling penting adalah plugin Model
. Ini adalah plugin kosong yang tidak mendefinisikan kelas atau metode instan apa pun dengan sendirinya. Sebagai gantinya, ini menyertakan plugin (submodul) lain yang mendefinisikan metode kumpulan data kelas, instance, atau model. Plugin Model memungkinkan penggunaan Sequel sebagai alat pemetaan relasional objek (ORM) dan sering disebut sebagai "plugin dasar".
class Player < Sequel::Model end
Model Sekuel secara otomatis mem-parsing skema database dan menyiapkan semua metode pengakses yang diperlukan untuk semua kolom. Diasumsikan bahwa nama tabel adalah jamak dan merupakan versi yang digarisbawahi dari nama model. Jika ada kebutuhan untuk bekerja dengan database yang tidak mengikuti konvensi penamaan ini, nama tabel dapat diatur secara eksplisit saat model didefinisikan.
class Player < Sequel::Model(:player) end
Jadi, kami sekarang memiliki semua yang kami butuhkan untuk mulai membangun API back-end.
Membangun API
Struktur Kode
Berlawanan dengan Rails, Sinatra tidak memaksakan struktur proyek apa pun. Namun, karena selalu merupakan praktik yang baik untuk mengatur kode untuk pemeliharaan dan pengembangan yang lebih mudah, kami juga akan melakukannya di sini, dengan struktur direktori berikut:

project root |-config |-helpers |-models |-routes
Konfigurasi aplikasi akan dimuat dari file konfigurasi YAML untuk lingkungan saat ini dengan:
Sinatra::Application.config_file File.join(File.dirname(__FILE__), 'config', "#{Sinatra::Application.settings.environment}_config.yml")
Secara default, nilai Sinatra::Applicationsettings.environment
adalah development,
dan diubah dengan menyetel variabel lingkungan RACK_ENV
.
Selanjutnya, aplikasi kita harus memuat semua file dari tiga direktori lainnya. Kita dapat melakukannya dengan mudah dengan menjalankan:
%w{helpers models routes}.each {|dir| Dir.glob("#{dir}/*.rb", &method(:require))}
Sekilas, cara memuat ini mungkin terlihat nyaman. Namun, dengan satu baris kode ini, kita tidak dapat dengan mudah melewatkan file, karena akan memuat semua file dari direktori dalam array. Itu sebabnya kami akan menggunakan pendekatan pemuatan file tunggal yang lebih efisien, yang mengasumsikan bahwa di setiap folder kami memiliki file manifes init.rb
, yang memuat semua file lain dari direktori. Juga, kami akan menambahkan direktori target ke jalur pemuatan 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
Pendekatan ini membutuhkan sedikit lebih banyak pekerjaan karena kita harus mempertahankan pernyataan yang dibutuhkan di setiap file init.rb
tetapi, sebagai gantinya, kita mendapatkan lebih banyak kontrol, dan kita dapat dengan mudah meninggalkan satu atau lebih file dengan menghapusnya dari file init.rb
manifes dalam direktori sasaran.
Otentikasi API
Hal pertama yang kita butuhkan di setiap API adalah otentikasi. Kami akan mengimplementasikannya sebagai modul pembantu. Logika otentikasi lengkap akan berada di file 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
Yang harus kita lakukan sekarang adalah memuat file ini dengan menambahkan pernyataan yang dibutuhkan dalam file manifes helper ( helpers/init.rb
) dan memanggil authenticate!
metode di kait before
Sinatra yang akan dieksekusi sebelum memproses permintaan apa pun.
before do authenticate! end
Basis Data
Selanjutnya, kita harus menyiapkan database kita untuk aplikasi. Ada banyak cara untuk mempersiapkan database, tetapi karena kita menggunakan Sequel, maka wajar untuk melakukannya dengan menggunakan migrator. Sekuel hadir dengan dua jenis migrasi - berbasis integer dan timestamp. Masing-masing memiliki kelebihan dan kekurangan. Dalam contoh kami, kami memutuskan untuk menggunakan migrator stempel waktu Sekuel, yang mengharuskan file migrasi diawali dengan stempel waktu. Migrator stempel waktu sangat fleksibel dan dapat menerima berbagai format stempel waktu, tetapi kami hanya akan menggunakan yang terdiri dari tahun, bulan, hari, jam, menit, dan detik. Berikut adalah dua file migrasi kami:
# 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
Kami sekarang siap untuk membuat database dengan semua tabel.
bundle exec sequel -m db/migrations sqlite://db/development.sqlite3
Terakhir, kami memiliki file model sport.rb
dan player.rb
di direktori 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
Di sini kita menggunakan cara Sekuel untuk mendefinisikan hubungan model, di mana objek Sport
memiliki banyak pemain dan Player
hanya dapat memiliki satu olahraga. Selain itu, setiap model mendefinisikan metode to_api
-nya, yang mengembalikan hash dengan atribut yang perlu diserialisasi. Ini adalah pendekatan umum yang dapat kita gunakan untuk berbagai format. Namun, jika kita hanya akan menggunakan format JSON di API kita, kita dapat menggunakan to_json
Ruby dengan only
argumen untuk membatasi serialisasi ke atribut yang diperlukan, yaitu player.to_json(only: [:id, :name, :sport_i])
. Tentu saja, kita juga bisa mendefinisikan BaseModel
yang mewarisi dari Sequel::Model
dan mendefinisikan metode to_api
default, yang kemudian mewarisi semua model.
Sekarang, kita dapat mulai mengimplementasikan titik akhir API yang sebenarnya.
Titik Akhir API
Kami akan menyimpan definisi semua titik akhir dalam file di dalam direktori routes
. Karena kami menggunakan file manifes untuk memuat file, kami akan mengelompokkan rute berdasarkan sumber daya (yaitu, menyimpan semua rute terkait olahraga di file sports.rb
, semua rute pemain di routes.rb
, dan seterusnya).
# 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
Rute bersarang, seperti rute untuk mendapatkan semua pemain dalam satu sport /sports/:id/players
, dapat ditentukan dengan menempatkannya bersama dengan rute lain, atau dengan membuat file sumber daya terpisah yang hanya berisi rute bersarang.
Dengan rute yang ditentukan, aplikasi sekarang siap menerima permintaan:
curl -i -XGET 'http://localhost:9292/sports?client_id=<client_id>&client_secret=<client_secret>'
Perhatikan bahwa seperti yang dipersyaratkan oleh sistem otentikasi aplikasi yang ditentukan dalam file helpers/authentication.rb
, kami meneruskan kredensial secara langsung dalam parameter permintaan.
Kesimpulan
Prinsip-prinsip yang ditunjukkan dalam contoh aplikasi sederhana ini berlaku untuk aplikasi back-end API apa pun. Hal ini tidak didasarkan pada arsitektur model-view-controller (MVC), namun menjaga pemisahan tanggung jawab yang jelas dengan cara yang sama; logika bisnis lengkap disimpan dalam file model sementara penanganan permintaan dilakukan dalam metode rute Sinatra. Berlawanan dengan arsitektur MVC, di mana tampilan digunakan untuk membuat respons, aplikasi ini melakukannya di tempat yang sama saat menangani permintaan - dalam metode rute. Dengan file pembantu baru, aplikasi dapat dengan mudah diperluas untuk mengirim pagination atau, jika diperlukan, meminta informasi batas kembali ke pengguna di header respons.
Pada akhirnya, kami telah membangun API lengkap dengan perangkat yang sangat sederhana dan tanpa kehilangan fungsionalitas apa pun. Jumlah dependensi yang terbatas membantu memastikan bahwa aplikasi memuat dan memulai lebih cepat, dan memiliki jejak memori yang jauh lebih kecil daripada yang berbasis Rails. Jadi, lain kali Anda mulai mengerjakan API baru di Ruby, pertimbangkan untuk menggunakan Sinatra dan Sequel karena keduanya adalah alat yang sangat kuat untuk kasus penggunaan seperti itu.