Ruby on Rails - szybkie wprowadzenie
Wprowadzenie do Ruby on Rails prezentujące CRUD, relacje bazodanowe, mailera oraz komunikację przez web sockets.
Daniel Gustaw
• 12 min read
W roku 2019 przepisywałem pewien system medyczny z Rails na PHP, w 2021 z Rails na NodeJS. Być może ty też spotykasz się z systemami opartymi na Rails, które tracą utrzymanie. To wprowadzenie pomoże Ci szybko zapoznać się z podstawami tego frameworka.
Napiszemy w nim bloga całkowicie od zera. Zaznaczę, że nie znam dobrze ani Ruby ani Rails, więc zamiast obszernego wprowadzenia mamy tu odtworzenie mojego procesu uczenia się.
Założenia:
- używamy linuxa (arch)
Postawienie aplikacji - CRUD
Zaczniemy od instalacji odpowiedniej wersji ruby.
curl -sSL https://get.rvm.io | bash -s stable --rails
rvm
jest narzędziem analogicznym do nvm
- pozwala zarządzać wersją interpretera co jest wyjątkowo przydatne przy pracy z systemami, które używają różnych wersji interpreterów. Możesz o nim poczytać tu:
RVM: Ruby Version Manager - Installing RVM
Tworzymy aplikację poleceniem
rails new weblog && cd weblog
Ta komenda zajmuje dużo czasu, ponieważ wymaga instalacji wszystkich paczek gem
i kompilacji node-sass
.
Następnym krokiem jest wygenerowanie automatycznie kodu do wykonywania operacji CRUD na poście. Posty będą miały tytuł i zawartość.
rails generate scaffold post title:string body:text
Ta komenda powoduje wygenerowanie się sporej ilości plików:
Jednym z nich jest migracja na bazie danych, która zapisana w db/migrate/20210418121400_create_posts.rb
wygląda tak:
class CreatePosts < ActiveRecord::Migration[6.1]
def change
create_table :posts do |t|
t.string :title
t.text :body
t.timestamps
end
end
end
Aby zsynchronizować bazę danych z wynikiem tej migracji wpisujemy
rails db:migrate
Tu może się pojawić pytanie: “Jaką bazę danych?”. W pliku config/database.yml
możemy zobaczyć konfigurację z której wynika, że domyślnie jest to sqlite
. W pliku db/schema.rb
jest schemat bazy.
To jest dobre miejsce na dygresję. Migrując systemy oparte na Ruby on Rails zastanawiałem się dlaczego środowisko produkcyjne ma “sqlite”, myślałem, że ktoś celowo tak to skonfigurował. Okazuje się, że wystarczyło nie zmienić konfiguracji w tym pliku. Innym problemem, który zaprzątał mi głowę dwa lata temu, było pole “updated_at” w tabelach, które nie obsługiwały edycji. Widząc “updated_at” i nie mając dokumentacji myślałem, że istnieje proces edycji tych tabel, jednak to również jest wynik domyślnej konfiguracji “rails”, który wszędzie wrzuca te kolumny.
Żeby wystartować server używamy komendy
rails server
Ogromną zaletą rails jest, że już teraz możemy korzystać z działającego CRUD pod linkiem
Po wytworzeniu posta ręcznie dostajemy:
Co jeszcze przyjemniejsze mamy też od razu “api” pod adresem /posts.json
Niestety próba utworzenia posta przez API
http POST localhost:3000/posts.json title="Hej" body="Ok"
kończy się błędem
Can't verify CSRF token authenticity.
Żeby wyłączyć protekcję “CSRF” w pliku app/controllers/application_controller.rb
konfigurujemy opcję protect_from_forgery
class ApplicationController < ActionController::Base
protect_from_forgery with: :null_session
end
Teraz zapis postów przez API działa. Zarówno
http POST localhost:3000/posts.json title=ok
jak i
http POST localhost:3000/posts.json body=ok
zapiszą swoje posty bez walidacji ich poprawności.
Aby wymusić obecność parametru title
w poście, w pliku app/models/post.rb
dodajemy flagę validates_presence_of
class Post < ApplicationRecord
validates_presence_of :title
end
Dzięki niej niemożliwe będzie dodawanie postów bez tytułu zarówno na stronie
jak i przez API
Debugowanie - Rails Console
Bardzo przydatnym narzędziem w pracy z Ruby on Rails jest konsola dostępna po wpisaniu polecenia:
rails console
Pozwala ona na interaktywny dostęp do danych za pomocą języka Ruby i zdefiniowanych w Rails obiektów. Na przykład pierwszy post zobaczymy wpisując
Post.first
Żeby dostać wszystkie posty piszemy
Post.all
Posty utworzone od wczoraj do jutra dostaniemy pisząc
Post.where(created_at: Date.yesterday..Date.tomorrow)
Łatwo można przekształcić to do formy zapytania SQL dodając własność to_sql
na końcu
Post.where(created_at: Date.yesterday..Date.tomorrow).to_sql
Aby utworzyć nowy post piszemy
Post.create! title: 'Hello', body: 'World'
Relacje między tabelami
Typowym przykładem relacji względem postów są komentarze. Nie potrzebujemy dla nich takich samych kontrolerów i widoków jak dla postów, dlatego do generowania zamiast scaffold
użyjemy flagi resource
.
rails generate resource comment post:references body:text
Pełną listę dostępnych generatorów zobaczymy wpisując polecenie:
rails generate
lub czytając dokumentację
The Rails Command Line — Ruby on Rails Guides
Tym czasem wrócimy do plików wygenerowanych dzięki opcji resource
.
Ponownie powstała tutaj migracja tym razem o zawartości:
class CreateComments < ActiveRecord::Migration[6.1]
def change
create_table :comments do |t|
t.references :post, null: false, foreign_key: true
t.text :body
t.timestamps
end
end
end
Aby ją wykonać wpisujemy
rails db:migrate
Zajmijmy się teraz routingiem. Nie ma sensu nigdy pytać o wszystkie komentarze. Zawsze są one powiązane z postem, którego dotyczą. Zatem w pliku config/routes.yml
zastępujemy występujące obok siebie
Rails.application.routes.draw do
resources :posts
resources :comments
end
na konfigurację mówiącą, żeby komentarze były zagnieżdżone w poście
Rails.application.routes.draw do
resources :posts do
resources :comments
end
end
Wyświetlenie routingu jest możliwe dzięki poleceniu:
rails routes
Co do kierunku relacji, to w tym momencie komentarze należą do postów co opisano w pliku app/models/comment.rb
class Comment < ApplicationRecord
belongs_to :post
end
Ale posty nie mają oznaczonej relacji z komentarzami, co naprawimy dodając has_many
do app/models/post.rb
class Post < ApplicationRecord
has_many :comments
validates_presence_of :title
end
W konsoli możemy utworzyć teraz przykładowy komentarz
Post.second.comments.create! body: "My first comment to second post"
Żeby wyświetlić komentarze i móc je dodawać napiszemy pomocnicze fragmenty widoków (partials). app/views/comments/_comment.html.erb
posłuży nam do wyświetlania jednego komentarza
<p><%= comment.body %> -- <%= comment.created_at.to_s(:long) %></p>
Natomiast app/views/comments/_new.html.erb
będzie formularzem do tworzenia komentarza
<%= form_for([ @post, Comment.new], remote: true) do |form| %>
Your comment: <br/>
<%= form.text_area :body, size: '50x2' %><br/>
<%= form.submit %>
<% end %>
Załączymy je w widoku pojedynczego postu dołączając do pliku app/views/posts/shwo.html.erb
kod
<hr>
<h2>Comments (<span id="count"><%= @post.comments.count %></span>)</h2>
<div id="comments">
<%= render @post.comments %>
</div>
<%= render 'comments/new', post: @post %>
Teraz nasz widok postu będzie wyglądała następująco
Mimo, że wygląda jak gotowy do działania, to funkcja dodawania komentarzy wciąż nie jest dostępna. Przygotowaliśmy jedynie widok, ale brakuje logiki, która obsłużyła by zapisywanie komentarzy do bazy i łączenie ich z postami.
Aby ją dołączyć musimy obsłużyć tworzenie komentarzy w kontrolerze app/controllers/comments_controller.rb
class CommentsController < ApplicationController
before_action :set_post
def create
@post.comments.create! comments_params
redirect_to @post
end
private
def set_post
@post = Post.find(params[:post_id])
end
def comments_params
params.required(:comment).permit(:body)
end
end
Przyjrzyjmy mu się uważnie. Zaczyna się od opcji before_action
, która ustawia post na podstawie parametru z adresu url
. Następnie w create
używamy tego posta aby do niego utworzyć komentarz, jego parametry pochodzą z comments_params
, które pobiera je z ciała żądania.
Następnie następuje przekierowanie do strony z postami. Na stronie działa to bardzo dobrze.
Ale jeśli chcemy tworzyć posty z poziomu API. to za każdym razem będąc przekierowani do postu zobaczymy go bez komentarzy. Jeśli zastąpimy
redirect_to @post
w kontrolerze przez instrukcje analogiczne jak dla posta
respond_to do |format|
if @post.save
format.html { redirect_to @post, notice: "Comment was successfully created." }
format.json { render :show, status: :created, location: @post }
else
format.html { render :new, status: :unprocessable_entity }
format.json { render json: @post.errors, status: :unprocessable_entity }
end
end
dostaniemy błąd
Jest tak dlatego, że teraz komentarze wymagają aby nadać im strukturę przy układaniu ich w plik JSON. Jest to rozwiązane dzięki fantastycznej bibliotece jbuilder
.
Tworząc plik app/views/comments/show.json.jbuilder
o treści
json.partial! "posts/post", post: @post
json.comments @post.comments, :id, :body, :created_at
skonfigurujemy serwer, aby po utworzeniu komentarza odpowiadał widokiem posta z listą wszystkich odpowiadających mu komentarzy. Jest to widok odpowiadający temu co widzimy w wersji HTML, choć nie zgodny z zasadami REST.
Jeśli chcieli byśmy wyświetlić ten konkretny komentarz i możemy użyć składni
def create
comment = @post.comments.create! comments_params
respond_to do |format|
if @post.save
format.html { redirect_to @post, notice: "Comment was successfully created." }
format.json { render json: comment.to_json(include: [:post]) }
else
format.html { render :new, status: :unprocessable_entity }
format.json { render json: @post.errors, status: :unprocessable_entity }
end
end
end
w kontrolerze. Wtedy w widoku odpowiedzi zobaczymy komentarz wraz z postem.
Więcej o formatowaniu można przeczytać tutaj:
Wysyłka e-maili
Bardzo częstą funkcją w serwisach internetowych jest wysyłanie e-maili w reakcji na jakieś zdarzenia. Zamiast pisać kod ponownie skorzystamy z generatora:
rails generate mailer comments submitted
Jest to e-mailer wysyłający powitanie. Pierwszą rzeczą, jaką zrobimy, będzie konfiguracja danych, jakie będzie wstrzykiwał do szablonów. W pliku comments_mailer.rb
piszemy kod:
class CommentsMailer < ApplicationMailer
def submitted(comment)
@comment = comment
mail to: "[email protected]", subject: 'New comment'
end
end
W app/views/comments_mailer
mamy dwa pliki z szablonami. Dla widoku HTML jest to plik submitted.html.erb
. Zmodyfikujemy go tak, żeby używając wcześniej zdefiniowanego partiala pokazywał nowy komentarz:
<h1>New comment on post: <%= @comment.post.title %></h1>
<%= render @comment %>
W pliku submitted.text.erb
nie możemy użyć już render
dlatego uprościmy widok tekstowy do formy:
New comment on post: <%= @comment.post.title %>: <%= @comment.body %>
Niesamowite w Rails jest to, że mamy gotowy widok do podglądu tych e-maili bez konieczności ich wysyłania. Aby z niego skorzystać musimy tylko wskazać komentarz, który wyświetlimy. W tym celu w pliku test/mailers/previews/comments_mailer_preview.rb
linię
CommentsMailer.submitted
zmieniamy na
CommentsMailer.submitted Comment.first
Pod adresem
http://localhost:3000/rails/mailers/comments_mailer/submitted
Możemy zobaczyć podgląd tego e-maila
Nie możemy jednak oczekiwać, że ten e-mail będzie od razu wysyłany. Aby dołączyć jego wysyłanie musimy dodać linię
CommentsMailer.submitted(comment).deliver_later
w kontrolerze komentarzy. Cały kontroler powinien wyglądać teraz tak:
class CommentsController < ApplicationController
before_action :set_post
def create
comment = @post.comments.create! comments_params
CommentsMailer.submitted(comment).deliver_later
respond_to do |format|
if @post.save
format.html { redirect_to @post, notice: "Comment was successfully created." }
format.json { render json: comment.to_json(include: [:post]) }
else
format.html { render :new, status: :unprocessable_entity }
format.json { render json: @post.errors, status: :unprocessable_entity }
end
end
end
private
def set_post
@post = Post.find(params[:post_id])
end
def comments_params
params.required(:comment).permit(:body)
end
end
Flaga “deliver_later” pozwala na załączenie wysyłki e-maila do wewnętrznej pętli Ruby on Rails, która wyśle go najszybciej jak to możliwe nie blokując jednocześnie wykonania reszty kodu. Utworzenie komentarza nadal nie wyśle e-maila na prawdziwą pocztę, ale w konsoli zobaczymy, że taka akcja była by podjęta gdyby wysyłka faktycznie była do końca skonfigurowana.
Nie będziemy iść w tą stronę, ale jeśli chcesz dokończyć konfigurację to poczytaj o smtp_settings
i delivery_method
w dokumentacji:
Action Mailer Basics — Ruby on Rails Guides
Teraz przejdziemy do komunikacji z serwerem w czasie rzeczywistym.
Cable - komunikacja przez web socket
Aby używać komunikacji w czasie rzeczywistym, potrzebujemy kanału. Wygenerujemy go poleceniem:
rails generate channel comments
W pliku app/channels/comments_channel.rb
o zawartości:
class CommentsChannel < ApplicationCable::Channel
def subscribed
# stream_from "some_channel"
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
end
dodajemy metodę broadcast
def self.broadcast(comment)
broadcast_to comment.post, comment:
CommentsController.render(partial: 'comments/comment', locals: { comment: comment })
end
zrobimy tu też uproszczenie polegające na tym, że subskrypcja będzie dotyczyła tylko ostatniego postu. Naszym celem jest pokazanie podstaw Rails, więc skupimy się na doprowadzenie do prezentacji mechanizmu kanałów, pomijając ten aspekt. W ramach tego uproszczenia piszemy
def subscribed
stream_for Post.last
end
Aby włączyć wysyłanie wiadomości do przeglądarki dodajemy linię
CommentsChannel.broadcast(comment)
za załączeniem e-mailera w kontrolerze komentarzy.
Do przeglądarki zostanie załączony plik z konfiguracją kanału app/javascript/channels/comments_channel.js
. Ustawiamy w nim, że w reakcji na załączenie komentarza do publikacji (kanału) powinien być on dołączany na końcu wątku, a licznik komentarzy powinien podnosić się o 1:
received(data) {
const commentsElement = document.querySelector('#comments');
const countElement = document.querySelector('#count');
if (commentsElement) {
commentsElement.innerHTML += data.comment
}
if (countElement) {
countElement.innerHTML = String(1 + parseInt(countElement.innerHTML))
}
}
Efekt jest następujący:
Do dalszej nauki polecam Ci materiały:
Other articles
You can find interesting also.
Broadcast Channel API
Wpis pokazuje jak używać Broadcast Channel API do przesyłania danych między kartami lub oknami przeglądarki bez wykorzystania serwera i socketów.
Daniel Gustaw
• 11 min read
Scraping z money.pl w 30 liniach kodu.
Zobacz proste case study pobrania i przetworzenia danych z paginowanej tabeli.
Daniel Gustaw
• 9 min read
Ile rodzin zmieści się w samolocie - zadanie z algorytmiki
Porównujemy dwa rozwiązania zadania polegającego na zliczaniu wolnych zestawów przyległych miejsc. Dowiesz się jak używać Profilowania i jak wielką różnicę robi użycie pop oraz shift na tablicach w js.
Daniel Gustaw
• 12 min read