🚀 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.