Tema 1 — Arquitectura SPA

Arquitectura de dos servidores

Una SPA separa frontend (recursos estáticos) y backend (API REST):

Servidor webSirve index.html, bundle JS, CSS, imágenes. Sin lógica de negocio. Puerto 5173 en dev.
Servidor de appSpring Boot. Expone API REST JSON. No genera HTML. Puerto 8080.
NavegadorDescarga index.html + JS bundle → React arranca → peticiones AJAX al backend.

Spring Boot

@SpringBootApplication  // = @Configuration + @ComponentScan + @EnableAutoConfiguration
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
mvn spring-boot:runDesarrollo. Arranca con classpath de Maven.
java -jar app.jarProducción. Fat JAR con Tomcat embebido incluido.

El fat JAR contiene Tomcat embebido → NO necesita un servidor de aplicaciones externo. Solo java -jar. (Pregunta 10b del examen: "instalarlo en servidor de aplicaciones Java" → FALSO)

application.yml

spring:
  datasource:
    url: jdbc:mysql://localhost/pa
    hikari:
      maximum-pool-size: 5   # Para tests: 1 (evita deadlocks)
  jpa:
    hibernate:
      ddl-auto: none         # No modifica el esquema
    open-in-view: false      # Recomendado

YAML: espacios, nunca tabuladores. application-test.yml SOBREESCRIBE propiedades de application.yml — NO lo reemplaza. Ambos se cargan.

Vite / Frontend

npm run dev    # Dev server → localhost:5173 (HMR)
npm run build  # Genera dist/ (bundle optimizado para producción)
npm run lint   # ESLint

Tema 2 — JPA: Entidades y Relaciones ★ EXAMEN

Entidades básicas

@Entity
@Table(name = "Product")
public class Product {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "productName")
    private String name;

    // JPA accede VÍA GETTERS (acceso por propiedad)
    public Long getId()     { return id; }
    public String getName() { return name; }
}

Regla fundamental de las relaciones ★ EXAMEN

REGLA DE ORO — memorizar:

  • Owning side (lado dueño) = el que tiene la FK en la tabla = siempre el lado N en 1:N = @ManyToOne + @JoinColumn
  • Inverse side (lado inverso) = el lado 1 en 1:N = @OneToMany(mappedBy=...)
  • mappedBy SOLO va en @OneToMany. Nunca en @ManyToOne.
  • @JoinColumn SOLO va en el lado dueño (ManyToOne). Nunca en el lado OneToMany.

Relación 1:N bidireccional — código correcto

// ── Lado N (owning side) — TIENE la FK en su tabla ──────────────────
@Entity
public class Account {

    @ManyToOne                      // ← @ManyToOne en el lado N
    @JoinColumn(name = "userId")    // ← @JoinColumn indica la columna FK
    private User user;

    public User getUser() { return user; }
}

// ── Lado 1 (inverse side) — NO tiene FK ──────────────────────────────
@Entity
public class User {

    @OneToMany(mappedBy = "user")   // ← mappedBy = nombre del campo en Account
    private Set<Account> accounts = new HashSet<>();

    public Set<Account> getAccounts() { return accounts; }
}
Pregunta 1 del examen

User-Account bidireccional 1:N. La tabla Account tiene columna userId (FK). ¿Cuál anotación es correcta?

a)@ManyToOne(mappedBy="accounts") sobre getUser() en Account← @ManyToOne NO acepta mappedBy. mappedBy solo existe en @OneToMany
b)@JoinColumn(name="userId") sobre getAccounts() en User← @JoinColumn va en el lado DUEÑO (Account), no en User
c)Todas las anteriores← ninguna es correcta
d)Ninguna de las anteriores

Relación 1:1

// User tiene bankCardId (FK) → User es el owning side
@Entity
public class User {
    @OneToOne(optional = false, fetch = FetchType.LAZY)
    @JoinColumn(name = "bankCardId")   // FK en tabla User
    private BankCard bankCard;
    public BankCard getBankCard() { return bankCard; }
}

En 1:1 igual que en N:1: @JoinColumn va en el lado que TIENE la FK. Si la tabla User tiene bankCardId, @JoinColumn va en User.getBankCard(), NO en BankCard con name="userId".

FetchType por defecto

AnotaciónFetchType por defecto
@ManyToOneEAGER
@OneToOneEAGER
@OneToManyLAZY
@ManyToManyLAZY

Spring Data CrudRepository

public interface ProductDao extends CrudRepository<Product, Long> {
    // Spring genera el SQL automáticamente por nombre del método:
    List<Product> findByCategoryIdOrderByName(Long categoryId);

    Optional<Product> findById(Long id);
    // Devuelve Optional → usar .get() o .orElse(null)
}

Paginación con Slice

Slice<Product> findByCategoryId(Long catId, Pageable pageable);

Pageable pageable = PageRequest.of(0, 10);  // página 0, tamaño 10
Slice<Product> slice = productDao.findByCategoryId(1L, pageable);

List<Product> items = slice.getContent();
boolean hasMore    = slice.hasNext();

// Slice NO hace COUNT(*) — solo comprueba si hay +1 elemento más

JPQL

// JPQL usa nombres de ENTIDADES y PROPIEDADES (no tablas/columnas)
@Query("SELECT p FROM Product p WHERE p.category.id = ?1 AND p.name LIKE ?2")
List<Product> find(Long catId, String kw);

@Query("SELECT p FROM Product p WHERE p.name LIKE :kw ORDER BY p.name")
Slice<Product> findByKeyword(@Param("kw") String keyword, Pageable p);

DAO personalizado

// 1. Interfaz con métodos custom
public interface CustomizedProductDao {
    Slice<Product> findByCriteria(ProductSearchCriteria criteria, int page);
}

// 2. Implementación — sufijo "Impl" obligatorio
public class CustomizedProductDaoImpl implements CustomizedProductDao {
    @PersistenceContext
    private EntityManager em;
}

// 3. DAO principal extiende ambos
public interface ProductDao
    extends CrudRepository<Product, Long>, CustomizedProductDao { }

Tema 2 — JPA: LAZY, queries, tests ★ EXAMEN

FetchType.LAZY — cuándo se lanza la consulta SQL ★ EXAMEN

getId() sobre un proxy LAZY NO lanza consulta. El ID ya está disponible en el proxy sin cargar el objeto. La consulta se lanza al acceder a CUALQUIER otra propiedad (getName(), getPrice(), etc.).

Event event = eventDao.findById(id).get();   // [A] query para Event
Category category = event.getCategory();     // [B] devuelve proxy, SIN query
Long id2    = category.getId();              // [C] NO lanza query (ID en proxy)
String name = category.getName();            // [D] AQUÍ se lanza la query
Pregunta 13 (examen 2013)

Event→Category LAZY. ¿En qué línea se lanza la consulta a BD?

a)Hibernate lanza consulta en [B]: event.getCategory()← devuelve proxy, no lanza query
b)Hibernate lanza consulta en [C]: category.getId()← getId() no inicializa el proxy
c)Hibernate lanza consulta en [D]: category.getName()
d)Lanza en [C] y en [D]← solo en [D]

LAZY en colecciones

Order order = orderDao.findById(id).get();     // [1] query para Order
Set<OrderItem> items = order.getOrderItems(); // [2] proxy de colección, SIN query
for (OrderItem item : items) {                // [3] AQUÍ se lanza query de OrderItems
    item.getId();                             // [4] no query (ya cargado)
    item.getDate();                           // [5] no query (ya cargado)
    Product p = item.getProduct();            // [6] proxy Product, SIN query
    p.getId();                                // [7] no query (ID en proxy)
    p.getName();                              // [8] AQUÍ se lanza query de Product
}
Pregunta 2 del examen

Order→OrderItems→Product todos LAZY. ¿Qué líneas causan consulta a BD?

a)[1], [2], [5] y [8]← [2] no lanza query
b)[1], [3], [4] y [7]← [4] y [7] son IDs, no lanzan query
c)[1], [3] y [8]
d)[1], [2] y [6]← [2] y [6] devuelven proxies sin lanzar query

@Version — Optimistic Locking ★ EXAMEN

@Entity
public class ShoppingCart {
    @Version
    private int version;  // Hibernate gestiona automáticamente
}
// UPDATE ShoppingCart SET ..., version=2 WHERE id=? AND version=1
// Si 0 filas actualizadas → alguien modificó antes → OptimisticLockException
// OptimisticLockException extiende RuntimeException → ROLLBACK automático
Pregunta 12 (examen 2013)

Dos usuarios ejecutan concurrentemente el mismo caso de uso sobre la misma entidad. ¿Cuál afirmación es correcta?

a)Solo con @Version: una actualización sobreescribe a la otra silenciosamente← sin @Version es cuando hay sobreescritura silenciosa
b)Con @Version: una transacción tiene éxito, la otra recibe una excepción runtime← esto es correcto pero no completo como respuesta
c)Ambas afirmaciones son correctas (sin @Version → sobreescritura silenciosa; con @Version → excepción runtime)
d)Ninguna← ambas son correctas

Tests de integración ★ EXAMEN

@SpringBootTest
@ActiveProfiles("test")    // Activa perfil test → carga application.yml + application-test.yml
@Transactional             // ← Auto-rollback tras cada @Test
public class UserServiceTest {

    @Autowired
    private UserService userService;

    @Test
    public void testSignUpAndLoginFromId() throws ... {
        User user = new User("login", "pass", "nombre", "apellido", "email");
        userService.signUp(user);

        User user2 = userService.loginFromId(user.getId());

        assertEquals(user, user2);  // Funciona SIN redefinir equals/hashCode
    }
}

4 trampas sobre tests (Pregunta 4):

  • NO hay que redefinir equals/hashCode: Hibernate devuelve el mismo objeto Java en la misma sesión (caché de primer nivel) → == funciona.
  • NO hay que eliminar el usuario manualmente: @Transactional en el test hace rollback automático tras cada test.
  • @ActiveProfiles("test") carga application.yml Y application-test.yml. El de test sobreescribe propiedades del principal, NO lo reemplaza.
  • maximum-pool-size: 1 en tests para evitar deadlocks.
Pregunta 4 del examen

Test con @SpringBootTest, @ActiveProfiles("test") y @Transactional. ¿Qué afirmación es correcta?

a)No hay que redefinir equals/hashCode para que assertEquals funcione
b)El desarrollador debe borrar el usuario creado explícitamente← @Transactional del test hace rollback automático
c)@ActiveProfiles('test') causa que ÚNICAMENTE se aplique application-test.yml← se cargan AMBOS yml; el de test sobreescribe al principal
d)La a) y la c)← c) es incorrecta

Tema 3 — Lógica de Negocio ★ EXAMEN

Inyección de dependencias

@Service                      // ← Bean de servicio (singleton)
public class CatalogService {

    private final ProductDao productDao;

    @Autowired                // ← Inyección (preferible en constructor)
    public CatalogService(ProductDao productDao) {
        this.productDao = productDao;
    }
}

@Repository                   // ← Bean de DAO. Activa traducción de excepciones
public interface ProductDao extends CrudRepository<Product, Long> { }

@Service va en la clase de servicio. @Autowired va en el punto de inyección (campo, constructor o setter). Son cosas distintas. (Pregunta 5a: "accountService debería anotarse con @Service en lugar de @Autowired" → FALSO)

@Transactional — semántica exacta ★ EXAMEN

Regla de rollback — memorizar:

  • Checked exception (Exception, IOException, InstanceNotFoundException...) → COMMIT
  • Unchecked exception (RuntimeException, Error, OptimisticLockException...) → ROLLBACK
  • Para rollback con checked: @Transactional(rollbackFor = MiException.class)
@Transactional
public void buy(Long cartId) throws InputValidationException {
    // Si lanza InputValidationException (checked) → COMMIT
    // Si lanza RuntimeException → ROLLBACK
}

@Transactional(readOnly = true)  // SÍ corre dentro de una transacción (solo lectura)
public ProductDto findProduct(Long id) throws NotFoundException { ... }

@Transactional — propagación ★ EXAMEN

Por defecto la propagación es REQUIRED: si ya existe una transacción activa, la segunda operación se une a ella (no crea una nueva). Si no existe, crea una nueva.

Pregunta 3 del examen

Servicio anotado con @Transactional. ¿Cuál afirmación es correcta?

a)Si lanza checked exception (ej. InstanceNotFoundException) → ROLLBACK← Checked exception → COMMIT por defecto
b)Con @Transactional(readOnly=true) la operación NO corre dentro de una transacción← Sí corre en transacción, pero solo de lectura
c)Si se invoca otra operación de otro servicio también @Transactional, por defecto se engancha a la transacción de la primera
d)Ninguna← c) es correcta

Domain Model — @Transient

@Entity
public class ShoppingCart {
    @OneToMany(mappedBy = "cart", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    private List<ShoppingItem> items;

    @Transient           // ← NO se persiste en BD, es método de negocio
    public BigDecimal getTotalPrice() {
        return items.stream()
            .map(i -> i.getPrice().multiply(BigDecimal.valueOf(i.getQuantity())))
            .reduce(BigDecimal.ZERO, BigDecimal::add);
    }
}

Tema 4 — REST Controllers ★ EXAMEN

Anotaciones REST

@RestController                    // = @Controller + @ResponseBody en todos los métodos
@RequestMapping("/catalog")
public class CatalogController {

    @Autowired
    private CatalogService catalogService;

    // GET /catalog/products?categoryId=1&keywords=matrix&page=0
    @GetMapping("/products")
    public ProductSearchResultDto findProducts(
        @RequestParam(required = false) Long categoryId,
        @RequestParam(required = false, defaultValue = "") String keywords,
        @RequestParam(defaultValue = "0") int page) { ... }

    // GET /catalog/products/42
    @GetMapping("/products/{id}")
    public ProductDto findById(@PathVariable Long id, Locale locale) { ... }

    // POST /catalog/products  — body JSON → DTO
    @PostMapping("/products")
    @ResponseStatus(HttpStatus.CREATED)     // 201
    public ProductDto add(
        @Validated @RequestBody ProductDto dto) { ... }
    //   ↑ valida    ↑ deserializa body JSON ← EXAMEN
}

@RequestBody — crítico ★ EXAMEN

Sin @RequestBody Spring no lee el cuerpo de la petición. @RequestBody le dice a Jackson que deserialice el JSON del body al DTO. Si falta → el parámetro será null o Spring lanzará error.

Pregunta 5 del examen

POST /accounts/transfer con @Validated TransferParamsDto params. ¿Qué falta?

a)accountService debería anotarse con @Service en lugar de @Autowired← @Service va en la clase servicio; @Autowired va en el punto de inyección
b)Falta anotar la clase con @Autowired← @Autowired no va en la clase, va en el campo/constructor
c)El cliente debe enviar parámetros en el PATH← son parámetros del body JSON, no del path
d)Falta especificar @RequestBody sobre el parámetro params

Parámetros REST — tipos

AnotaciónOrigenEjemplo
@PathVariableURL: /products/42@GetMapping("/products/{id}")
@RequestParamQuery string: ?page=0@RequestParam(defaultValue="0") int page
@RequestBodyBody JSON@RequestBody ProductDto dto
@RequestAttributeAtributo de request (set por filtros)Usado por JwtFilter para pasar userId

Bean Validation

public class BuyParamsDto {
    @NotNull
    private Long shoppingCartId;

    @NotBlank @Size(min=1, max=100)
    private String postalAddress;

    @NotBlank @Size(min=1, max=20)
    private String postalCode;
}

@PostMapping("/orders")
public OrderDto buy(@Validated @RequestBody BuyParamsDto dto) {
    // Si hay errores → MethodArgumentNotValidException → 400 Bad Request automático
}

Manejo de excepciones

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(InstanceNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)       // 404
    @ResponseBody
    public ErrorsDto handleNotFound(InstanceNotFoundException e) {
        return new ErrorsDto(e.getMessage());
    }

    @ExceptionHandler(InputValidationException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)      // 400
    @ResponseBody
    public ErrorsDto handleValidation(InputValidationException e) { ... }
}

Internacionalización

@GetMapping("/products/{id}")
public ProductDto find(@PathVariable Long id, Locale locale) throws ... {
    // locale inyectado por Spring desde el header Accept-Language
}

@Autowired private MessageSource messageSource;
String msg = messageSource.getMessage("project.error.notFound", null, locale);

Tema 4 — JWT y Spring Security ★ EXAMEN

JWT — estructura ★ EXAMEN

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEsInJvbGUiOiJVU0VSIn0.firma
└─── HEADER (base64url) ──────────────┘ └── PAYLOAD ─────────────────────────┘ └sig┘

Header  → {"alg":"HS256","typ":"JWT"}         — algoritmo de firma
Payload → {"userId":1,"role":"USER","exp":...} — NO cifrado, solo firmado
Firma   → HMAC-SHA256(header+"."+payload, secreto_del_servidor)
alg: HS256Simétrico: misma clave para firmar y verificar (solo el servidor la conoce)
EnvíoAuthorization: Bearer <token> — en el header HTTP, NO en la URL
Validez1 día (campo exp en el payload)
AlmacenamientosessionStorage (se borra al cerrar la pestaña)

El JWT se envía en el header Authorization, NO como parámetro de URL (?authorization=token). (Pregunta 10a → FALSO)

El JWT está en base64url, no en JSON directo. (Pregunta 10d: "tokens JWT en formato JSON" → FALSO)

Pregunta 10 del examen

¿Cuál afirmación es correcta sobre el backend de la asignatura?

a)JWT se pasa en parámetro HTTP ?authorization=tokenJWT← se pasa en el header Authorization: Bearer <token>
b)Para producción es necesario instalarlo en un servidor de aplicaciones Java← Fat JAR incluye Tomcat embebido → solo java -jar
c)No todo el control de acceso se puede hacer declarativamente en base a roles
d)Los tokens JWT se envían directamente en formato JSON← JWT está en base64url, no JSON puro

No todo el acceso se puede controlar con roles: p.ej. "un usuario solo puede ver SUS propios pedidos" requiere comprobación programática (no solo hasRole).

Spring Security — configuración

http
  .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
  .authorizeHttpRequests(auth -> auth
      .requestMatchers(HttpMethod.POST, "/users/login").permitAll()
      .requestMatchers(HttpMethod.POST, "/users/signUp").permitAll()
      .requestMatchers(HttpMethod.GET, "/catalog/**").permitAll()
      .requestMatchers("/shopping/**").hasRole("USER")
      .anyRequest().denyAll()   // ← todo lo demás: denegado
  );

CORS — por qué es necesario

Frontend: http://localhost:5173   ← origen A
Backend:  http://localhost:8080   ← origen B (distinto puerto = distinto origen)

Sin CORS → el navegador bloquea las peticiones fetch del frontend al backend
Con CORS → el backend añade cabeceras: Access-Control-Allow-Origin: *

El mismo origen = mismo protocolo + mismo host + mismo puerto. Si difiere cualquiera → orígenes distintos → Same-origin policy → se necesita CORS. Configurado en SecurityConfig.corsConfigurationSource().

Tema 5 — React: Fundamentos ★ EXAMEN

Componentes y JSX ★ EXAMEN

// Componente = función que devuelve JSX
const Greeting = ({ name }) => {
    return (
        <div className="container">   {/* ← className, NO class — EXAMEN */}
            <h1>{name}</h1>            {/* ← llaves para expresiones JS */}
            <p>{2 + 2}</p>
        </div>
    );
};

// Fragment — cuando no hay elemento raíz natural
const Pair = () => (
    <>
        <span>A</span>
        <span>B</span>
    </>    // no genera ningún elemento HTML
);

Trampas JSX (Pregunta 11):

  • Atributos en camelCase: className (no class), htmlFor (no for), onClick, onChange...
  • Variables JS con llaves {"{variable}"}, NO con @variable (eso es otro framework)
  • Los navegadores NO ejecutan JSX directamente — necesita transpilación con Babel/Vite
  • Componentes propios en Mayúscula; elementos HTML en minúscula
Pregunta 11 del examen

¿Cuál afirmación es correcta sobre JSX?

a)No todos los atributos HTML se pueden usar con nombre original (ej. className en vez de class)
b)Para incluir una variable JS en JSX se usa la expresión @variable← se usan llaves: {variable}
c)El navegador ejecuta ficheros JSX sin transformación previa← necesita transpilación (Babel/Vite)
d)Ninguna← a) es correcta

Reglas de los Hooks

  • Solo en componentes funcionales o custom hooks (no en clases ni funciones normales)
  • Solo al nivel raíz del componente — no dentro de if, for, funciones anidadas
  • Los custom hooks deben empezar por use

Props

// Pasar props
<ProductCard product={p} onBuy={handleBuy} />

// Recibir por destructuring
const ProductCard = ({ product, onBuy }) => { ... }

// key obligatoria en listas — usar ID de BD, no índice del array
products.map(p => <ProductCard key={p.id} product={p} />)

Tema 5 — React: useState, listas, Virtual DOM ★ EXAMEN

useState — comportamiento exacto ★ EXAMEN

const App = () => {
    const [todos, setTodos] = useState([]);
    //                                  ↑ solo se usa en el PRIMER render
    //   En renders siguientes, todos tiene el valor actual del estado

    const handleAddTodo = text => setTodos([{ text }, ...todos]);
    //    ↑ llamar a setTodos → RE-RENDERIZACIÓN del componente

    return (
        <div>
            <AddTodo onAddTodo={handleAddTodo}/>
            <Todos todos={todos}/>
        </div>
    );
};

3 trampas de useState (Pregunta 6):

  • useState([]) → el [] inicial solo se usa en el primer render. En renders siguientes todos tiene el valor actual, NO siempre [].
  • Llamar al setter (setTodos) → provoca re-renderización. ✓
  • todos está en estado LOCAL del componente, NO en Redux. Son conceptos distintos.
Pregunta 6 del examen

Componente App con useState([]) y handleAddTodo. ¿Cuál afirmación es correcta?

a)En cada renderización, la variable todos tomará el valor []← el [] solo se usa en el primer render; después tiene el valor actual
b)La ejecución de handleAddTodo causará que el componente se vuelva a renderizar
c)El valor de todos se guarda en el estado de Redux← useState es estado LOCAL, no Redux
d)Todas las anteriores← solo b) es correcta

Inmutabilidad del estado

// ❌ INCORRECTO — muta el array existente
const addTodo = text => {
    todos.push({ text });    // Muta! React no detecta cambio (misma referencia)
    setTodos(todos);
};

// ✅ CORRECTO — nuevo array
const addTodo = text => setTodos([{ text }, ...todos]);

// ✅ CORRECTO para objetos
setUser({ ...user, name: "nuevo" });

// Por qué importa: useSelector y React usan === para comparar

Listas y prop key

const ProductList = ({ products }) => (
    <ul>
        {products.map(p => (
            <li key={p.id}>   {/* ← key obligatorio, único entre siblings */}
                {p.name}       {/* ← usar ID de BD, no índice del array */}
            </li>
        ))}
    </ul>
);

La key debe ser el ID de BD, no el índice del array. Usar el índice causa bugs al reordenar porque React reutiliza elementos DOM equivocados.

Virtual DOM ★ EXAMEN

  1. setState() → React re-ejecuta la función del componente → genera nuevo árbol VDOM
  2. Compara nuevo VDOM con el anterior (reconciliation/diffing)
  3. Calcula el mínimo conjunto de cambios necesarios
  4. Aplica SOLO esos cambios al DOM real
Pregunta 8 del examen

¿Cuál afirmación sobre renderización es correcta?

a)Los componentes React se renderizan en el Virtual DOM
b)Los suscritos a Redux, cuando se re-renderizan, se renderizan en el DOM del navegador← también van por el Virtual DOM primero
c)Se puede usar Redux para que los suscritos se rendericen en el Virtual DOM← todos los componentes React van por Virtual DOM, con o sin Redux
d)Redux reduce el número de renderizaciones en el DOM con respecto a solo React← useSelector evita re-renders innecesarios del componente, pero los que se hacen siguen pasando por VDOM

Tema 6 — Redux ★ EXAMEN

Conceptos

StoreEstado global único. configureStore({reducer})
ActionObjeto plano: {type: 'TIPO', ...payload}
Action creatorFunción que crea y devuelve una action
ReducerFunción pura (state, action) => newState. Nunca muta.
SelectorFunción pura rootState => valor. Recibe el ESTADO RAÍZ.
dispatchLlama al reductor RAÍZ → actualiza store → notifica suscriptores

Reducer — reglas críticas ★ EXAMEN

const initialState = { messages: [] };

const reducer = (state = initialState, action) => {
    switch (action.type) {

        case 'MARK_MESSAGE_AS_READ':
            // ❌ INCORRECTO — muta el array y devuelve el mismo:
            // state.messages.find(m => m.id === action.messageId).read = true;
            // return state;  // ← misma referencia → useSelector NO detecta cambio

            // ✅ CORRECTO — nuevo array, nuevo objeto:
            return {
                ...state,
                messages: state.messages.map(m =>
                    m.id === action.messageId ? { ...m, read: true } : m
                )
            };

        default:
            return state;   // ← SIEMPRE devolver state en default
    }
};

Selectores — reciben el ESTADO RAÍZ ★ EXAMEN

// Estado shape con combineReducers:
// { app: {...}, users: {...}, catalog: {...}, messages: {...} }

// ❌ INCORRECTO — asumir que recibe solo la propiedad messages:
export const getNumberOfUnreadMessages = messages =>
    messages.filter(m => !m.read).length;

// ✅ CORRECTO — recibe el estado RAÍZ completo:
export const getNumberOfUnreadMessages = state =>
    state.messages.filter(m => !m.read).length;

useSelector — cuándo re-renderiza ★ EXAMEN

const UnreadMessagesIcon = () => {
    const count = useSelector(selectors.getNumberOfUnreadMessages);
    // Re-renderiza SOLO si el valor devuelto por el selector cambia (===)
    // Si se borra un mensaje ya leído → messages cambia → pero count no cambia
    // → useSelector detecta count === count → NO re-renderiza
    return <div>{count}</div>;
};

useSelector compara con === el valor anterior y el nuevo devuelto por el selector. Si messages cambia (nuevo array) pero el número de no leídos es el mismo, devuelve el mismo número → === → NO re-renderiza.

Pregunta 7 del examen

App de email. dispatch(markMessageAsRead). ¿Cuál afirmación es correcta?

a)El selector getNumberOfUnreadMessages recibe como parámetro la propiedad messages← recibe el estado RAÍZ completo, no una propiedad concreta
b)El reductor debe buscar el mensaje, poner read=true y devolver EL MISMO array← mutar y devolver el mismo array → useSelector no detecta cambio
c)dispatch(actions.markMessageAsRead(id)) provoca la ejecución del reductor RAÍZ de Redux
d)Cada vez que messages cambie, UnreadMessagesIcon se re-renderiza aunque no cambie el número← useSelector compara con ===; si el número no cambia, no re-renderiza

combineReducers y flujo completo

const rootReducer = combineReducers({
    app:      app.reducer,
    users:    users.reducer,
    catalog:  catalog.reducer,
    shopping: shopping.reducer,
});
// Estado: { app:{...}, users:{...}, catalog:{...}, shopping:{...} }

// Flujo: dispatch(action)
// → rootReducer(state, action)
//   → catalog.reducer(state.catalog, action)
//   → shopping.reducer(state.shopping, action)
// → nuevo estado en el store
// → useSelector compara (===) → re-renderiza si cambió

async/await — comportamiento en JS ★ EXAMEN

const handleSubmit = async event => {
    event.preventDefault();
    // [1]
    const response = await backend.shoppingService.buy(cartId, address, code);
    //              ↑ NO bloquea el thread JS
    // await suspende ESTA función async y libera el thread para otras tareas
    // El thread puede procesar otros eventos mientras espera la respuesta
    // Cuando llega la respuesta, la función se reanuda
    if (response.ok) { ... }
};
Pregunta 9 del examen

await backend.shoppingService.buy(...). ¿Cuál afirmación es correcta?

a)El thread de ejecución se queda bloqueado en [1] hasta que llega la respuesta← await NO bloquea el thread. JS es single-threaded no bloqueante; await suspende la función pero el thread queda libre
b)Se envía POST con dos parámetros HTTP en el cuerpo: postalAddress y postalCode← son campos de un objeto JSON; además el shoppingCartId va en la URL
c)Todas las anteriores← ninguna es correcta
d)Ninguna de las anteriores

Tema 7.1 — PA Shop: Arquitectura global

Estructura de módulos

src/
├── modules/
│   ├── app/        → App.jsx, Header.jsx, Body.jsx
│   ├── users/      → SignUp, Login, autenticación
│   ├── catalog/    → FindProducts, ProductDetails
│   ├── shopping/   → Buy, ShoppingCart, Orders
│   └── common/     → BackLink, Errors (reutilizables)
├── backend/
│   └── appFetch.js → abstracción HTTP + JWT automático
├── store/
│   └── rootReducer.js
└── i18n/messages/  → messages_en.js, messages_es.js

Módulo — estructura interna

// Cada módulo tiene:
// actions.js, actionTypes.js, reducer.js, selectors.js, components/, index.js

// index.js — re-exporta todo
export { default as FindProducts } from './components/FindProducts';
export { default as ProductDetails } from './components/ProductDetails';

import actions from './actions';
import * as selectors from './selectors';
export default { actions, reducer, selectors };

rootReducer y estado global

const rootReducer = combineReducers({
    app:      app.reducer,      // { locale }
    users:    users.reducer,    // { loggedIn, user, jwt }
    catalog:  catalog.reducer,  // { categories, productSearch }
    shopping: shopping.reducer, // { cart, lastOrderId }
});

main.jsx — todos los wrappers

root.render(
    <React.StrictMode>
        <Provider store={store}>           {/* Redux */}
            <IntlProvider locale={locale} messages={messages}>  {/* i18n */}
                <BrowserRouter>            {/* React Router */}
                    <App/>
                </BrowserRouter>
            </IntlProvider>
        </Provider>
    </React.StrictMode>
);

Body.jsx — routing condicional

const Body = () => {
    const loggedIn = useSelector(users.selectors.isLoggedIn);
    return (
        <Routes>
            <Route path="/*"                             element={<Home/>}/>
            <Route path="/catalog/product-details/:id"   element={<ProductDetails/>}/>
            {loggedIn  && <Route path="/shopping/buy"    element={<Buy/>}/>}
            {!loggedIn && <Route path="/users/login"    element={<Login/>}/>}
        </Routes>
    );
};

appFetch

const appFetch = async (method, path, body) => {
    const jwt = sessionStorage.getItem('serviceToken');
    const config = {
        method,
        headers: { 'Content-Type': 'application/json' },
    };
    if (jwt)  config.headers['Authorization'] = `Bearer ${jwt}`;
    if (body) config.body = JSON.stringify(body);

    const response = await fetch(`http://localhost:8080${path}`, config);
    const payload = response.status !== 204 ? await response.json() : null;
    return { ok: response.ok, payload };  // ← siempre { ok, payload }
};

Tema 7.2 — PA Shop: Buscar productos

Componentes controlados vs no controlados

No controladoControlado (usado en PA Shop)
Datos en el DOM → refDatos en estado local → useState
Leer con ref.current.valuevalue={state} + onChange
Difícil validarReact controla el valor en todo momento

FindProducts

const FindProducts = () => {
    const dispatch   = useDispatch();
    const navigate   = useNavigate();
    const categories = useSelector(catalog.selectors.getCategories);
    const [categoryId, setCategoryId] = useState('');
    const [keywords,   setKeywords]   = useState('');

    const handleSubmit = async event => {
        event.preventDefault();
        dispatch(catalog.actions.clearProductSearch());
        navigate('/catalog/find-products-result');  // Navega ANTES de la respuesta

        const response = await backend.catalogService.findProducts({...});
        if (response.ok) {
            dispatch(catalog.actions.findProductsCompleted({
                criteria, result: response.payload
            }));
        }
    };

    return (
        <Form onSubmit={handleSubmit}>
            <Form.Select value={categoryId} onChange={e => setCategoryId(e.target.value)}>
                <option value="">-- Todas --</option>
                {categories?.map(c => <option key={c.id} value={c.id}>{c.name}</option>)}
            </Form.Select>
            <Form.Control value={keywords} onChange={e => setKeywords(e.target.value)}/>
            <Button type="submit">Buscar</Button>
        </Form>
    );
};

Reducer del módulo catalog

const initialState = { categories: null, productSearch: null };

const productSearch = (state = initialState.productSearch, action) => {
    switch (action.type) {
        case actionTypes.FIND_PRODUCTS_COMPLETED:
            return action.productSearch;
        case actionTypes.CLEAR_PRODUCT_SEARCH:
            return initialState.productSearch;  // null
        default:
            return state;
    }
};

const reducer = combineReducers({ categories, productSearch });

FindProductsResult — paginación

const FindProductsResult = () => {
    const productSearch = useSelector(catalog.selectors.getProductSearch);

    if (!productSearch) return null;

    const { criteria, result: { items, existMoreItems } } = productSearch;

    const loadMore = async () => {
        const newCriteria = { ...criteria, page: criteria.page + 1 };
        const response = await backend.catalogService.findProducts(newCriteria);
        if (response.ok) {
            dispatch(catalog.actions.findProductsCompleted({
                criteria: newCriteria, result: response.payload
            }));
        }
    };

    return (
        <>
            <Products products={items}/>
            {existMoreItems && <Button onClick={loadMore}>Cargar más</Button>}
        </>
    );
};

Tema 7.3 — PA Shop: useEffect ★ EXAMEN

ProductDetails — estado local + useEffect

Los datos del detalle de producto se guardan en estado LOCAL (useState), no en Redux, porque solo los necesita este componente.

const ProductDetails = () => {
    const loggedIn = useSelector(users.selectors.isLoggedIn);
    const [product, setProduct] = useState(null);  // ← estado LOCAL
    const { id }   = useParams();
    const productId = Number(id);

    useEffect(() => {
        const findProductById = async productId => {
            if (!Number.isNaN(productId)) {
                const response = await backend.catalogService.findProductById(productId);
                if (response.ok) {
                    setProduct(response.payload);
                }
            }
        };
        findProductById(productId);
    }, [productId]);   // ← dependencias del efecto

    if (!product) return null;  // Primer render: product=null → no muestra nada

    return (
        <>
            <BackLink/>
            <h2>{product.name}</h2>
            {loggedIn && <AddToShoppingCart productId={productId}/>}
        </>
    );
};

useEffect — semántica completa ★ EXAMEN

useEffect(efecto, [dependencias])

Dependencias            │ Cuándo ejecuta {clean-up} + {efecto}
────────────────────────┼───────────────────────────────────────
Sin segundo argumento   │ Tras CADA render
[] (array vacío)        │ Solo tras el PRIMER render (mount)
[dep1, dep2]            │ Tras mount + cuando dep1 o dep2 cambian

Orden de ejecución:
  1. Primer render      → ejecuta {efecto}
  2. Re-render, dep ≠   → ejecuta {clean-up previo} + {efecto}
  3. Re-render, dep ==  → NO ejecuta nada
  4. Unmount            → ejecuta {clean-up}

Flujo de ProductDetails

[1] Primer render → product=null → devuelve null (no muestra nada)
[2] Tras ese render → se ejecuta el efecto → fetch al backend
[3] Llega respuesta → setProduct(response.payload) → RE-RENDER
[4] Segundo render → product tiene datos → muestra la vista
[5] productId no cambió → efecto NO se vuelve a ejecutar

Con StrictMode en desarrollo: el efecto se ejecuta dos veces (mount→unmount→mount). En ProductDetails esto envía 2 peticiones GET. Solo ocurre en desarrollo, no en producción.

Clean-up function

useEffect(() => {
    const subscription = eventBus.subscribe('topic', handleEvent);

    return () => {
        subscription.unsubscribe();  // Se ejecuta antes del próximo efecto o al unmount
    };
}, []);  // Solo se suscribe al montar y desuscribe al desmontar

Tema 7.4 — PA Shop: Comprar y formularios ★ EXAMEN

BuyForm — estructura completa

const BuyForm = ({ shoppingCartId }) => {
    const dispatch = useDispatch();
    const navigate = useNavigate();

    // Estado LOCAL del formulario (NO en Redux):
    const [postalAddress, setPostalAddress] = useState('');
    const [postalCode,    setPostalCode]    = useState('');
    const [backendErrors, setBackendErrors] = useState(null);
    const [formValidated, setFormValidated] = useState(false);

    let form;  // referencia al nodo DOM del formulario

    const handleSubmit = async event => {
        event.preventDefault();

        if (form.checkValidity()) {             // valida HTML5 programáticamente
            const response = await backend.shoppingService.buy(
                shoppingCartId, postalAddress, postalCode
            );
            if (response.ok) {
                dispatch(actions.buyCompleted(response.payload));
                navigate('/shopping/purchase-completed');
            } else {
                setBackendErrors(response.payload);   // errores → estado LOCAL
            }
        } else {
            setBackendErrors(null);
            setFormValidated(true);   // activa visualización de errores cliente
        }
    };

    return (
        <div>
            <Errors errors={backendErrors} onClose={() => setBackendErrors(null)}/>
            <Form ref={node => form = node}
                  validated={formValidated}
                  noValidate              {/* deshabilita validación automática del browser */}
                  onSubmit={e => handleSubmit(e)}>
                <Form.Group controlId="postalAddress">
                    <Form.Control
                        type="text"
                        value={postalAddress}
                        onChange={e => setPostalAddress(e.target.value)}
                        required                 {/* restricción HTML5 */}
                    />
                    <Form.Control.Feedback type="invalid">
                        <FormattedMessage id="...required"/>
                    </Form.Control.Feedback>
                </Form.Group>
                <Button type="submit">Comprar</Button>
            </Form>
        </div>
    );
};

Validaciones HTML5 — dos enfoques ★ EXAMEN

Enfoque declarativoEnfoque programático (PA Shop)
El browser ejecuta automáticamente al submitnoValidate desactiva ejecución automática
No hace falta código JS para lanzarlasDeveloper llama form.checkValidity()
Mensajes en idioma del navegadorMensajes personalizables con React Intl
Look-and-feel no personalizableLook-and-feel con React Bootstrap

3 trampas de formularios (Pregunta 12):

  • Los errores del backend se guardan en estado LOCAL (useState), NO en Redux.
  • Con noValidate (enfoque programático), las restricciones HTML5 NO se ejecutan automáticamente al pulsar submit — hay que llamar a form.checkValidity().
  • Aunque el backend valide, SÍ se deben especificar restricciones HTML5 en el frontend (para UX: feedback inmediato al usuario).
Pregunta 12 del examen

¿Cuál afirmación es correcta sobre el enfoque de desarrollo del frontend?

a)Los errores que puede devolver el backend se guardan en el estado de Redux← se guardan en estado LOCAL del componente (useState)
b)Las restricciones HTML5 se ejecutan automáticamente al pulsar submit← con noValidate NO se ejecutan automáticamente
c)No se deben especificar restricciones HTML5 si el backend ya valida← sí se especifican para dar feedback inmediato al usuario
d)Ninguna de las anteriores

El backend siempre debe validar todo (incluido lo que valida el frontend), por seguridad. Las validaciones del cliente son UX, no seguridad.

Tema 7.5 — PA Shop: CORS, recargas, sessionStorage

Recargas en Vite

Modifica un .jsxModifica un .js
Solo ese componente recarga (hot reload)Todo el frontend recarga (full reload)
En muchos casos, el estado local (useState) se conservaTodo el estado se pierde (Redux, useState, todo)

Para evitar re-autenticarse tras recargar un .js: al montar App se hace POST /users/loginFromServiceToken con el JWT de sessionStorage → el backend devuelve perfil de usuario y carrito.

JWT en sessionStorage

sessionStorage.setItem('serviceToken', jwt);     // Al autenticarse
sessionStorage.getItem('serviceToken');          // appFetch lo lee automáticamente
sessionStorage.removeItem('serviceToken');       // Al cerrar sesión

// localStorage: persiste al cerrar el navegador
// sessionStorage: se elimina al cerrar la pestaña ← PA Shop usa este

Same-origin policy y CORS

Mismo origen = mismo protocolo + mismo host + mismo puerto

http://localhost:5173  ≠  http://localhost:8080
                              ↑ puerto distinto = origen distinto

Sin CORS → navegador bloquea las peticiones fetch (Same-origin policy)
Con CORS → backend añade cabeceras en respuestas:
    Access-Control-Allow-Origin: *
    Access-Control-Allow-Methods: GET, POST, PUT, DELETE
    Access-Control-Allow-Headers: Authorization, Content-Type

CORS se configura en el backend (SecurityConfig.corsConfigurationSource()). Es el backend quien autoriza el acceso cross-origin, no el frontend.

Tema 8 — Microservicios, Docker, Mobile

Microservicios

Core µSBD propia. Casos de uso finales. Ej: UserService, CatalogService.
Composition µSCoordinan varios core µS. Sin BD propia.
GatewayFachada. Punto de entrada único para los clientes.

Docker

docker build -t my-backend .         # Crear imagen (código + dependencias + JRE)
docker run -p 8080:8080 my-backend   # Ejecutar contenedor
docker-compose up                    # Levantar todo el stack

Cloud

PúblicaAWS, Azure, Google Cloud. Infraestructura compartida.
PrivadaCloudFoundry, OpenShift. En servidores propios.

React Native

import { View, Text, Button } from 'react-native';
// View ≈ div, Text ≈ p/span
// NO hay HTML, NO hay DOM, NO hay CSS estándar
// NO usa un browser embebido → app NATIVA en iOS/Android

Apps híbridas

Capacitor/ElectronSPA dentro de un browser embebido en la app.
vs React NativeHíbridas más fáciles si ya tienes SPA. React Native tiene mejor rendimiento y look nativo.

Resumen Examen — Todas las trampas

JPA — Relaciones

@ManyToOne(mappedBy=...)INCORRECTO — mappedBy NO existe en @ManyToOne
@ManyToOne + @JoinColumnCORRECTO — va en el lado N (owning, tiene la FK)
@JoinColumn en @OneToManyINCORRECTO — @JoinColumn nunca en el lado inverso
@OneToMany(mappedBy=...)CORRECTO — mappedBy en el lado 1 (inverso)

JPA — LAZY

proxy.getId()NO lanza query (ID disponible en el proxy)
proxy.getName()SÍ lanza query (primera propiedad no-ID)
colección = order.getOrderItems()NO lanza query (devuelve proxy de colección)
primer for(item : colección)SÍ lanza query (primera iteración)

@Transactional

Checked exception → ROLLBACKFALSO → Checked exception → COMMIT
RuntimeException / Error → ROLLBACKCORRECTO
readOnly=true → no corre en transacciónFALSO — sí corre en transacción (solo lectura)
Propagación REQUIRED (default) → joinCORRECTO — se une a la transacción existente

Tests

@ActiveProfiles carga SOLO application-test.ymlFALSO — carga ambos yml; test sobreescribe al principal
Hay que borrar datos manualmente en tests @TransactionalFALSO — rollback automático
Necesitas equals/hashCode para assertEqualsFALSO — misma sesión Hibernate = mismo objeto Java
maximum-pool-size: 1 en testsCORRECTO — evita deadlocks

REST

@Service en lugar de @AutowiredFALSO — son distintos: @Service en la clase, @Autowired en el punto de inyección
Sin @RequestBody Spring lee el body JSONFALSO — @RequestBody es obligatorio para deserializar el body
JWT en parámetro URL ?authorization=tokenFALSO — va en header Authorization: Bearer token
Fat JAR necesita servidor de aplicaciones externoFALSO — incluye Tomcat embebido, solo java -jar
JWT en formato JSON puroFALSO — en base64url

React / JSX

classNameCORRECTO en JSX
classINCORRECTO en JSX
@variableINCORRECTO — se usan llaves: {variable}
El browser ejecuta JSX directamenteFALSO — necesita Babel/Vite para transpilar
useState([]) → todos siempre vale []FALSO — el [] solo es el valor inicial (primer render)
todos en useState = estado ReduxFALSO — es estado LOCAL del componente

Redux

Selector recibe solo su slice del estadoFALSO — recibe el estado RAÍZ completo
Reducer muta y devuelve el mismo objetoFALSO — debe devolver un objeto NUEVO
dispatch → llama al reductor RAÍZCORRECTO
useSelector re-renderiza si array cambia aunque el valor no cambieFALSO — compara con === el valor devuelto por el selector
await bloquea el thread JSFALSO — suspende la función async pero libera el thread

Formularios (T7)

Errores del backend → estado ReduxFALSO — estado LOCAL del componente (useState)
Con noValidate, restricciones HTML5 se ejecutan solasFALSO — hay que llamar form.checkValidity()
Si el backend valida, no hace falta restricciones en el frontendFALSO — el frontend también valida (UX)