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.
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
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']);
}
?>
$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)
]);
?>
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 => (
))}
);
}