# Active Record Avanzado

Referencia de patrones avanzados de Active Record para Rails 7+.

---

## Concerns — comportamiento compartido entre modelos

Los concerns encapsulan lógica reutilizable que múltiples modelos comparten.

```ruby
# app/models/concerns/trackable.rb
module Trackable
  extend ActiveSupport::Concern

  included do
    belongs_to :created_by_user, class_name: 'User', optional: true
    belongs_to :updated_by_user, class_name: 'User', optional: true

    before_create { self.created_by_user = Current.user }
    before_save   { self.updated_by_user = Current.user }
  end
end

# app/models/concerns/searchable.rb
module Searchable
  extend ActiveSupport::Concern

  included do
    scope :buscar, ->(termino) {
      where("titulo ILIKE :q OR descripcion ILIKE :q", q: "%#{sanitize_sql_like(termino)}%")
    }
  end
end

# Uso en modelo
class Post < ApplicationRecord
  include Trackable
  include Searchable
end
```

**Regla**: usar `sanitize_sql_like` siempre que el término de búsqueda viene del usuario.

---

## Counter Cache — contadores automáticos

Evita `SELECT COUNT(*)` repetidos en vistas de listas.

```ruby
# En la asociación (migration y modelo)
class Comment < ApplicationRecord
  belongs_to :post, counter_cache: true
end

# Migración para agregar la columna
class AddCommentsCountToPosts < ActiveRecord::Migration[7.1]
  def change
    add_column :posts, :comments_count, :integer, default: 0, null: false
    # Inicializar contadores en datos existentes
    Post.find_each { |p| Post.reset_counters(p.id, :comments) }
  end
end

# Uso — no ejecuta query adicional
post.comments_count  # lee la columna, no hace COUNT(*)
```

**Touch**: `belongs_to :post, counter_cache: true, touch: true` actualiza `updated_at` del post al crear/eliminar un comentario — útil para cache HTTP.

---

## Optimistic Locking con lock_version

Previene actualizaciones concurrentes que sobreescriben datos.

```ruby
# Migración
add_column :posts, :lock_version, :integer, default: 0

# Rails lo activa automáticamente al existir la columna.
# Cuando dos usuarios editan el mismo registro:
post_a = Post.find(1)  # lock_version = 5
post_b = Post.find(1)  # lock_version = 5

post_a.update!(titulo: "Versión A")  # lock_version → 6, OK
post_b.update!(titulo: "Versión B")  # lanza ActiveRecord::StaleObjectError
```

```ruby
# En el controller, rescatar y reintentar
def update
  @post.update!(post_params)
rescue ActiveRecord::StaleObjectError
  @post.reload
  flash.now[:alert] = 'Otro usuario modificó este registro. Revisa los cambios.'
  render :edit, status: :conflict
end
```

---

## Soft Delete con paranoia / discard

Dos opciones idiomáticas:

```ruby
# Opción A: gem 'discard' (más simple, recomendada para Rails 7+)
class Post < ApplicationRecord
  include Discard::Model
  default_scope -> { kept }
end

post.discard   # marca deleted_at, no elimina de BD
post.undiscard # restaura

# Migración necesaria
add_column :posts, :discarded_at, :datetime
add_index  :posts, :discarded_at

# Opción B: gem 'paranoia' (más funcionalidad, más magia)
class Post < ApplicationRecord
  acts_as_paranoid
end

post.destroy   # soft delete (sets deleted_at)
post.really_destroy!  # elimina permanentemente
Post.with_deleted      # incluye registros eliminados
```

---

## Single Table Inheritance (STI)

**Cuándo usar STI**: subtipos con comportamiento distinto pero mismos atributos base.
**Cuándo NO**: subtipos con atributos muy distintos → usar polimorfismo o tablas separadas.

```ruby
# Una sola tabla "vehicles" con columna "type"
class Vehicle < ApplicationRecord
  validates :marca, presence: true
end

class Car < Vehicle
  validates :puertas, numericality: { greater_than: 0 }
end

class Truck < Vehicle
  validates :capacidad_toneladas, numericality: { greater_than: 0 }
end

# Rails almacena el nombre de clase en la columna "type"
Car.create!(marca: "Toyota", puertas: 4)
Vehicle.all        # devuelve Cars y Trucks
Car.all            # solo Cars
```

**Anti-patrón**: columnas `null: true` en exceso por diferencias de atributos → señal de que STI no es la solución correcta.

---

## Bulk Operations (Rails 6+)

Para insertar o actualizar grandes volúmenes sin instanciar modelos.

```ruby
# insert_all — ignora duplicados por defecto
Post.insert_all([
  { titulo: "Post 1", status: "publicado", author_id: 1 },
  { titulo: "Post 2", status: "borrador",  author_id: 2 }
])

# upsert_all — actualiza si existe conflicto en la PK
Post.upsert_all(
  registros,
  unique_by: :titulo,
  update_only: [:status, :updated_at]
)

# NOTA: insert_all/upsert_all NO ejecutan callbacks ni validaciones.
# Usar solo para importaciones masivas donde el origen es confiable.
```

---

## ActiveRecord::Encryption (Rails 7+)

Cifrado transparente de columnas sensibles sin cambios en el código de negocio.

```ruby
# config/application.rb
config.active_record.encryption.primary_key        = Rails.application.credentials.ar_encryption[:primary_key]
config.active_record.encryption.deterministic_key  = Rails.application.credentials.ar_encryption[:deterministic_key]
config.active_record.encryption.key_derivation_salt = Rails.application.credentials.ar_encryption[:salt]

# Modelo
class User < ApplicationRecord
  encrypts :numero_tarjeta
  encrypts :email, deterministic: true  # permite búsquedas exactas
end

# Uso — transparente para el código
user.email           # devuelve valor descifrado
User.find_by(email: "a@b.com")  # funciona por ser deterministic: true
```

**Regla**: usar `deterministic: true` solo cuando necesitas buscar por el campo. Sin `deterministic`, el cifrado usa nonce aleatorio (más seguro).

---

## Callbacks — cuándo usar y anti-patrones

**Callbacks válidos:**
- `before_validation`: normalizar datos (`strip`, `downcase`)
- `before_save`: derivar valores calculados del mismo modelo
- `after_initialize`: valores default complejos

**Callbacks a evitar:**
```ruby
# MAL — request HTTP en callback (bloquea la transacción DB)
after_create :notificar_por_email  # si el mailer falla, la TX puede quedar abierta

# BIEN — delegar a un job
after_create_commit -> { NotificarJob.perform_later(id) }

# MAL — lógica de negocio en callback (difícil de testear y reutilizar)
before_save :calcular_precio_total_con_impuestos_y_descuentos

# BIEN — service object que llama al modelo con el valor ya calculado
```

---

## Query cache y EXPLAIN

```ruby
# Ver el plan de ejecución SQL de una query
Post.includes(:author).where(status: :publicado).explain
# Imprime EXPLAIN ANALYZE en PostgreSQL

# Query cache — Rails cachea queries idénticas dentro de la misma request
# Forzar recarga cuando el cache interfiere en tests:
ActiveRecord::Base.connection.clear_query_cache

# Índices que casi siempre se necesitan
add_index :posts, :status
add_index :posts, [:author_id, :created_at]  # índice compuesto para order frecuente
add_index :posts, :discarded_at              # si usas soft delete
```

---

## Referencias externas

- [Active Record Associations — Rails Guides](https://guides.rubyonrails.org/association_basics.html)
- [Active Record Query Interface](https://guides.rubyonrails.org/active_record_querying.html)
- [Active Record Encryption](https://guides.rubyonrails.org/active_record_encryption.html)
- [gem discard](https://github.com/jhawthorn/discard)
- [gem bullet (detección N+1)](https://github.com/flyerhzm/bullet)
