Módulo 15-16: Rails Avanzado
Optimización, APIs y deployment de aplicaciones Rails
🚀 Módulo 15-16: Rails Avanzado
Duración: Semanas 19 y 20
Objetivos del Módulo
- Optimizar performance de aplicaciones Rails
- Implementar caching estratégico
- Crear APIs GraphQL profesionales
- Configurar deployment automatizado
- Aplicar monitoring y logging avanzado
Contenido Teórico
1. Performance y Optimización
N+1 Queries
# Problema: N+1 queries
def index
@posts = Post.all
# En la vista: @posts.each { |post| post.user.name } genera N queries adicionales
end
# Solución: Eager loading
def index
@posts = Post.includes(:user, :comments)
end
# Con condiciones
def index
@posts = Post.includes(:user)
.joins(:comments)
.where(comments: { approved: true })
.distinct
end
Database Indexing
# Migration para indices
class AddIndexesToPosts < ActiveRecord::Migration[7.0]
def change
add_index :posts, :user_id
add_index :posts, :published
add_index :posts, [:user_id, :published]
add_index :posts, :created_at
# Índice parcial
add_index :posts, :title, where: "published = true"
end
end
Query Optimization
class PostsController < ApplicationController
def index
@posts = Post.published
.includes(:user, :tags)
.order(created_at: :desc)
.limit(20)
# Paginación
@posts = @posts.page(params[:page]).per(10)
end
def search
@posts = Post.published
.joins(:user)
.where("posts.title ILIKE ? OR users.name ILIKE ?",
"%#{params[:q]}%", "%#{params[:q]}%")
.distinct
end
end
2. Caching Estratégico
Fragment Caching
<!-- app/views/posts/index.html.erb -->
<% @posts.each do |post| %>
<% cache post do %>
<article class="post">
<h2><%= post.title %></h2>
<p>By <%= post.user.name %></p>
<div><%= truncate(post.content, length: 200) %></div>
</article>
<% end %>
<% end %>
Russian Doll Caching
# Model
class Post < ApplicationRecord
belongs_to :user
has_many :comments, dependent: :destroy
# Actualizar cache cuando cambian comentarios
after_update :touch_user
private
def touch_user
user.touch
end
end
class Comment < ApplicationRecord
belongs_to :post
# Actualizar cache del post cuando cambia comentario
after_save :touch_post
after_destroy :touch_post
private
def touch_post
post.touch
end
end
<!-- Vista con Russian Doll Caching -->
<% cache @user do %>
<h1><%= @user.name %></h1>
<% cache "posts-#{@user.posts.maximum(:updated_at)}" do %>
<% @user.posts.each do |post| %>
<% cache post do %>
<%= render post %>
<% cache "comments-#{post.comments.maximum(:updated_at)}" do %>
<% post.comments.each do |comment| %>
<% cache comment do %>
<%= render comment %>
<% end %>
<% end %>
<% end %>
<% end %>
<% end %>
<% end %>
<% end %>
Application-level Caching
# config/environments/production.rb
config.cache_store = :redis_cache_store, { url: ENV['REDIS_URL'] }
# En modelos
class User < ApplicationRecord
def expensive_calculation
Rails.cache.fetch("user-#{id}-expensive-calc", expires_in: 1.hour) do
# Cálculo costoso aquí
complex_calculation
end
end
def self.popular_posts
Rails.cache.fetch("popular-posts", expires_in: 30.minutes) do
Post.joins(:likes)
.group('posts.id')
.order('COUNT(likes.id) DESC')
.limit(10)
end
end
end
3. APIs GraphQL
# Gemfile
gem 'graphql'
# Instalación
rails generate graphql:install
Definir Types
# app/graphql/types/user_type.rb
module Types
class UserType < Types::BaseObject
field :id, ID, null: false
field :name, String, null: false
field :email, String, null: false
field :posts, [Types::PostType], null: true
field :posts_count, Integer, null: false
def posts_count
object.posts.count
end
end
end
# app/graphql/types/post_type.rb
module Types
class PostType < Types::BaseObject
field :id, ID, null: false
field :title, String, null: false
field :content, String, null: false
field :published, Boolean, null: false
field :user, Types::UserType, null: false
field :comments, [Types::CommentType], null: true
field :created_at, GraphQL::Types::ISO8601DateTime, null: false
end
end
Queries
# app/graphql/types/query_type.rb
module Types
class QueryType < Types::BaseObject
field :posts, [Types::PostType], null: false do
argument :published, Boolean, required: false
argument :limit, Integer, required: false
end
field :post, Types::PostType, null: true do
argument :id, ID, required: true
end
field :users, [Types::UserType], null: false
def posts(published: nil, limit: nil)
posts = Post.all
posts = posts.where(published: published) if published.present?
posts = posts.limit(limit) if limit.present?
posts
end
def post(id:)
Post.find(id)
end
def users
User.all
end
end
end
Mutations
# app/graphql/mutations/create_post.rb
module Mutations
class CreatePost < BaseMutation
argument :title, String, required: true
argument :content, String, required: true
argument :published, Boolean, required: false
field :post, Types::PostType, null: false
field :errors, [String], null: false
def resolve(title:, content:, published: false)
post = context[:current_user].posts.build(
title: title,
content: content,
published: published
)
if post.save
{
post: post,
errors: []
}
else
{
post: nil,
errors: post.errors.full_messages
}
end
end
end
end
4. Deployment con Docker
Dockerfile
FROM ruby:3.2.0
RUN apt-get update -qq && apt-get install -y nodejs postgresql-client
WORKDIR /myapp
COPY Gemfile /myapp/Gemfile
COPY Gemfile.lock /myapp/Gemfile.lock
RUN bundle install
COPY . /myapp
# Precompile assets
RUN bundle exec rake assets:precompile
EXPOSE 3000
CMD ["rails", "server", "-b", "0.0.0.0"]
Docker Compose
# docker-compose.yml
version: '3.8'
services:
db:
image: postgres:14
environment:
POSTGRES_PASSWORD: password
POSTGRES_DB: myapp_development
volumes:
- postgres_data:/var/lib/postgresql/data
redis:
image: redis:7
web:
build: .
ports:
- "3000:3000"
depends_on:
- db
- redis
environment:
DATABASE_URL: postgresql://postgres:password@db:5432/myapp_development
REDIS_URL: redis://redis:6379/0
volumes:
- .:/myapp
volumes:
postgres_data:
5. CI/CD con GitHub Actions
# .github/workflows/test.yml
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:14
env:
POSTGRES_PASSWORD: postgres
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v3
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.2.0
bundler-cache: true
- name: Setup Database
run: |
bundle exec rails db:create
bundle exec rails db:migrate
env:
DATABASE_URL: postgres://postgres:postgres@localhost:5432/test
- name: Run tests
run: bundle exec rspec
env:
DATABASE_URL: postgres://postgres:postgres@localhost:5432/test
6. Monitoring y Logging
Configuración de Logs
# config/environments/production.rb
config.log_level = :info
config.log_tags = [:request_id, :remote_ip]
# Configurar lograge
config.lograge.enabled = true
config.lograge.formatter = Lograge::Formatters::Json.new
Health Checks
# app/controllers/health_controller.rb
class HealthController < ApplicationController
def show
checks = {
database: database_check,
redis: redis_check,
version: Rails.application.class.module_parent_name
}
status = checks.values.all? ? :ok : :service_unavailable
render json: checks, status: status
end
private
def database_check
ActiveRecord::Base.connection.execute('SELECT 1')
'OK'
rescue
'ERROR'
end
def redis_check
Redis.new.ping == 'PONG' ? 'OK' : 'ERROR'
rescue
'ERROR'
end
end
7. Security Best Practices
# config/application.rb
config.force_ssl = true # En producción
# Configurar CORS
# Gemfile
gem 'rack-cors'
# config/application.rb
config.middleware.insert_before 0, Rack::Cors do
allow do
origins 'localhost:3000', 'myapp.com'
resource '*',
headers: :any,
methods: [:get, :post, :put, :patch, :delete, :options, :head],
credentials: true
end
end
# Content Security Policy
# config/application.rb
config.content_security_policy do |policy|
policy.default_src :self, :https
policy.font_src :self, :https, :data
policy.img_src :self, :https, :data
policy.object_src :none
policy.script_src :self, :https
policy.style_src :self, :https, :unsafe_inline
end
Ejercicios Prácticos
Ve al archivo ejercicios.md para practicar los conceptos avanzados.
Proyecto del Módulo
Ve al archivo proyecto_api_completa.md para completar el proyecto final del curso.