Інтро
Минулого разу ми створили API на основі Grape, який може зберігати та повертати наші книжки (поки ви не перезавантажите сервер). Цього разу ми вдосконалимо застосунок, додавши до нього певну структуру, Bundler, Rake і, що найголовніше, спосіб зберігати наші книги за допомогою Sequel та postgres.
Додаємо Bundler
Оскільки в нас понад дві залежності (rack
та grape
), ми скористаємось bundler
– менеджером залежностей для Ruby.
Наш проект знаходиться всього в двох кроках від власного менеджера залежностей:
- Встановлюємо bundler:
gem install bundler
; cd
у ваш каталог проекту і запускаємоbundle init
;
Тепер ви повинні побачити новий файл у вашому каталозі під назвою Gemfile
. Тут ми можемо додати геми, які ми використовували минулого разу:
# frozen_string_literal: true
source 'https://web.archive.org/web/20230321180801/https://rubygems.org'
git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
gem 'grape'
gem 'rack'
Тепер коли ми будемо виконувати bundle install
, наші залежності будуть встановлені та з'явиться новий файл Gemfile.lock
, який відслідковує версії гемів під час інсталяції.
Простий Sequel
Почнемо зі вбудованої (in-memory) БД Sqlite
. Так, ми втрачатимемо дані після перезавантаження сервера, але пізніше зможемо налаштувати Sequel
і замінити зв'язок на наш postgres-db. Додамо sequel
та sqlite3
до нашого Gemfile
.
# frozen_string_literal: true
source 'https://web.archive.org/web/20230321180801/https://rubygems.org'
git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
gem 'grape'
gem 'rack'
gem 'sequel'
gem 'sqlite3'
Примітка: Можливо, спочатку вам необхідно буде встановити sqlite3
.
Наші залежності відсортовані, а ми можемо знову поглянути на наш застосунок:
# application.rb
class MyApp
def books
@books ||= []
end
end
Application = MyApp.new
Нам вистачало простенького масиву @books
, але використаємо по максимуму переваги БД. Для цього нам потрібна БД. Додамо новий каталог lib/initializers/
з database.rb
в ньому.
# lib/initializers/database.rb
DB = Sequel.sqlite
DB.create_table?(:books) do
primary_key :id
String :title
String :author
end
Якщо раніше ви використовували Rails — знайте — ви тільки що створили свою першу db/schema.rb
! Вітаю! Наведений вище код досить простий:
DB = Sequel.sqlite
створює нову БДSqlite3
в пам'яті й (дотримуючись кращих практикSequel
) присвоює її константіDB
.DB.create_table?(:books)
створює таблицю з ім'ямbooks
.?
вказує на те, що таблиця буде створена, лише якщо таблиця books ще не існує. Це насправді не обов'язково, якщо база даних не живе довше, ніж сервер, але в майбутньому це дозволить уникнути помилок.primary_key :id
додає авто-інкрементprimary_key
під назвоюid
до нашої таблиці.String :title
іString :author
додає два поля до нашого таблиці, назва якихtitle
таauthor
, які є рядками. Цікаво, щоsequel
не дотримується загальної конвенції Ruby, визначаючи методи з іменами, де є великі літери.
Тепер у вас є акуратна, маленька БД, щоб зберігати записи book
.
Створення інтерфейсу до БД
Хоча тепер нам нічого не заважає отримати доступ до БД з будь-якого місця в нашому застосунку, нам слід застосувати інкапсуляцію, додавши клас, який буде керувати доступом до нашої БД. Ми будемо мати гарний і відокремлений код, який може стати нам в нагоді, коли ми вирішимо змінити нашу БД. Викличемо ці інтерфейси (можливо, в майбутньому їх буде набагато більше) і додамо їх в наш каталог / lib
.
mkdir lib/repositories
touch lib/repositories/book_repository.rb
Додамо наш інтерфейс до цього файлу.
# lib/repositories/book_repository.rb
class BookRepository
class << self
extend Forwardable
def_delegator :data_set, :to_a
def data_set
@data_set ||= DB[:books]
end
def insert(set)
data_set.insert(set)
self
end
def to_json(*_)
to_a.to_json
end
alias << insert
end
end
Тут потрібно пояснити кілька речей:
- Ми додали
Forwardable
, оскільки ми очікуємо делегування великої кількості повідомлень до - Нашого
data_set
. Ви можете думати проdata_set
як про нашу таблицюbooks
у базі даних. Ви можете отримати доступ до всієї вашої таблиці, використовуючиDB[:"#{table_name}"]
; - Ми повертаємо наш
BookRepository
для сумісності з більш ранньою версією застосунка, де ми повертали змінну@books
після додавання нової книги. Ми використовуємо аліас<<
іinsert
з тієї ж самої причини; - Оскільки Grape викличе
to_json
у всьому, що ми повернемо в блоці, ми хочемо, щоб методto_json
повертав щось корисне. Наприклад, всі книги.
Примітка: З якоїсь причини, довелося додати (*_)
в визначення to_json
, щоб мовчки проковтнути передані йому аргументи. Схоже, що Grape передає якийсь аргумент to_json
при рендерингу {books: books}
в api
Складаємо все до купи
Тепер у нас є база даних та інтерфейс для її використання. Але вона ніде не завантажується (життя без автозавантаження Rails важке, так). Тож змінимо це в нашому app.rb
.
# app.rb
require 'forwardable'
require 'sqlite3'
require 'sequel'
require_relative 'lib/initializers/db'
require_relative 'lib/repositories/book_repository'
class MyApp
def books
@books ||= BookRepository
end
end
Application = MyApp.new
Я вирішив запитувати (require
) всі відповідні залежності для застосунку в app.rb
, тоді як зберігати в api.rb
все необхідне для grape
.
Гаразд! Ми вимкнули наш примітивний масив за допомогою високопрофесійного і надзвичайно складного BookRepository
. Так звучить набагато крутіше.
Додаємо Postgres
Застосунок працює, має реальну базу даних SQL
і відчуває себе супер фантастично. Але річ, яку ми хочемо вивчити в цьому розділі, пов'язана з усіма труднощами використання БД без допомоги Rails. Замість використання нашої sqlite3 в пам'яті (або переходу на постійну базу даних sqlite3, оскільки це махлювання), ми додамо великий і могутній postgres
в наш стек. І щоб все було простіше, ми додамо і dotenv
. Але про це пізніше.
Примітка: Якщо ви ще не встановили postgres
, тепер самий час для цього. Вам також знадобиться користувач, який може принаймні створити нову базу даних. Тож зробіть це.
Просто замініть sqlite3
на pg
та додайте dotenv
до вашого Gemfile
.
# Gemfile
# frozen_string_literal: true
source 'https://web.archive.org/web/20230321180801/https://rubygems.org'
git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
gem 'dotenv'
gem 'grape'
gem 'pg'
gem 'rack'
gem 'sequel'
Керування базою даних
Ми могли б просто створити базу даних, використовуючи postgres. Але ми очікуємо, що нам доведеться багато разів створювати та видаляти цю базу даних, тож просто напишемо для цього rake
обробники. Якщо ви розробляли застосунки для Rails, ймовірно, ви стикалися з завданнями в rake db:
. Вони завжди гарно підходять для того, щоб створити (create
), видалити (drop
) та наповнити (seed
) вашу базу даних для вас.
Попри те, що ми навмисно уникаємо парадигм Rails у цій серії, нам не обов'язково бути дикунами. Створимо власний rake db:create db:drop db:setup
. Додайте rake
у свій Gemfile
і виконайте:
touch Rakefile
Тепер ви можете запустити bundle exec rake
і він нічого не зробить! Агов?
Базові завдання
Коли ви встановили postgres
, ви, ймовірно, встановили утиліти, які постачаються разом з ним. Нас цікавить createdb
(ви можете перевірити її наявність за допомогою which createdb
). Оскільки ми ліниві, ми просто дамо можливість createdb
зробити всю роботу за нас!
namespace :db do
desc 'Creates a new database based on the variables in .env'
task :create do
puts 'Creating database...'
# Якщо створення успішне
puts `createdb && echo 'Created db'`
end
end
Додамо й rake db: drop:
.
namespace :db do
desc 'Creates a new database based on the variables in .env'
task :create do
puts 'Creating database...'
# Якщо створення успішне
puts `createdb && echo 'Created db'`
end
desc 'Drops the database specified in the variables in .env'
task :drop do
puts 'Dropping database...'
# Якщо drop пройшов успішно
puts `dropdb && echo 'Dropped db'`
end
end
Тепер це не спрацює, тому що createdb
і dropdb
потребують аргументів для роботи. Але як ми можемо додати ці аргументи в наше rake-завдання? Якщо ви подумали про систему аргументів rakes, то ні. Це нудно та важко. Замість цього ми будемо використовувати змінні середовища!
Використання dotenv
createdb
і dropdb
відмінно підходять для створення сценаріїв, оскільки обидва використовують змінні середовища (якщо їх називати правильно). Отже, ви можете просто виконати своє завдання так:
PGDATABASE=mydb PGPASS=12345 PGUSER=dau ... rake db:create
І все буде добре. Але знову ж таки, ми ледачі: ми не будемо вводити змінні середовища.
Замість цього ми будемо записувати їх у файл, де ми зможемо зчитати їх знову і знову. Якщо ви не чули про dotenv
, ось що він робить: він завантажує всі змінні, які ви визначаєте у файлі .env
, у середовище. Не варто починати з самого .env
, просто додайте файл .env.sample
, який виглядає так:
# .env.sample
PGUSER=
PGPASS=
PGDATABASE=
PGHOST=
PGPORT=
Залиште значення порожніми. Цей файл буде додано в контроль версій, оскільки в ньому немає нічого, що ми б хотіли приховати.
Примітка: Якщо ви використовуєте git
у своєму проекті, обов'язково додайте .env
до списку .gitignore
, щоб ви випадково не додали конфіденційні дані до контролю версій.
Тепер ви можете виконати:
cp .env.sample .env
і заповніть порожні значеннч! Якщо ви хочете, щоб postgres використовувала значення за замовчуванням, просто не пишіть нічого (наприклад, PGHOST та PGPORT залишаються за замовчуванням). Те, що ви повинні заповнити PGUSER, PGPASS і PGDATABASE. Якщо у вас все це заповнено (з правильною інформацією, звичайно), ваші rake завдання не будуть видавати помилки.
Ви можете сказати «Почекай, Rake не буде завантажувати ці змінні самостійно! Як він може знати про них!» І ви маєте рацію. Нашому Rakefile
все бракує чогось.
desc 'Loads our .env into ENV'
task :environment do
puts 'Loading environment...'
require 'dotenv/load'
end
namespace :db do
desc 'Creates a new database based on the variables in .env'
task :create => :environment do
puts 'Creating database...'
# Якщо створення успішне
puts `createdb && echo 'Created db'`
end
desc 'Drops the database specified in the variables in .env'
task :drop => :environment do
puts 'Dropping database...'
# Якщо drop пройшов успішно
puts `dropdb && echo 'Dropped db'`
end
desc 'Resets our database by dropping and creating it again.'
task :reset => :environment do
Rake::Task['db:drop'].invoke
Rake::Task['db:create'].invoke
end
end
Тепер завдання :environment
вимагає dotenv/load
, який завантажує змінні середовища. :create => :environment
говорить про те, що :environment
слід викликати раніше, ніж :create
може бути викликаний. Таким чином ми гарантуємо, що змінні завантажуються і postgres
має до них доступ.
Завдання :reset
призначене для rake db:reset
, який ми знаємо і любимо з Rails. Він видаляє БД і створює її заново.
Перехід на pg
Тепер ми можемо просто виконати:
rake db:create
і тепер у нас є наша база даних. Але наш застосунок все ще намагається використовувати sqlite-базу даних. Таким чином, ми швидко змінюємо деталі підключення в lib / initializers / database.rb:
.
DB = Sequel.connect("postgres://#{ENV['PGHOST']}/#{ENV['PGDATABASE']}?user=#{ENV['PGUSER']}&password=#{ENV['PGPASS']}")
DB.create_table?(:books) do
primary_key :id
String :title
String :author
end
Це поганий приклад, я визнаю, але це працює і точно показує, що відбувається.
Підбиваємо підсумки
Тепер у вас є власний маленький db-setup. І ви зробили все це самостійно! Без Rails! Випробуйте його, запустіть свій сервер, використовуючи rackup
, та додайте кілька книг за допомогою curl
(ви можете знову переглянути частину 1, щоб побачити, як це все точно працює), перезавантажте сервер і відвідайте http: // localhost: 9292 / books
щоб побачити, що ваші книги збереглися.
І нам не доводилося нічого змінювати в нашому api.rb
. Сила інкапсуляції, оуу-є!
Наступного разу
У нашому API відсутні декілька речей:
- Бізнес-логіка.
- Аутентифікація.
- Авторизація.
Ще немає коментарів