{
 "subtemas": [],
 "tarjetas": [
  {
   "sub": "Spring Data JPA",
   "q": "Query derivation: keywords que se preguntan",
   "a": "findByEmailAndActivo(email, bool), findByNombreStartingWith/Containing/EndingWith, Between, IsNull/IsNotNull, In, OrderByEdadDesc (en el nombre), Top5/First3 (limit), findByEdadGreaterThanEqual.\nConteo: countBy...; existencia: existsBy...; borrado: deleteBy... (requiere @Transactional y carga entidades).\nSi el nombre crece demasiado → @Query.",
   "nivel": "basico",
   "clave": true
  },
  {
   "sub": "Spring Data JPA",
   "q": "@Query: JPQL vs nativo y cuándo cada uno",
   "a": "@Query(\"select p from Pedido p where p.total > :min\") — JPQL contra ENTIDADES (portable, tipo-aware con @Param).\n@Query(value=\"select * from pedidos...\", nativeQuery=true) — SQL real: funciones de BD, hints, UNION; acopla al motor.\nRetornos posibles: entidades, DTOs con constructor expression (select new com.x.Dto(p.id, p.total)), proyecciones de interfaz, scalars.",
   "nivel": "basico",
   "clave": true
  },
  {
   "sub": "Spring Data JPA",
   "q": "Proyecciones: interface-based y class-based",
   "a": "Interface projection: interface NombreVista { String getNombre(); BigDecimal getTotal(); } — el repo devuelve List<NombreVista>; Spring genera proxy que solo trae esas columnas (SQL optimizado).\nClass/DTO projection: @Query con 'select new com.x.Dto(p.nombre, sum(p.total))'.\nDynamic: <T> List<T> findBy...(Class<T> tipo).\nEvita cargar entidades para solo leer columnas.",
   "nivel": "basico",
   "clave": false
  },
  {
   "sub": "Spring Data JPA",
   "q": "JOIN FETCH y @EntityGraph: resolver N+1",
   "a": "N+1: 1 query de pedidos + N queries de cliente al tocar cada relación lazy.\n@Query(\"select p from Pedido p join fetch p.cliente where p.id in :ids\") — fetch join trae la relación EAGER en ESA query.\n@EntityGraph(attributePaths = {\"cliente\", \"items\"}) sobre el método del repo — mismo efecto declarativo.\nDistinct + fetch de colecciones → in-memory dedup (o hibernate.default_batch_fetch_size).",
   "nivel": "intermedio",
   "clave": true
  },
  {
   "sub": "Spring Data JPA",
   "q": "@Modifying y updates masivos",
   "a": "@Modifying + @Query(\"update Pedido p set p.estado = :e where p.creado < :fecha\") — UPDATE/DELETE directo en BD.\nRequiere @Transactional.\nNO actualiza el contexto de persistencia: entidades en memoria quedan desactualizadas (clearAutomatically=true para forzar).\nMucho más rápido que cargar+modificar miles de entidades.",
   "nivel": "intermedio",
   "clave": false
  },
  {
   "sub": "Spring Data JPA",
   "q": "save() vs saveAndFlush() vs merge",
   "a": "save: si es nueva → persist; si existe (no transient) → merge; el INSERT/UPDATE puede diferirse al flush.\nsaveAndFlush: fuerza el SQL YA (necesitas el id generado o triggers al instante).\nmerge: copia el estado de una entidad detached a una managed (devuelve la MANAGED — ignorar el retorno es un bug clásico).",
   "nivel": "basico",
   "clave": true
  },
  {
   "sub": "Spring Data JPA",
   "q": "Relaciones JPA: anotaciones y el lado dueño",
   "a": "@OneToOne, @OneToMany/@ManyToOne, @ManyToMany.\nEl lado DUEÑO tiene la FK: @JoinColumn(\"cliente_id\") en ManyToOne.\n@OneToMany(mappedBy=\"cliente\"): lado inverso (no dueño) — sin tabla intermedia propia.\nEn @ManyToMany ALWAYS solo un lado mappedBy + @JoinTable; cuida cascade y orphanRemoval.",
   "nivel": "basico",
   "clave": true
  },
  {
   "sub": "Spring Data JPA",
   "q": "Cascade: tipos y cuándo usarlos",
   "a": "PERSIST (guardar hijos con el padre), MERGE, REMOVE (borrar hijos con el padre — cuidado), ALL, REFRESH, DETACH.\nUso: ciclo de vida COMPLETAMENTE dependiente (Pedido→Items: ALL + orphanRemoval=true).\nNUNCA cascade a entidades compartidas (no pongas ALL de Pedido→Cliente: borrar un pedido no debe borrar un cliente).",
   "nivel": "basico",
   "clave": true
  },
  {
   "sub": "Spring Data JPA",
   "q": "orphanRemoval vs CascadeType.REMOVE",
   "a": "orphanRemoval=true: si QUITAS un hijo de la colección del padre, se DELETE de BD (los huérfanos se eliminan).\nCascadeType.REMOVE: borrar el PADRE borra a los hijos.\nCombinación estándar en composiciones: @OneToMany(mappedBy..., orphanRemoval=true, cascade=ALL).\nOrphan removal es reemplazo de hijo: pedido.getItems().remove(item) → DELETE.",
   "nivel": "intermedio",
   "clave": false
  },
  {
   "sub": "Spring Data JPA",
   "q": "Herencia en entidades: las 3 estrategias",
   "a": "@Inheritance(strategy=...):\nSINGLE_TABLE (default): una tabla + columna discriminador — rápida, columnas nullable.\nJOINED: tabla por clase unidas por FK — normalizada, joins en cada lectura.\nTABLE_PER_CLASS: tabla completa por clase concreta — uniones costosas, ids compartidos.\nDefault razonable: SINGLE_TABLE.",
   "nivel": "intermedio",
   "clave": false
  },
  {
   "sub": "Spring Data JPA",
   "q": "@Embeddable y @Embedded: value objects en entidades",
   "a": "Clase @Embeddable Direccion (calle, ciudad, cp) embebida en @Embedded Direccion direccion → columnas en la MISMA tabla.\n@AttributeOverrides renombra columnas si se repite.\nPerfecto para value objects inmutables con equals/hashCode (dinero, dirección, rango de fechas).",
   "nivel": "basico",
   "clave": false
  },
  {
   "sub": "Spring Data JPA",
   "q": "AttributeConverter: mapear enums/objetos custom",
   "a": "@Converter(autoApply=true) class RolConverter implements AttributeConverter<Rol, String> { convertToDatabaseColumn / convertToEntityAttribute }.\nCasos: enums por código legado, clases de valor (Dinero→2 columnas via Embeddable mejor), listas como JSON.\nAplica a todas las entidades sin repetir.",
   "nivel": "basico",
   "clave": false
  },
  {
   "sub": "Spring Data JPA",
   "q": "Optimistic locking con @Version",
   "a": "Campo @Version Long version en la entidad: cada UPDATE incluye WHERE version = leída y la incrementa.\nConcurrencia: segundo hilo con versión vieja → OptimisticLockException (o ObjectOptimisticLockingFailureException en Spring).\nSin locks de BD: ideal para baja contención.\nReintentar la operación es responsabilidad de la app.",
   "nivel": "intermedio",
   "clave": false
  },
  {
   "sub": "Spring Data JPA",
   "q": "Pessimistic locking: cuándo y cómo",
   "a": "Contención alta donde reintentar es caro (reservas, saldos): @Lock(LockModeType.PESSIMISTIC_WRITE) en el método del repo → SELECT ... FOR UPDATE (bloquea la fila hasta el commit).\nCon timeout: @QueryHints(@QueryHint(name=\"javax.persistence.lock.timeout\", value=\"3000\")).\nCuidado: orden consistente de bloqueo para evitar deadlocks.",
   "nivel": "intermedio",
   "clave": false
  },
  {
   "sub": "Spring Data JPA",
   "q": "Specifications (Criteria): queries dinámicas tipadas",
   "a": "extend JpaSpecificationExecutor<T>: findAll(Specification<T> spec, Pageable).\nConstruir con Specification.where(A.and(B)): (root, query, cb) -> cb.equal(root.get(\"estado\"), estado).\nCaso de uso: filtros de búsqueda construidos por combinación (campos opcionales).\nTipado con Metamodel para evitar strings.",
   "nivel": "intermedio",
   "clave": false
  },
  {
   "sub": "Spring Data JPA",
   "q": "Auditoría automática: @CreatedDate y compañía",
   "a": "@EnableJpaAuditing en config + @EntityListeners(AuditingEntityListener.class).\n@CreatedDate/@LastModifiedDate (fechas), @CreatedBy/@LastModifiedBy (necesitas AuditorAware<T> bean que devuelve el usuario actual).\nCampos auditables en un MappedSuperclass base para reusar en todas las entidades.",
   "nivel": "intermedio",
   "clave": false
  },
  {
   "sub": "Spring Data JPA",
   "q": "Batching: cómo hacer que inserts/updates masivos no sean 1 a 1",
   "a": "spring.jpa.properties.hibernate.jdbc.batch_size=50 + order_inserts=true + order_updates=true.\nREQUISITO con MySQL: rewriteBatchedStatements=true en la URL.\nOJO: IDs IDENTITY DESACTIVAN batching en inserts (usa SEQUENCE/SEQUENCE-style o asignados).\nGuardar en chunks de 50 con clear() periódico del contexto.",
   "nivel": "intermedio",
   "clave": false
  },
  {
   "sub": "Spring Data JPA",
   "q": "ddl-auto: valores y cuál en cada entorno",
   "a": "create: borra y crea TODO al arrancar (solo dev).\ncreate-drop: crea al arrancar, borra al cerrar (tests).\nupdate: altera tablas existentes (NUNCA en prod: sin rollback ni historial).\nvalidate: solo verifica el esquema (prod recomendado con migraciones).\nEsquema real de prod: Flyway/Liquibase + validate.",
   "nivel": "intermedio",
   "clave": true
  },
  {
   "sub": "Spring Data JPA",
   "q": "Flyway/Liquibase: migraciones versionadas",
   "a": "Scripts SQL versionados en resources/db/migration: V1__crear_tabla.sql, V2__add_column.sql.\nAl arrancar, Flyway aplica los pendientes en orden y registra en flyway_schema_history.\nBeneficios: esquema reproducible, migraciones revisadas en PR, rollback claro (V_down o revert).\nRegla: cambiar entidad = nueva migración, nunca editar la vieja aplicada.",
   "nivel": "intermedio",
   "clave": true
  },
  {
   "sub": "Spring Data JPA",
   "q": "Dirty checking: cómo Hibernate detecta cambios sin save()",
   "a": "Al cargar, Hibernate guarda snapshot del estado; al flush compara y genera UPDATE solo con columnas cambiadas.\nConsecuencia: dentro de una transacción, entidad cargada + setter = UPDATE automático (sin llamar save).\nFuera de transacción (o detached) no ocurre — por eso la gente 'pierde' cambios.",
   "nivel": "intermedio",
   "clave": false
  },
  {
   "sub": "Spring Data JPA",
   "q": "flush y el orden: queries antes del commit",
   "a": "Flush = enviar SQL pendiente a BD (sin commit). Ocurre: en commit, en queries JPQL (para ver cambios pendientes, flush AUTO), y con flush() explícito.\nTrampa: query nativa NO dispara flush de la entidad (FlushModeType.AUTO no aplica igual) → leer datos viejos; usar entityManager.flush() antes.\nCommit = flush + transacción confirmada.",
   "nivel": "intermedio",
   "clave": false
  },
  {
   "sub": "Spring Data JPA",
   "q": "Repositorios múltiples y @NoRepositoryBean",
   "a": "Base repository compartido: interface BaseRepo<T> extends JpaRepository<T, ID> con métodos comunes — anótala @NoRepositoryBean para que Spring NO la instancie.\nCada entidad extiende la base. También exist: fragmentos custom (interface custom + Impl) para lógica de acceso compartida.",
   "nivel": "basico",
   "clave": false
  },
  {
   "sub": "Spring Data JPA",
   "q": "Repositorio custom: interface + Impl (el patrón completo)",
   "a": "interface PedidoRepoCustom { List<Pedido> buscarComplejo(Filtro f); }\nclass PedidoRepoCustomImpl implements PedidoRepoCustom { @PersistenceContext EntityManager em; ... } — nombre CLAVE: Impl + nombre de la interface.\ninterface PedidoRepo extends JpaRepository<Pedido,Long>, PedidoRepoCustom {} — Spring fusiona ambos.\nPara SQL complejo y dinámico sin pelear con JPQL.",
   "nivel": "intermedio",
   "clave": false
  },
  {
   "sub": "Spring Data JPA",
   "q": "EntityManager vs repositorio: cuándo bajar al EntityManager",
   "a": "Repositorio: 90% de casos CRUD/queries nombradas.\nEntityManager (@PersistenceContext): queries criteria dinámicas, flush/clear manuales en procesos masivos, detach para liberar memoria, batching con clear cada N, locks con hints, referencias lazy getReference.\nRegla: repositorio primero; EM cuando elORM necesita control fino.",
   "nivel": "intermedio",
   "clave": false
  },
  {
   "sub": "Transacciones",
   "q": "Las 7 propagaciones de @Transactional (cuáles y cuándo)",
   "a": "REQUIRED (default): une o crea.\nREQUIRES_NEW: suspende la actual y abre una NUEVA (logs de auditoría que deben persistir aunque falle el negocio).\nNESTED: savepoint dentro de la actual (rollback parcial posible).\nSUPPORTS: usa si existe.\nNOT_SUPPORTED: suspende (código sin tx dentro de una).\nMANDATORY: exige tx existente (servicio interno).\nNEVER: prohíbe.",
   "nivel": "avanzado",
   "clave": true
  },
  {
   "sub": "Transacciones",
   "q": "readOnly=true: qué garantiza de verdad",
   "a": "Hint al driver y al ORM: Hibernate desactiva dirty checking y flush → consultas mucho más rápidas en lecturas masivas.\nEn BD con soporte (Postgres SET TRANSACTION READ ONLY) puede impedir escrituras.\nNO es una garantía de integridad por sí sola: úsalo en métodos de consulta como optimización + intención.",
   "nivel": "avanzado",
   "clave": false
  },
  {
   "sub": "Transacciones",
   "q": "Timeout y trampas del rollback",
   "a": "@Transactional(timeout=30): rollback si excede (protege locks).\nRollback por defecto SOLO en RuntimeException/Error; checked exceptions NO revierten → rollbackFor = Exception.class si el negocio lanza checked.\nnoRollbackFor para excepciones 'esperables' que no deben revertir el trabajo ya hecho.",
   "nivel": "avanzado",
   "clave": false
  },
  {
   "sub": "Transacciones",
   "q": "¿Qué pasa si @Transactional está en un método private o se llama desde el mismo bean?",
   "a": "NO hace nada (silenciosamente sin transacción):\n1) private/static/final: el proxy CGLIB no puede sobrescribirlo.\n2) Self-invocation: this.metodo() llama directo, SIN pasar por el proxy.\nFix: mover a otro bean, inyectarse a sí mismo (@Lazy), o TransactionTemplate programático.\nCausa #1 de 'mi transacción no funciona'.",
   "nivel": "avanzado",
   "clave": false
  },
  {
   "sub": "Transacciones",
   "q": "TransactionTemplate: la alternativa programática",
   "a": "transactionTemplate.execute(status -> { ...; return resultado; });\nRollback: status.setRollbackOnly() o excepción.\nVentajas: control explícito, transacciones PARCIALES dentro de un método (sin mover código a otro bean), imposible olvidar el proxy.\nInyecta PlatformTransactionManager.",
   "nivel": "avanzado",
   "clave": false
  },
  {
   "sub": "Transacciones",
   "q": "@Transactional y eventos: post-commit",
   "a": "@TransactionalEventListener(phase = AFTER_COMMIT): el listener corre SOLO si la tx confirmó — para notificaciones, emails, invalidar caché, publicar a otro sistema.\nBEFORE_COMMIT, AFTER_ROLLBACK, AFTER_COMPLETION.\n@EventListener normal corre DENTRO de la misma tx (verá datos sin confirmar).\nClave para no enviar emails de pedidos que luego fallaron.",
   "nivel": "avanzado",
   "clave": false
  },
  {
   "sub": "Transacciones",
   "q": "¿Qué es el problema de dual-write y cómo se evita?",
   "a": "Escribir en BD + publicar en cola/mensaje son 2 sistemas: puede confirmar uno y fallar el otro → inconsistencia.\nSoluciones: Outbox pattern (evento en tabla outbox en la MISMA tx + proceso que publica), eventos en la misma tx a un broker transaccional, o idempotencia + reconciliación.\nBase de la consistencia entre servicios.",
   "nivel": "avanzado",
   "clave": false
  },
  {
   "sub": "Transacciones",
   "q": "Isolation level: anomalías concretas que resuelve cada nivel",
   "a": "Dirty read (leer no confirmado): evita READ_COMMITTED+.\nNon-repeatable read (misma fila cambia entre lecturas): evita REPEATABLE_READ+.\nPhantom read (aparecen filas nuevas en el rango): evita SERIALIZABLE (aprox; Postgres usa MVCC).\nDefault típico: Postgres/Oracle READ_COMMITTED, MySQL REPEATABLE_READ.",
   "nivel": "avanzado",
   "clave": false
  },
  {
   "sub": "Transacciones",
   "q": "Transacciones y tests: @Transactional en tests hace rollback",
   "a": "@Transactional en un test: cada test corre en tx que se revierte al final — tests aislados y rápidos SIN limpiar BD.\nTrampas: no ves el commit real (problemas de constraints deferred, triggers de commit no se prueban), y con Testcontainers/threads separados NO comparten la tx (usa commit real ahí o TestTransaction).\nTestTransaction framework para control manual.",
   "nivel": "intermedio",
   "clave": false
  },
  {
   "sub": "Transacciones",
   "q": "Locks de BD y transacciones largas: por qué son veneno",
   "a": "Tx larga = locks sostenidos → otros hilos esperan (timeouts en cascada), conexiones del pool ocupadas, replays de GC del COMMIT...\nBuenas prácticas: transacciones cortas (abrir dentro del servicio justo para persistir), NUNCA I/O externa (HTTP, email) dentro de la tx, procesar en chunks.\nSíntoma: 'connection is not available, request timed out' (HikariCP).",
   "nivel": "avanzado",
   "clave": false
  }
 ]
}