[BE][Certs] Batch create registers in a single SQL query

This commit is contained in:
Araozu 2023-12-13 12:35:17 -05:00
parent ffb7518ef6
commit ecdf900299
7 changed files with 216 additions and 16 deletions

View File

@ -28,6 +28,21 @@ Si se quiere lanzar mediante Jenkins ya hay un `Jenkinsfile` una carpeta arriba.
Modificar lo que sea necesario para ejecutar en el entorno objetivo. Modificar lo que sea necesario para ejecutar en el entorno objetivo.
## Base de datos
**IMPORTANTE**: Este sistema (backend y frontend) depende de que la base de datos tenga ciertos
registros con ciertos ids, estos valores estan escritos en el código (hard coded)
- La tabla `custom_label` debe tener una fila con id: `0` y valor `<vacio>`
- La tabla `course` debe tener filas para los cursos matpel:
- Matpel 1 con id `10`
- Matpel 2 con id `11`
- Matpel 3 con id `12`
Estos valores deben estar en la base de datos. Si se cambiaron, es necesario
cambiarlos en el código (sería bueno sacarlos de .env).
## Funcionamiento ## Funcionamiento
Básicamente, al iniciar el backend realiza lo siguiente: Básicamente, al iniciar el backend realiza lo siguiente:

View File

@ -17,6 +17,8 @@ pub fn options_delete(_r: i32) -> Status {
#[post("/register/batch", format = "json", data = "<data>")] #[post("/register/batch", format = "json", data = "<data>")]
pub async fn insert_all(data: Json<Vec<RegisterCreate>>) -> (Status, Json<JsonResult<()>>) { pub async fn insert_all(data: Json<Vec<RegisterCreate>>) -> (Status, Json<JsonResult<()>>) {
// Old way, inserts one by one
/*
for register_create in data.iter() { for register_create in data.iter() {
let res = register_create.create().await; let res = register_create.create().await;
@ -30,6 +32,18 @@ pub async fn insert_all(data: Json<Vec<RegisterCreate>>) -> (Status, Json<JsonRe
} }
(Status::Ok, JsonResult::ok(())) (Status::Ok, JsonResult::ok(()))
*/
match RegisterCreate::batch_create(data.into_inner()).await {
Ok(_) => (Status::Ok, JsonResult::ok(())),
Err(err) => {
eprintln!("Error creating register: {:?}", err);
(
Status::InternalServerError,
JsonResult::err(format!("Error creando registros: {}", err)),
)
}
}
} }
#[get("/register/<dni>")] #[get("/register/<dni>")]

View File

@ -237,14 +237,14 @@ fn get_image_info(path: PathBuf) -> ScanInfo {
// Here, an iid is found. Extract it & return // Here, an iid is found. Extract it & return
let dni_length = p - 31; let dni_length = p - 31;
let equals_pos = match url.find('=') { let equals_pos =
Some(p) => p, match url.find('=') {
None => { Some(p) => p,
return ScanInfo::Error( None => return ScanInfo::Error(
"QR invalido: La URL tiene signo de interrogacion (?) pero no igual (=).".into(), "QR invalido: La URL tiene signo de interrogacion (?) pero no igual (=)."
) .into(),
} ),
}; };
let dni = url.chars().skip(31).take(dni_length).collect::<String>(); let dni = url.chars().skip(31).take(dni_length).collect::<String>();
let iid = url.chars().skip(equals_pos + 1).collect::<String>(); let iid = url.chars().skip(equals_pos + 1).collect::<String>();

View File

@ -1,9 +1,7 @@
use cors::Cors; use cors::Cors;
use once_cell::sync::OnceCell;
use sqlx::mysql::MySqlPoolOptions;
use sqlx::{MySql, Pool, MySqlConnection};
use std::env;
use sqlx::Connection; use sqlx::Connection;
use sqlx::MySqlConnection;
use std::env;
#[macro_use] #[macro_use]
extern crate rocket; extern crate rocket;

View File

@ -35,7 +35,9 @@ impl Course {
} }
}; };
let results = sqlx::query!("SELECT * FROM course").fetch_all(&mut db).await; let results = sqlx::query!("SELECT * FROM course")
.fetch_all(&mut db)
.await;
let results = match results { let results = match results {
Ok(res) => res, Ok(res) => res,

View File

@ -1,3 +1,5 @@
use std::collections::{HashMap, HashSet};
use rocket::form::validate::Contains; use rocket::form::validate::Contains;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::{Executor, Row}; use sqlx::{Executor, Row};
@ -138,6 +140,165 @@ impl RegisterCreate {
Ok(new_max) Ok(new_max)
} }
pub async fn batch_create(registers: Vec<RegisterCreate>) -> Result<(), String> {
// Collect all the courses ids in a set
let mut course_ids = std::collections::HashSet::<i32>::new();
for register in registers.iter() {
let course_id = register.course_id;
// TODO: Move hardcoded values to .env
// If the course id is 10, 11 or 12 (matpel ids), add all of them to the set
if course_id == 10 || course_id == 11 || course_id == 12 {
course_ids.insert(10);
course_ids.insert(11);
course_ids.insert(12);
} else {
course_ids.insert(course_id);
}
}
// Get next values from all courses ids
// This is a map of course_id -> next_register_code,
// and will be updated for every register to insert
let mut current_register_codes = Self::get_next_register_codes(course_ids).await?;
// If there's matpel courses (ids 10, 11 & 12), use only id 10, and set it to the highest value
if current_register_codes.contains_key(&10) {
// Get the highest value
let max = current_register_codes
.iter()
.filter(|(id, _)| *id == &10 || *id == &11 || *id == &12)
.map(|(_, value)| value)
.max()
.unwrap_or(&50);
current_register_codes.insert(10, *max);
}
// Create values for each register
let mut values = Vec::<String>::new();
for register in registers.iter() {
let course_id = register.course_id;
// This is the key in the hash map for the current course.
let course_code_key = if course_id == 10 || course_id == 11 || course_id == 12 {
// As per above, matpel courses share codes. Use the value at 10
10
} else {
course_id
};
// Get the next register code for this course
let next_register_code = *current_register_codes.get_mut(&course_code_key).unwrap() + 1;
// Get the custom label id
// This still requires several trips to the database, but:
// - Only if there's a custom label
// - Custom labels are used only in machineries certs
// - Usually there's between 1-2 machineries per request (I personally have never seen more than 3)
let custom_label_id = {
if register.custom_label.is_empty() {
1
} else {
// Get custom_label_id from db based of self.custom_label
let id = CustomLabel::get_id_by_value(&register.custom_label).await?;
if id > 0 {
id
} else {
CustomLabel::create(&register.custom_label).await?
}
}
};
// Current date in YYYY-MM-DD format
let current_date = chrono::Local::now().format("%Y-%m-%d").to_string();
// Create the value string
let value = format!(
"({}, '{}', '{}', {}, {}, {}, {})",
next_register_code,
current_date,
register.date,
custom_label_id,
register.is_preview,
register.person_id,
register.course_id
);
// Add it to the list
values.push(value);
// Update the current register code
current_register_codes.insert(course_code_key, next_register_code);
}
// Insert all in a single statement
let sql = format!(
"INSERT INTO register (
register_code,
register_creation_date,
register_display_date,
register_custom_label,
register_is_preview,
register_person_id,
register_course_id
) VALUES {}",
values.join(", ")
);
let mut db = db().await?;
match db.fetch_all(sql.as_str()).await {
Ok(_) => Ok(()),
Err(err) => {
log::error!("Error fetching course & person: {:?}", err);
return Err("Error inserting multiple registers".into());
}
}
}
async fn get_next_register_codes(
course_ids: HashSet<i32>,
) -> Result<HashMap<i32, i32>, String> {
let mut db = db().await?;
let sql = format!(
"SELECT MAX(register_code) AS max, register_course_id
FROM register
WHERE register_course_id IN ({})
GROUP BY register_course_id",
course_ids
.iter()
.map(|id| id.to_string())
.collect::<Vec<String>>()
.join(",")
);
log::info!("SQL: {}", sql);
let result = match db.fetch_all(sql.as_str()).await {
Ok(res) => res,
Err(err) => {
log::error!("Error fetching max register codes: {:?}", err);
return Err("Error fetching max register codes".into());
}
};
// Convert to a hashmap
let data = result
.iter()
.map(|row| {
(
row.try_get("register_course_id").unwrap(),
// TODO: This 50 (the minimum value for new registers) should be defined in .env
row.try_get("max").unwrap_or(50),
)
})
.collect();
Ok(data)
}
} }
#[derive(Serialize, Deserialize, Clone)] #[derive(Serialize, Deserialize, Clone)]

View File

@ -18,11 +18,14 @@ function wait(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms)); return new Promise((resolve) => setTimeout(resolve, ms));
} }
// TODO: Refactor
export function RegisterPreview(props: {selections: Array<RegistrationPreview>, personId: number | null, onDelete: (v: number) => void, onRegister: () => void}) { export function RegisterPreview(props: {selections: Array<RegistrationPreview>, personId: number | null, onDelete: (v: number) => void, onRegister: () => void}) {
const [error, setError] = createSignal("");
const [loading, setLoading] = createSignal(false); const [loading, setLoading] = createSignal(false);
const submit = async() => { const submit = async() => {
setLoading(true); setLoading(true);
setError("");
await wait(2000); await wait(2000);
@ -41,11 +44,12 @@ export function RegisterPreview(props: {selections: Array<RegistrationPreview>,
console.log("Create register: success"); console.log("Create register: success");
// Custom labels may have changed, reload them // Custom labels may have changed, reload them
loadCustomLabels(); loadCustomLabels();
props.onRegister();
} else { } else {
console.log(`error. ${result}`); console.log(`error. ${result}`);
setError(result);
} }
props.onRegister();
setLoading(false); setLoading(false);
}; };
@ -85,6 +89,12 @@ export function RegisterPreview(props: {selections: Array<RegistrationPreview>,
Registrar {props.selections.length} cursos Registrar {props.selections.length} cursos
</span> </span>
</button> </button>
<Show when={error() !== ""}>
<p class="bg-c-on-error text-c-error p-2 rounded-md">
{error()}
</p>
</Show>
</div> </div>
</FilledCard> </FilledCard>
); );
@ -140,6 +150,6 @@ async function createRegisters(data: RegisterBatchCreate): Promise<null | string
return null; return null;
} else { } else {
const data = await response.json(); const data = await response.json();
return JSON.stringify(data); return data.Error.reason;
} }
} }