search 7. CRUD Avançado - READ

A operação READ é a mais complexa do CRUD em aplicações reais. Não basta retornar dados: você precisa implementar paginação (para não sobrecarregar o servidor), filtros dinâmicos (busca por categoria, autor, data), ordenação (mais recentes, mais populares) e joins eficientes para trazer dados relacionados sem N+1 queries.

Uma listagem mal otimizada pode retornar milhões de registros e derrubar seu servidor. Imagine um usuário acessando /api/posts e o backend fazendo SELECT * FROM posts sem LIMIT - em um banco com 500k posts, isso consumiria gigabytes de RAM e travaria a aplicação por minutos.

Nesta seção, você aprenderá a construir endpoints READ profissionais com paginação cursor-based, filtros compostos, busca full-text, joins otimizados e cache de resultados para máxima performance em ambientes de alta carga.

speed
📊 Performance: Um endpoint READ bem otimizado deve responder em <100ms mesmo com milhões de registros. A chave está em índices corretos, queries eficientes e paginação adequada.

view_list Tipos de Paginação

looks_one Offset-Based (Tradicional)

SELECT * FROM posts
ORDER BY created_at DESC
LIMIT 10 OFFSET 20;  -- Página 3

Prós: Simples, permite "ir para página X"
Contras: Performance degrada em offsets altos
OFFSET 100000 força scan de 100k linhas

fast_forward Cursor-Based (Moderna)

SELECT * FROM posts
WHERE id < 1234  -- Cursor do último item
ORDER BY id DESC
LIMIT 10;

Prós: Performance constante, ideal para feeds infinitos
Contras: Não permite "ir para página X"
Usado por Twitter, Facebook, Instagram

recommend
✅ Recomendação: Use offset-based para admin panels (usuário precisa navegar páginas) e cursor-based para feeds públicos (scroll infinito).

code Endpoint READ com Paginação e Filtros

 $status];
    
    if ($categoryId) {
        $whereClauses[] = 'p.category_id = :category_id';
        $params[':category_id'] = $categoryId;
    }
    
    if ($search) {
        $whereClauses[] = '(p.title LIKE :search OR p.content LIKE :search)';
        $params[':search'] = '%' . $search . '%';
    }
    
    $whereSQL = implode(' AND ', $whereClauses);
    
    // 4. QUERY PRINCIPAL COM JOIN
    $sql = "
        SELECT 
            p.id,
            p.title,
            p.slug,
            p.excerpt,
            p.views,
            p.likes,
            p.created_at,
            p.updated_at,
            c.name as category_name,
            u.name as author_name,
            u.avatar as author_avatar
        FROM posts p
        INNER JOIN categories c ON p.category_id = c.id
        INNER JOIN users u ON p.user_id = u.id
        WHERE $whereSQL
        ORDER BY p.$sortBy $sortOrder
        LIMIT :limit OFFSET :offset
    ";
    
    $stmt = $pdo->prepare($sql);
    
    // Bind de parâmetros
    foreach ($params as $key => $value) {
        $stmt->bindValue($key, $value);
    }
    $stmt->bindValue(':limit', $perPage, PDO::PARAM_INT);
    $stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
    
    $stmt->execute();
    $posts = $stmt->fetchAll();
    
    // 5. CONTAR TOTAL (para meta de paginação)
    $countSQL = "SELECT COUNT(*) FROM posts p WHERE $whereSQL";
    $countStmt = $pdo->prepare($countSQL);
    foreach ($params as $key => $value) {
        $countStmt->bindValue($key, $value);
    }
    $countStmt->execute();
    $total = $countStmt->fetchColumn();
    
    // 6. CALCULAR META
    $totalPages = ceil($total / $perPage);
    
    // 7. RETORNAR RESPOSTA
    echo json_encode([
        'success' => true,
        'data' => $posts,
        'meta' => [
            'current_page' => $page,
            'per_page' => $perPage,
            'total' => (int)$total,
            'total_pages' => (int)$totalPages,
            'has_next' => $page < $totalPages,
            'has_prev' => $page > 1
        ]
    ]);
    
} catch (Exception $e) {
    error_log("List Posts Error: " . $e->getMessage());
    http_response_code(500);
    echo json_encode(['success' => false, 'message' => 'Erro ao listar posts']);
}
?>
security
🔐 Segurança: Note o whitelist de campos em $allowedSortFields. Sem isto, um atacante poderia enviar ?sort_by=id; DROP TABLE posts-- e executar SQL arbitrário.

article Buscar Post Individual (Por ID ou Slug)

prepare($sql);
    $stmt->execute([$identifier]);
    $post = $stmt->fetch();
    
    if (!$post) {
        http_response_code(404);
        echo json_encode(['success' => false, 'message' => 'Post não encontrado']);
        exit;
    }
    
    // Incrementar views (assíncrono, não bloquear resposta)
    $pdo->prepare('UPDATE posts SET views = views + 1 WHERE id = ?')->execute([$post['id']]);
    
    // Buscar posts relacionados (mesma categoria)
    $relatedStmt = $pdo->prepare("
        SELECT id, title, slug, excerpt, created_at
        FROM posts
        WHERE category_id = ? AND id != ? AND status = 'published'
        ORDER BY created_at DESC
        LIMIT 3
    ");
    $relatedStmt->execute([$post['category_id'], $post['id']]);
    $relatedPosts = $relatedStmt->fetchAll();
    
    echo json_encode([
        'success' => true,
        'data' => $post,
        'related' => $relatedPosts
    ]);
    
} catch (Exception $e) {
    http_response_code(400);
    echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
?>

manage_search Busca Full-Text Avançada

Para buscas mais sofisticadas, use FULLTEXT INDEX do MySQL/MariaDB, que oferece ranking de relevância e busca booleana.

-- 1. Criar índice FULLTEXT (uma vez no banco)
ALTER TABLE posts ADD FULLTEXT INDEX ft_search (title, content, excerpt);

-- 2. Query com MATCH AGAINST
 false, 'message' => 'Query deve ter no mínimo 3 caracteres']);
    exit;
}

// Busca full-text com ranking de relevância
$sql = "
    SELECT 
        p.id,
        p.title,
        p.slug,
        p.excerpt,
        MATCH(p.title, p.content, p.excerpt) AGAINST(:query IN NATURAL LANGUAGE MODE) as relevance
    FROM posts p
    WHERE MATCH(p.title, p.content, p.excerpt) AGAINST(:query IN NATURAL LANGUAGE MODE)
    AND p.status = 'published'
    ORDER BY relevance DESC
    LIMIT 20
";

$stmt = $pdo->prepare($sql);
$stmt->execute([':query' => $query]);
$results = $stmt->fetchAll();

echo json_encode([
    'success' => true,
    'query' => $query,
    'results' => $results,
    'count' => count($results)
]);
?>
lightbulb
💡 Dica: Para buscas muito complexas (sinônimos, correção ortográfica, autocomplete), considere ferramentas especializadas como Elasticsearch, Meilisearch ou Algolia.

integration_instructions Integração React com Paginação

// frontend/src/hooks/usePosts.js
import { useState, useEffect } from 'react';
import axios from 'axios';

export const usePosts = (filters = {}) => {
  const [posts, setPosts] = useState([]);
  const [meta, setMeta] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  
  const fetchPosts = async (page = 1) => {
    setLoading(true);
    setError(null);
    
    try {
      const params = new URLSearchParams({
        page,
        per_page: 10,
        ...filters
      });
      
      const { data } = await axios.get(
        `${import.meta.env.VITE_API_URL}/posts/list?${params}`
      );
      
      setPosts(data.data);
      setMeta(data.meta);
    } catch (err) {
      setError(err.response?.data?.message || 'Erro ao carregar posts');
    } finally {
      setLoading(false);
    }
  };
  
  useEffect(() => {
    fetchPosts();
  }, [JSON.stringify(filters)]);
  
  return { posts, meta, loading, error, refetch: fetchPosts };
};

// USO NO COMPONENTE:
function PostsList() {
  const [page, setPage] = useState(1);
  const [filters, setFilters] = useState({ status: 'published' });
  
  const { posts, meta, loading, error } = usePosts({ ...filters, page });
  
  if (loading) return ;
  if (error) return {error};
  
  return (
    
{posts.map(post => ( ))}
); }
arrow_forward
✅ Próxima Seção: Agora vamos ao CRUD Avançado - UPDATE, aprendendo a fazer atualizações parciais, versionamento e prevenção de race conditions.