Щоб визначити десять найрозповсюдженіших помилок в Ruby on Rails проектах, команда Rollbar переглянула понад тисячу проектів своїх клієнтів. Ось які результати вони отримали:
Помилки відсортовано за кількістю проектів у яких вони зустрічаються. Розглянемо кожну з них, щоб визначити чому вони виникають і як цьому запобігти.
В статті наведемо приклади рішень для Rails 5. Якщо ви використовуєте Rails 4, вони мають вказати вам вірний напрямок.
1. ActionController::RoutingError
Ми почнемо з класики будь-якого веб-застосунку — Rails-версії помилки 404. ActionController::RoutingError
означає, що користувач запросив URL-адресу, якої не існує у вашому застосунку. Rails занесе це в лог і все буде виглядати як помилка. Але здебільшого це не несправність вашого застосунку.
Вона може бути викликана неправильними посиланнями, що вказані у вашому застосунку або вказують на нього. Також це може бути зловмисник або бот, який перевіряє застосунок на наявність розповсюджених уразливостей. Якщо це так, то ви можете знайти щось подібне у своїх логах:
ActionController::RoutingError (No route matches [GET] "/wp-admin"):
Існує одна розповсюджена причина, через яку можна отримати ActionController::RoutingError
, яка викликана вашим застосунком, а не випадковими користувачами: якщо ви розгортаєте застосунок на Heroku або іншій платформі, яка не дозволяє вам обробляти статичні файли. Тоді ви можете виявити, що CSS та JavaScript не завантажуються. Якщо причина в цьому, то помилка буде виглядати таким чином:
ActionController::RoutingError (No route matches [GET] "/assets/application-eff78fd93759795a7be3aa21209b0bd2.css"):
Щоб виправити це й дозволити Rails обслуговувати статичні асети, потрібно додати рядок у файл config/environments/production.rb
вашого застосунку:
Rails.application.configure do
# інша конфігурація
config.public_file_server.enabled = true
end
Якщо ви не хочете заносити в логи 404 помилки, спричинені ActionController::RoutingError
, слід встановити перехоплення всіх маршрутів й обробляти 404 самостійно. Цей метод використовується у проекті lograge. Щоб зробити так само, додайте наступний код в кінці файлу config/routes.rb
:
Rails.application.routes.draw do
# всі інші ваші маршрути
match '*unmatched', to: 'application#route_not_found', via: :all
end
Потім додайте метод route_not_found
у ваш ApplicationController
:
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
def route_not_found
render file: Rails.public_path.join('404.html'), status: :not_found, layout: false
end
end
Перш ніж впроваджувати цей підхід, слід врахувати, чи важливо для вас знати про помилки 404. Також важливо пам'ятати, що будь-який змонтований після завантаження застосунку маршрут або рушій не буде доступним, оскільки вони, будуть перехоплені як і всі маршрути.
2. NoMethodError: undefined method '[]' for nil:NilClass
Це означає, що ви використовуєте нотацію квадратних дужок для читання властивості об'єкту, але сам об'єкт відсутній або nil
, і тому він не підтримує цей метод. Оскільки ми працюємо з квадратними дужками, цілком ймовірно, що ми пробираємося скрізь хеші та масиви, щоб отримати доступ до властивостей, і на цьому шляху щось загубилося. Це могло статися, коли ми парсили й витягали дані з JSON API або з CSV файлу, або просто отримали дані зі вкладених параметрів контролера.
Розглянемо, як користувач надсилає адресні дані через форму. Ви можете припустити, що ваші параметри виглядатимуть таким чином:
{ user: { address: { street: '123 Fake Street', town: 'Faketon', postcode: '12345' } } }
Тоді ви зможете отримати доступ до інформації про вулицю, викликавши params[:user][:address][:street]
. Якщо адреса не була передана, то params[:user][:address]
буде nil
, а виклик [:street]
призведе до NoMethodError
.
Можна виконати перевірку на нуль для кожного параметру й повернутися на початок, використовуючи оператор &&
, наприклад так:
street = params[:user] && params[:user][:address] && params[:user][:address][:street]
Хоча це й спрацює, існує кращий спосіб отримати доступ до вкладених елементів у хешах, масивах і об'єктах подій, таких як ActionController::Parameters
. З часів Ruby 2.3, хеші, масиви та ActionController::Parameters
мають метод dig
. Він дозволяє надати шлях до об'єкта, який ви хочете отримати. Якщо на будь-якому етапі повертається nil
, тоді dig
також повертає nil
, не викидаючи NoMethodError
. Щоб отримати інформацію про вулицю з параметрів вище, можна викликати:
street = params.dig(:user, :address, :street)
Ви не отримаєте ніяких помилок, хоча вам треба знати, що street
, як і раніше, може бути nil
.
Крім того, якщо ви пробираєтесь крізь вкладені об'єкти, використовуючи крапкову нотацію, ви можете зробити це безпечно й в Ruby 2.3, використовуючи оператор безпечної навігації. Тому, замість того, щоб викликати:
street = user.address.street
і отримувати NoMethodError: undefined method street
для nil:NilClass
, тепер можна викликати:
street = user&.address&.street
Це працюватиме так само, як і при використанні dig
. Якщо адреса має значення nil
, street
буде nil
, і коли ви пізніше звертатиметесь до street
, вам доведеться обробляти nil
. Якщо всі об'єкти присутні, street
буде призначена правильно.
Хоча цей спосіб приховує помилки від користувача, якщо це все ще впливає на користувацький досвід, можливо, вам знадобиться створити внутрішню помилку для відстеження або у ваших логах, або в системі відстеження помилок, так щоб у вас була видимість проблеми.
Якщо ви не використовуєте Ruby 2.3 або вище, тоді можна досягти аналогічних результатів, використовуючи ruby_dig gem та try
з ActiveSupport.
3. ActionController::InvalidAuthenticityToken
Номер три у нашому списку вимагає ретельного розгляду, оскільки ця помилка пов'язана з безпекою нашого застосунку.
ActionController::InvalidAuthenticityToken
виникає, коли запити POST, PUT, PATCH або DELETE відсутні або мають некоректний токен CSRF (Cross Site Request Forgery).
CSRF є потенційною вразливістю у веб застосунках — шкідливий сайт робить запит до вашого застосунку від імені незнайомого користувача. Якщо користувач залогінений у свій сеанс, кукі-файли надсилатимуться поряд з запитом, і зловмисник може виконувати команди від імені користувача.
Rails зменшує ризик CSRF атак шляхом включення безпечного токена у всі форми, які відомі та підтверджені сайтом, але не можуть бути відомі третій стороні. Це виконується за допомогою знайомого рядка ApplicationController
:
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
end
Отже, якщо ваш застосунок у продакшені видаватиме помилки ActionController::InvalidAuthenticityToken
, це може означати, що зловмисник націлився на користувачів вашого сайту, проте Rails зберігатиме безпеку.
Є й інші причини, через які ви можете ненавмисне отримати цю помилку.
Ajax
Якщо ви, наприклад, робите Ajax запити з вашого фронтенду, вам потрібно впевнитись, що ви включили CSRF токен у запит. Якщо ви використовуєте jQuery і робите збірку в скриптовому адаптері Rails. У цьому випадку все буде оброблено для вас. Якщо ви хочете обробляти Ajax інакше, скажімо, використовуючи Fetch API, то, знову ж таки, треба впевнитись, що ви додали CSRF токен. Для будь-якого підходу треба бути певним, що макет застосунку містить мета тег CSRF у <head>
документу:
<%= csrf_meta_tags %>
Це виведе метатег, який виглядає якось так:
<meta name='csrf-token' content='THE-TOKEN'>
Коли ви робили Ajax запит, прочитайте вміст мета тегу й додайте його до заголовків в якості заголовку X-CSRF-Token
:
const csrfToken = document.querySelector('[name="csrf-token"]').getAttribute('content');
fetch('/posts', {
method: 'POST',
body: JSON.stringify(body),
headers: {
'Content-Type': 'application/json'
'X-CSRF-Token': csrfToken
}
).then(function(response) {
// обробка відповіді
});
Вебхуки/API
Іноді є вагомі причини вимкнути захист CSRF. Якщо ви очікуєте отримувати у вашому застосунку вхідні запити POST від третіх сторін на певні URL-адреси, то не захочете їх блокувати на основі CSRF. Наприклад, розробляючи API для сторонніх розробників або отримуючи вхідні вебхуки із сервісу.
Ви можете вимкнути CSRF, але переконайтеся, що занесли у whitelist кінцеві точки, які не потребують захисту. Можна зробити це в контролері, пропустивши аутентифікацію:
class ApiController < ApplicationController
skip_before_action :verify_authenticity_token
end
Якщо ви приймаєте вхідні вебхуки, вам слід мати можливість перевірити, що запит прийшов з перевіреного джерела замість перевірки токена CSRF.
4. Net::ReadTimeout
Net::ReadTimeout
виникає, коли Ruby потребує більше часу на читання даних із сокета, ніж значення read_timeout
, яке за замовчуванням складає 60 секунд. Ця помилка може виникнути, якщо ви використовуєте Net::HTTP
, open-uri
або HTTParty
для виконання HTTP запитів.
Примітно те, що це не означає, що помилка буде викинута, якщо сам по собі запит займає більше часу, ніж значення read_timeout
. Тільки якщо певне читання займає більше часу, ніж read_timeout
. Ви можете більше прочитати про Net::HTTP
та тайм-аути тут.
Є кілька речей, які можна зробити, щоб припинити отримувати помилки Net::ReadTimeout
. Як тільки ви зрозумієте, які HTTP-запити кидають помилку, то можете спробувати скорегувати значення read_timeout
. Як і в ситуації вище, якщо сервер, на який ви робите запит, вимагає багато часу на формування відповіді, перш ніж відправити її всю одразу, вам захочеться мати більше значення read_timeout
. Якщо сервер повертає відповідь шматками, вам потрібне менше значення read_timeout
.
Ви можете задати read_timeout
, встановивши значення в секундах у відповідному HTTP клієнті, яким ви користуєтесь:
з Net::HTTP
http = Net::HTTP.new(host, port, read_timout: 10)
з open-uri
open(url, read_timeout: 10)
з HTTParty
HTTParty.get(url, read_timeout: 10)
Ви не завжди можете розраховувати на те, що інший сервер відповість в очікувані вами часові рамки. Якщо ви можете запустити HTTP-запит у фоновому режимі з повтореннями, як Sidekiq, то це може зменшити кількість помилок з іншого сервера. Хоча вам знадобиться обробити випадок, де сервер ніколи не відповідає вчасно.
Якщо вам потрібно запустити HTTP-запит в рамках дії контролера, тоді слід використати rescue
до помилки Net::ReadTimeout
і надати вашому користувачеві альтернативний досвід, відстежуючи його в своєму рішенні моніторингу помилок:
def show
@post = Post.find(params[:slug])
begin
@comments = HTTParty.get(COMMENTS_SERVER, read_timeout: 10)
rescue Net::ReadTimeout => e
@comments = []
@error_message = "Comments couldn't be retrieved, please try again later."
Rollbar.error(e);
end
end
5. ActiveRecord::RecordNotUnique: PG::UniqueViolation
Це повідомлення про помилку конкретно для баз даних PostgreSQL, проте адаптери ActiveRecord для MySQL та SQLite викликатимуть подібні помилки. Проблема тут в тому, що таблиця бази у вашому застосунку має унікальний індекс для одного або кількох полів, і транзакція, що була надіслана у базу даних, порушує цей індекс. Цю проблему важко вирішити повністю, але подивімося спочатку на те, що вирішується простіше.
Уявіть, що ви створили модель User
, і в процесі міграції виявилось, що адреса електронної пошти користувача є унікальною. Міграція може виглядати якось так:
class CreateUsers < ActiveRecord::Migration[5.1]
def change
create_table :users do |t|
t.string :email
t.timestamps
end
add_index :users, :email, unique: true
end
end
Щоб уникнути більшості випадків з ActiveRecord::RecordNotUnique
, ви маєте додати перевірку на унікальність у модель User
.
class User < ApplicationRecord
validates_uniqueness_of :email
end
Без цієї перевірки, всі адреси електронної пошти надсилатимуться у базу даних при виклику User#save
, і будуть викликати помилку, якщо вони не унікальні. Однак, валідація не може гарантувати, що цього не станеться. Для повного розуміння, вам слід прочитати розділ, присвячений багатонитковості та цілісності у документації validates_uniqueness_of
. Короткий опис полягає в тому, що перевірка унікальності в Rails схильна до станів гонитви, заснованих на порядку операцій для декількох запитів. Через це цю помилку складно відтворити локально.
Щоб справитися з цією помилкою, потрібен певний контекст. Якщо помилки викликані станами гонитви, то це може бути пов'язано з тим, що користувач помилково надіслав форму двічі. Ми можемо спробувати виправити цю проблему за допомогою JavaScript, щоб відключити кнопку надсилання після першого натискання. Щось на зразок цього:
const forms = document.querySelectorAll('form');
Array.from(forms).forEach((form) => {
form.addEventListener('submit', (event) => {
const buttons = form.querySelectorAll('button, input[type=submit]')
Array.from(buttons).forEach((button) => {
button.setAttribute('disabled', 'disabled');
});
});
});
Це порада на Coderwall для використання first_or_create!
ActiveRecord разом з rescue
та retry
на випадок виникнення помилки — акуратне обхідне рішення. Вам слід продовжити заносити цю помилку у логи за допомогою вашого рішення з моніторингу помилок, щоб можна було підтримувати її видимість.
def self.set_flag( user_id, flag )
# Упевнитись, що ми використовуємо retry тільки 2 рази
tries ||= 2
flag = UserResourceFlag.where( :user_id => user_id , :flag => flag).first_or_create!
rescue ActiveRecord::RecordNotUnique => e
Rollbar.error(e)
retry unless (tries -= 1).zero?
end
6. NoMethodError: undefined method 'id' for nil:NilClass
NoMethodError
з'являється знову, хоча на цей раз з іншим пояснювальним повідомленням. Ця помилка зазвичай прокрадається навколо створення дії для об'єкта з відношенням. Щасливий шлях — успішне створення об'єкта — зазвичай працює, але ця помилка виникає, коли перевірки не виконуються. Подивімося на приклад.
Ось контролер з діями зі створення застосунку для курсу.
class CourseApplicationsController < ApplicationController
def new
@course_application = CourseApplication.new
@course = Course.find(params[:course_id])
end
def create
@course_application = CourseApplication.new(course_application_params)
if @course_application.save
redirect_to @course_application, notice: 'Application submitted'
else
render :new
end
end
private
def course_application_params
params.require(:course_application).permit(:name, :email, :course_id)
end
end
Форма у новому шаблоні виглядає приблизно так:
<%= form_for [@course, @course_application] do |ca| %>
<%# rest of the form %>
<% end %>
Проблема тут полягає в тому, що ви викликаєте render :new
з дії create
, а змінна екземпляру @course
не була встановлена. Ви повинні впевнитися, що всі об'єкти, необхідні шаблону new
, також ініціалізовані в дії create
. Для виправлення цієї помилки слід оновити дію create
, щоб вона виглядала наступним чином:
def create
@course_application = CourseApplication.new(course_application_params)
if @course_application.save
redirect_to @course_application, notice: 'Application submitted'
else
@course = Course.find(params[:course_id])
render :new
end
end
Перегляньте цю статтю, якщо ви зацікавлені у тому, щоб більше знати про проблеми з nil
у Rails та як їх уникнути.
7. ActionController::ParameterMissing
Ця помилка є частиною реалізації сильних параметрів (strong parameters) Rails. Хоча вона не проявляється як помилка 500 — вона обробляється ActionController::Base
й повертається як 400 Bad Request.
Повна помилка може виглядати так:
ActionController::ParameterMissing: param is missing or the value is empty: user
Все це супроводжуватиметься контролером, який може виглядати якось так:
class UsersController < ApplicationController
def create
@user = User.new(user_params)
if @user.save
redirect_to @user
else
render :new
end
end
private
def user_params
params.require(:user).permit(:name, :email)
end
end
params.require(:user)
означає, що якщо викликається user_params
, а params не має ключа :user
або params[:user]
порожній, то виникне ActionController::ParameterMissing
.
Якщо ви створюєте застосунок для використання через веб-інтерфейс, і ви створили форму для правильного розміщення параметрів user
для цієї дії, тоді відсутній параметр user
може означати, що хтось намагається зіпсувати ваш застосунок. Якщо причина в цьому, відповідь 400 Bad Request, скоріш за все, є найкращим варіантом, оскільки вам не потрібно обслуговувати потенційних зловмисників.
Якщо ваш застосунок надає API, тоді 400 Bad Request також є доречною відповіддю на відсутній параметр.
8. ActionView::Template::Error: undefined local variable or method
Це єдина помилка в нашому топі, пов'язана з ActionView
, і це хороший знак. Чим менше роботи представленням треба робити для рендеру шаблонів, тим краще. Менша кількість роботи веде до меншої кількості помилок. Проте, ми все ще залишаємося з цією помилкою, в якій змінна чи метод, які мають існувати, попросту не існують.
Найчастіше вона зустрічається в часткових представленнях (partials), можливо через велику кількість різноманітних способів, якими ви можете включити partial з локальними змінними на сторінці. Якщо у вас є partial під назвою _post.html.erb
, який містить шаблон посту блогу та екземпляр змінної @post
, встановленої у вашому контролері, тоді ви можете відобразити partial таким чином:
<%= render @post %>
або так:
<%= render 'post', post: @post %>
або так:
<%= render partial: 'post', locals: { post: @post } %>
Rails любить давати нам багато можливостей для роботи, але другий і третій варіанти — місця, де може виникнути плутанина. Спроба рендерити partial ось так:
<%= render 'post', locals: { post: @post } %>
або так:
<%= render partial: 'post', post: @post %>
залишить вас з невизначеною локальною змінною або методом. Щоб уникнути цього, залишайтесь послідовними, і завжди рендеріть partials за допомогою точного синтаксису partial, виражаючи локальні змінні у хеші locals:
<%= render partial: 'post', locals: { post: @post } %>
Існує ще одне місце, в якому ви можете помилитися з локальними змінними у partials. Якщо ви іноді передаєте змінну у partial, тестування для цієї змінної в partial відрізняється від звичайного коду Ruby. Якщо, наприклад, ви оновлюєте вищевказаний partial post
, щоб прийняти локальну змінну, яка повідомить вам, чи показувати зображення заголовку в partial, то ви обробляли б partial наступним чином:
<%= render partial: 'post', locals: { post: @post, show_header_image: true } %>
Тоді сам по собі partial може виглядати так:
<h1><%= @post.title %></h1>
<%= image_tag(@post.header_image) if show_header_image %>
<!-- і так далі -->
Це добре працюватиме під час передачі локальної змінної show_header_image
, проте, коли ви викличете
<%= render partial: 'post', locals: { post: @post } %>
воно не виконається через невизначену локальну змінну. Для перевірки наявності локальної змінної всередині partial, вам потрібно перевірити, чи вона визначена, перед тим, як її використовувати.
<%= image_tag(@post.header_image) if defined?(show_header_image) && show_header_image %>
Хоча, є варіант навіть краще — у partial є хеш під назвою local_assigns
, який ми можемо використати натомість:
<%= image_tag(@post.header_image) if local_assigns[:show_header_image] %>
Для не булевих змінних можна використати інші хеш-методи, як наприклад fetch
, щоб зробити обробку більш витонченою. Використовуючи show_header_image
як приклад, цей сценарій також буде працювати:
<%= image_tag(@post.header_image) if local_assigns.fetch(:show_header_image, false) %>
Взагалі, будьте уважними, коли передаєте змінні у partials!
9. ActionController::UnknownFormat
Ця помилка, як і ActionController::InvalidAuthenticityToken
, скоріше може бути викликана необережними користувачами або зловмисниками, ніж самим застосунком. Якщо ви створили застосунок, в якому дії відповідають за допомогою HTML-шаблонів, і хтось запитує JSON версію сторінки, то ви знайдете помилку у логах, схожу на цю:
ActionController::UnknownFormat (BlogPostsController#index is missing a template for this request format and variant.
request.formats: ["application/json"]
request.variant: []):
Користувач отримає 406 Not Acceptable response. Він побачить цю помилку, тому що не був визначений шаблон для цієї відповіді.
Однак, можливо ви створили Rails застосунок для відповіді на звичайні HTML-запити й більш API-подібні JSON-запити в одному й тому ж контролері. Якщо ви зробили це, слід визначити формати, на які ви хочете відповідати, і будь-які формати, що виходять за рамки цього, також призведуть до ActionController::UnknownFormat
, повертаючи статус 406. Припустімо, у вас є індекс постів у блозі, який виглядає так:
class BlogPostsController < ApplicationController
def index
respond_to do |format|
format.html { render :index }
end
end
end
Виконання запиту до JSON призведе до відповіді 406, і ваші логи покажуть помилку:
ActionController::UnknownFormat (ActionController::UnknownFormat):
На цей раз помилка не скаржиться на відсутність шаблону — це навмисна помилка, оскільки єдиний формат, який ви визначили для відповіді — HTML. А що робити, якщо це було ненавмисно?
Доволі часто можна пропустити формат у відповіді, який збирались підтримувати. Розглянемо дію, в якій ви хочете відповісти на HTML та JSON запити при створенні посту в блозі, щоб ваша сторінка могла підтримувати Ajax запит. Це може виглядати так:
class BlogPostsController < ApplicationController
def create
@blog_post = BlogPost.new(blog_post_params)
respond_to do |format|
if @blog_post.save
format.html { redirect blog_post_path(@blog_post) }
format.json { render json: @blog_post.to_json }
else
render :new
end
end
end
end
Помилка тут виникає у випадку, якщо пост провалив перевірку. У блоці respond_to
вам треба викликати render
в області видимості блоків форматів. Новий варіант виглядав би якось так:
class BlogPostsController < ApplicationController
def create
@blog_post = BlogPost.new(blog_post_params)
respond_to do |format|
if @blog_post.save
format.html { redirect blog_post_path(@blog_post) }
format.json { render json: @blog_post.to_json }
else
format.html { render :new }
format.json { render json: @blog_post.errors.to_json }
end
end
end
end
Тепер всі формати охоплені, і більше не буде ніяких ненавмисних виключень ActionController::UnknownFormat
.
10. StandardError: An error has occurred, this and all later migrations canceled
StandardError
є базовим класом помилок, з якого всі інші помилки повинні наслідуватись. Його використання робить помилку дуже загальною, коли насправді ця помилка виникає під час міграції бази даних. Краще було б побачити цю помилку як потомка ActiveRecord :: MigrationError
.
Існує ряд моментів, які можуть призвести до збою міграції. Наприклад, міграції можуть бути не синхронізовані з вашою поточною базою даних. У цьому випадку, вам прийдеться дослідити й виявити, що могло статися та як все виправити.
Однак, тут є ще одне питання, яке слід розглянути: міграція даних.
Якщо вам потрібно додати або розрахувати деякі дані для всіх об'єктів у таблиці, то вам може здатися, що міграція даних — хороша ідея. Наприклад, вам хочеться додати поле з повним ім'ям користувача до моделі user, яке включає їх ім'я та прізвище (це не дуже правдоподібна зміна, але вона добре підходить як приклад), і ви напишете міграцію якось так:
class AddFullNameToUser < ActiveRecord::Migration
def up
add_column :users, :full_name, :string
User.find_each do |user|
user.full_name = "#{user.first_name} #{user.last_name}"
user.save!
end
end
def down
remove_column :users, :full_name
end
end
З цим сценарієм виникає багато проблем. Якщо є користувач з пошкодженими даними, команда user.save!
викине помилку й скасує міграцію. По-друге, у продакшені у вас може бути багато користувачів. Це означає, що для міграції бази даних знадобиться багато часу. Через це ваш застосунок буде знаходитись у режимі офлайн протягом великого відрізка часу. І нарешті, по мірі зміни вашого застосунку, ви можете видалити або перейменувати модель User
, що призведе до збою міграції. Деякі поради вказують на те, щоб ви визначали модель User всередині міграції, щоб уникнути цього. Для ще більшої безпеки Еллі Мередіт радить зовсім уникати міграції даних у межах міграцій ActiveRecord та натомість створювати тимчасові задачі міграцій даних.
Зміна даних за межами міграції змусить вас розглянути, як працюватиме ваша модель, якщо даних немає. У нашому прикладі з повним ім'ям, ви, скоріш за все, визначили б метод доступу (accessor) для властивості full_name
, який міг би відповісти, якщо дані недоступні. Якщо це не так, тоді задайте повне ім'я шляхом конкатенації складових частин:
class User < ApplicationRecord
def full_name
@full_name || "#{first_name} #{last_name}"
end
end
Запуск міграції даних як окремого завдання також означає, що розгортання більше не залежить від зміни даних через базу даних у продакшені. Стаття Еллі містить більше причин, чому це працює краще, і включає найкращі практики щодо написання задачі.
Ще немає коментарів