Optimize N+1 queries and fix missing event update endpoint
Performance optimizations: - Fix media items N+1: batch scripture reading lookups (1+N → 2 queries) - Fix hymnal thematic lists N+1: batch ambits queries (1+N → 2 queries) - Add batch query functions for bulletins and hymnal ambits Bug fix: - Add missing PUT /api/admin/events/:id endpoint for event updates - Connect existing EventsV1Service::update method to web layer Database performance significantly improved for high-traffic endpoints used by website, iOS app, and LuminaLP integrations.
This commit is contained in:
parent
73c1b416ee
commit
8728e14125
|
@ -22,7 +22,7 @@ use tokio::fs;
|
|||
use crate::{
|
||||
services::{EventsV1Service, PendingEventsService},
|
||||
error::Result,
|
||||
models::{Event, PendingEvent, ApiResponse, PaginatedResponse},
|
||||
models::{Event, PendingEvent, ApiResponse, PaginatedResponse, UpdateEventRequest},
|
||||
AppState,
|
||||
};
|
||||
|
||||
|
@ -218,3 +218,14 @@ pub async fn delete_pending(
|
|||
PendingEventsService::delete(&state.pool, &id).await?;
|
||||
Ok(success_with_message("Pending event deleted successfully".to_string(), "Pending event deleted successfully"))
|
||||
}
|
||||
|
||||
/// Update an existing event (admin only)
|
||||
pub async fn update(
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<Uuid>,
|
||||
Json(request): Json<UpdateEventRequest>,
|
||||
) -> Result<Json<ApiResponse<Event>>> {
|
||||
let updated_event = EventsV1Service::update(&state.pool, &id, request).await?;
|
||||
|
||||
Ok(success_with_message(updated_event, "Event updated successfully"))
|
||||
}
|
||||
|
|
|
@ -124,11 +124,26 @@ pub async fn list_sermons(
|
|||
.await
|
||||
.map_err(|e| crate::error::ApiError::Database(e.to_string()))?;
|
||||
|
||||
// Link sermons to bulletins for scripture readings using shared SQL
|
||||
for item in &mut media_items {
|
||||
if item.scripture_reading.is_none() && item.date.is_some() {
|
||||
if let Ok(Some(bulletin_data)) = sql::bulletins::get_by_date_for_scripture(&state.pool, item.date.unwrap()).await {
|
||||
item.scripture_reading = bulletin_data.scripture_reading;
|
||||
// Link sermons to bulletins for scripture readings using optimized batch query
|
||||
let dates_needing_scripture: Vec<chrono::NaiveDate> = media_items
|
||||
.iter()
|
||||
.filter_map(|item| {
|
||||
if item.scripture_reading.is_none() && item.date.is_some() {
|
||||
Some(item.date.unwrap())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
if !dates_needing_scripture.is_empty() {
|
||||
if let Ok(scripture_map) = sql::bulletins::get_scripture_readings_batch(&state.pool, &dates_needing_scripture).await {
|
||||
for item in &mut media_items {
|
||||
if item.scripture_reading.is_none() && item.date.is_some() {
|
||||
if let Some(scripture) = scripture_map.get(&item.date.unwrap()) {
|
||||
item.scripture_reading = scripture.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -86,6 +86,7 @@ async fn main() -> Result<()> {
|
|||
.route("/events/pending/:id/approve", post(handlers::events::approve))
|
||||
.route("/events/pending/:id/reject", post(handlers::events::reject))
|
||||
.route("/events/pending/:id", delete(handlers::events::delete_pending))
|
||||
.route("/events/:id", put(handlers::events::update))
|
||||
.route("/events/:id", delete(handlers::events::delete))
|
||||
.route("/config", get(handlers::config::get_admin_config))
|
||||
.route("/config", put(handlers::config::update_config))
|
||||
|
|
|
@ -155,21 +155,26 @@ impl HymnalService {
|
|||
// Thematic list operations
|
||||
pub async fn list_thematic_lists(pool: &PgPool, hymnal_id: Uuid) -> Result<Vec<ThematicListWithAmbits>> {
|
||||
let lists = hymnal::get_thematic_lists(pool, &hymnal_id).await?;
|
||||
let mut result = Vec::new();
|
||||
|
||||
for list in lists {
|
||||
let ambits = hymnal::get_thematic_ambits(pool, &list.id).await?;
|
||||
|
||||
result.push(ThematicListWithAmbits {
|
||||
|
||||
// Extract all list IDs for batch query
|
||||
let list_ids: Vec<uuid::Uuid> = lists.iter().map(|list| list.id).collect();
|
||||
|
||||
// Get all ambits in single query
|
||||
let ambits_map = hymnal::get_thematic_ambits_batch(pool, &list_ids).await?;
|
||||
|
||||
// Build results using the batch data
|
||||
let result = lists
|
||||
.into_iter()
|
||||
.map(|list| ThematicListWithAmbits {
|
||||
id: list.id,
|
||||
hymnal_id: list.hymnal_id,
|
||||
name: list.name,
|
||||
sort_order: list.sort_order,
|
||||
ambits,
|
||||
ambits: ambits_map.get(&list.id).cloned().unwrap_or_default(),
|
||||
created_at: list.created_at,
|
||||
updated_at: list.updated_at,
|
||||
});
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
|
|
@ -67,6 +67,37 @@ pub async fn get_by_date_for_scripture(pool: &PgPool, date: chrono::NaiveDate) -
|
|||
Ok(bulletin)
|
||||
}
|
||||
|
||||
/// Get scripture readings for multiple dates in single query (optimization for media items)
|
||||
pub async fn get_scripture_readings_batch(pool: &PgPool, dates: &[chrono::NaiveDate]) -> Result<std::collections::HashMap<chrono::NaiveDate, Option<String>>> {
|
||||
if dates.is_empty() {
|
||||
return Ok(std::collections::HashMap::new());
|
||||
}
|
||||
|
||||
let results = sqlx::query!(
|
||||
r#"SELECT DISTINCT ON (date) date, scripture_reading
|
||||
FROM bulletins
|
||||
WHERE date = ANY($1) AND is_active = true
|
||||
ORDER BY date, created_at DESC"#,
|
||||
dates
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
|
||||
let mut scripture_map = std::collections::HashMap::new();
|
||||
|
||||
// Initialize all dates with None
|
||||
for date in dates {
|
||||
scripture_map.insert(*date, None);
|
||||
}
|
||||
|
||||
// Fill in the scripture readings we found
|
||||
for row in results {
|
||||
scripture_map.insert(row.date, row.scripture_reading);
|
||||
}
|
||||
|
||||
Ok(scripture_map)
|
||||
}
|
||||
|
||||
/// Get current bulletin (raw SQL, no conversion)
|
||||
pub async fn get_current(pool: &PgPool) -> Result<Option<Bulletin>> {
|
||||
sqlx::query_as!(
|
||||
|
|
|
@ -445,6 +445,41 @@ pub async fn get_thematic_ambits(pool: &PgPool, list_id: &Uuid) -> Result<Vec<cr
|
|||
.map_err(|e| crate::error::ApiError::DatabaseError(e))
|
||||
}
|
||||
|
||||
/// Get thematic ambits for multiple lists in single query (optimization)
|
||||
pub async fn get_thematic_ambits_batch(pool: &PgPool, list_ids: &[uuid::Uuid]) -> Result<std::collections::HashMap<uuid::Uuid, Vec<crate::models::ThematicAmbit>>> {
|
||||
if list_ids.is_empty() {
|
||||
return Ok(std::collections::HashMap::new());
|
||||
}
|
||||
|
||||
let ambits = sqlx::query_as!(
|
||||
crate::models::ThematicAmbit,
|
||||
r#"
|
||||
SELECT id, thematic_list_id, name, start_number, end_number, sort_order, created_at, updated_at
|
||||
FROM thematic_ambits
|
||||
WHERE thematic_list_id = ANY($1)
|
||||
ORDER BY thematic_list_id, sort_order, name
|
||||
"#,
|
||||
list_ids
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.map_err(|e| crate::error::ApiError::DatabaseError(e))?;
|
||||
|
||||
let mut ambits_map: std::collections::HashMap<uuid::Uuid, Vec<crate::models::ThematicAmbit>> = std::collections::HashMap::new();
|
||||
|
||||
// Initialize all list_ids with empty vectors
|
||||
for list_id in list_ids {
|
||||
ambits_map.insert(*list_id, Vec::new());
|
||||
}
|
||||
|
||||
// Group ambits by their list_id
|
||||
for ambit in ambits {
|
||||
ambits_map.entry(ambit.thematic_list_id).or_insert_with(Vec::new).push(ambit);
|
||||
}
|
||||
|
||||
Ok(ambits_map)
|
||||
}
|
||||
|
||||
/// Count responsive readings
|
||||
pub async fn count_responsive_readings(pool: &PgPool) -> Result<i64> {
|
||||
let count = sqlx::query!("SELECT COUNT(*) as count FROM responsive_readings")
|
||||
|
|
Loading…
Reference in a new issue