El propósito de este trabajo práctico se basa en el desarrollo de un sistema digital, destinado al supermercado ChangoOnline, el cual sea capaz de administrar el ingreso, la cancelación, y la entrega de pedidos. Además, se busca conservar toda la información de les clientes registrados y de los productos disponibles. Por último, se deberá controlar el stock de los productos y mantener a les clientes informados frente al estado de sus pedidos online.
A lo largo del informe mencionaremos las cuestiones de diseño respecto del sistema y, posteriormente, se detallarán las dificultades que se presentaron y las soluciones encontradas.
En primer lugar, las cuestiones de estructura y diseño se definieron, por un lado, sobre la aplicación CLI en lenguaje Go y, por otro, en base al contenido SQL encargado de realizar las consultas necesarias. En relación a esto, el CLI contiene un package main encargado de abrir una única conexión con la base de datos con la finalidad de que se lleven a cabo todas las operaciones establecidas y, además, muestra por consola las opciones que permiten ejecutar el código en un orden específico conjunto la visualización del resultado. De esta manera, se encuentran distintos archivos .go que se ocupan de:
-
Crear la base de datos.
-
Crear tablas.
-
Crear primary key y foreign key según corresponda.
-
Lectura de los datos .json (decoding) e insertar los registros en las tablas.
-
Realizar llamadas a los Stored Procedures y Triggers.
-
Visualizar en la consola el contenido de tablas y registros.
Todos estos archivos .go se vinculan con la base de datos para realizar las interacciones requeridas. Asimismo, cada Stored Procedure y Trigger se encuentra separado en un archivo .sql con el objetivo de mantener una organización y abstracción de los distintos puntos a resolver.
En segundo lugar, se presentaron ciertas dificultades durante el desarrollo del trabajo. Una de ellas, surgió al momento de implementar el procedimiento generar_solicitud_reposicion(), el mismo debía ejecutarse diariamente para generar peticiones de reposición a productos con bajo stock.
La dificultad encontrada fue que las validaciones y el retorno eran confusas; no se comprendía si se debian aplicar por cada producto o sobre la tabla en general. Para ello, la solución fue verificar en un principio que no exista alguna solicitud previa registrada con la fecha actual porque eso indicaría que el procedimiento ya se habia generado.
Y si eso no sucedía, entonces se consultaban aquellos productos que tengan bajo stock para registrar la solicitud en la tabla.
Por otra parte, se encontró una dificultad con respecto a la implementación de las Transacciones en determinados procedimientos. Dichas transacciones, se aplican sobre operaciones que manipulan valores que suelen ser consultados y modificados reiteradamente (por ejemplo, el stock_disponible y stock_reservado de los productos), con lo cual la primer idea fue establecer el nivel de aislamiento como serializable.
Sin embargo, se produjo un problema al corroborar su funcionamiento puesto que habian campos en donde se insertaban valores incorrectos o, en otros casos, no se veían afectados después de la ejecución.
De esta manera, optamos por mantener el nivel de aislamiento predeterminado de PostgreSQL READ COMMITTED debido a que no era necesario instanciar un nivel mayor de aislamiento que implicaría perder eficiencia y correr el riesgo de causar deadlocks.
También, tuvimos inconvenientes en selector_de_tablas.go para mostrar las tablas por consola, dado que se reutilizaban los strucs predefinidos que contenian tipos de datos como int, string y float64. Pero, habian datos en los strucs que no tenían ningún valor almacenado, por ende, eran null y al querer imprimirlos en pantalla, Go no permitía correr la función.
Para resolver esto, reemplazamos los tipos de datos en los strucs por sql.NullString, sql.NullINt64 y sql.NullFloat64 que verifican no sólo si hay un valor insertado y lo guarda, sino también que pueden tomar valores null en caso contrario.
Otro inconveniente fue el tema de la sintaxis, dado que no teníamos un editor que nos avise que olvidamos un ";" u otros tipos de errores de tipeo y no sabíamos en que línea de código nos habíamos equivocado. Por lo que cuando corríamos el programa nos tiraba un error y teniamos que buscar de donde provenía, debido a esto empezado a trabajar los problemas mas en bloques y viendo donde es que fallabamos.
El archivo SQL se encarga de crear el stored procedure creacion_del_pedido(int, int),
el cual toma como parámetro dos enteros, uno referenciado al id del cliente y
otro para la id de la dirección. Este devuelve false si se rechaza la creación del pedido,
insertando en la tabla error una fila con la información correspondiente del mismo,
y true si se concreta. A continuación, se explicarán las condiciones a cumplirse para cada caso.
-
Si el id del usuario no existe, carga el error ?id de usuarie no válido.
-
Si la id de la dirección de entrega no existe para el cliente, carga el error ?id de dirección no válido.
-
Si el código postal vinculado a la dirección de entrega no tiene una tarifa establecida, carga el error ?dirección de entrega fuera del área de atención.
-
Ninguna de las condiciones nombradas anteriormente tiene que cumplirse.
En caso de que se ejecute con éxito, se insertará una fila en la tabla pedido con los datos del cliente, la fecha, la hora del pedido y el costo de envío correspondiente al código postal, dejando su estado como ingresado.
link:stored-procedures/creacion_del_pedido.sql[role=include]El archivo SQL se encarga de crear el stored procedure agregado_de_producto(int, int, cantidad),
el cual toma como parámetro tres enteros, el primero referencia el id del pedido, el segundo el id del producto,
y el tercero la cantidad del producto añadido que se agregará al producto. Se encarga de agregar al pedido con el id
pasado como parámetro el producto con el id pasado como parámetro la cantidad de veces especificada previamente. Retorna un booleano
el cual a continuación se explicarán las condiciones a cumplir para que el valor del mismo sea true o false.
-
Si el id del pedido no existe, cargando el error con el mensaje ?id de pedido no válido.
-
Si el pedido no se encuentra en estado 'ingresado', cargando el error con el mensaje ?pedido cerrado.
-
Si el id de producto no existe, cargando el error ?id de producto no válido.
-
Que el producto tenga stock disponible para satisfacer la cantidad solicitada, cargando el error ?stock no disponible para el producto [id_producto].
-
Todos los condicionales previamente nombrados no deben cumplirse.
En caso de que se ejecute con éxito, se insertará una fila en la tabla pedido_detalle con los datos del producto. Si el pedido ya tiene el producto, se deberá actualizar la fila correspondiente, sumando la nueva cantidad. En cualquiera de los dos casos, se descuenta dicha cantidad del stock disponible y se suma al stock reservado. También se mantiene actualizado el monto total del pedido de forma coherente con el detalle de productos listados.
link:stored-procedures/agregado_de_producto.sql[role=include]El archivo SQL se encarga de crear el stored procedure cierre_del_pedido(int, timestamp), el cual retorna un booleano
el cual será true si se carga el pedido con éxito y false en caso contrario. Toma como parámetro el id del pedido y la fecha y hora de entrega.
A continuación, se explicarán las condiciones a cumplir para que el valor de retorno sea true o false.
-
Que el pedido con el id pasado como parámetro no exista, se cargará el error ?id de pedido no válido.
-
Que el pedido no tenga al menos un producto agregado, se cargará el error ?pedido vacío.
-
Que la fecha y hora de entrega no sea posterior a la actual, se cargará el error ?fecha de entrega no válida.
-
Todos los condicionales anteriormente mencionados no deben cumplirse.
En caso de éxito, se actualizará la fila correspondiente en la tabla pedido con la fecha de entrega, y las horas de entrega desde y hasta, dejando su estado como completado.
link:stored-procedures/cierre_del_pedido.sql[role=include]El archivo SQL se encarga de crear el stored procedure cancelacion_de_pedido(int), el cual retorna un booleano
que será true si se cancela el pedido con éxito y false en caso contrario. Este toma como parámetro el id del pedido.
A continuación, se explicarán las condiciones a cumplir para que el valor de retorno sea true o false.
-
El pedido con el id pasado como parámetro no existe, cargará el error ?id de pedido no válido.
-
Que el pedido no se encuentre en estado 'ingresado' o 'completado', cargará el error ?pedido ya entregado o cancelado.
En caso de que se ejecute con éxito, se marcará el estado del pedido como cancelado. Además, se deberá sumar al stock disponible de cada producto la cantidad incluida en el pedido cancelado, y descontarla del stock reservado.
link:stored-procedures/cancelacion_de_pedido.sql[role=include]El archivo SQL se encarga de crear el stored procedure entrega_pedido(int), el cual retorna un booleano
que será true si se marca el pedido como entregado o false en caso contrario.
Toma como parámetro un id referenciando el id del pedido.
A continuación, se explicarán las condiciones a cumplir para que el valor de retorno sea true o false.
-
El pedido con el id pasado como parámetro no existe, cargará el error ?id de pedido no válido.
-
Que el estado del pedido no se encuentre en 'completado', cargará el error ?pedido sin cerrar o ya entregado.
-
Ninguna de las condiciones anteriores se cumplan.
Si se ejecuta con éxito, se marcará el estado del pedido como entregado. Además, se restará el stock reservado de cada producto la cantidad incluida en el pedido entregado.
link:stored-procedures/entrega_pedido.sql[role=include]El archivo SQL se encargará de generar las solicitudes de reabastecimiento de los productos que tengan bajo stock mediante un stored procedure. Para ello, se inserta una fila en la tabla reposición por cada producto cuya suma del stock disponible y del reservado sea menor o igual al punto de reposición. La cantidad a reponer será la que permita llegar al stock máximo del producto y el estado de la solicitud se grabará como pendiente. La fecha de solicitud corresponderá a la fecha actual. El procedimiento retornará false en caso de que ya exista previamente, aunque sea una solicitud de reabastecimiento con la fecha igual a la actual, esto se basa en el razonamiento de que si existe aunque sea una solicitud con la fecha igual a la actual, el procedimiento fue ejecutado previamente en el día de la fecha. En caso contrario, retorna true.
link:stored-procedures/generar_solicitud_reposicion.sql[role=include]El archivo SQL se encargará de la generación de emails mediante un trigger, el cual se ejecuta cada vez que:
-
Cada vez que se haga efectivo el cierre de un pedido, se genera un email con el asunto 'Pedido aceptado', e indicando en el cuerpo del email los datos del pedido y el detalle de los productos solicitados.
-
Cada vez que un pedido sea cancelado, se genera un email con el asunto 'Pedido cancelado', e indicando en el cuerpo del email los datos del pedido y el detalle de productos.
-
Cada vez que un pedido sea entregado, se genera automáticamente un email con el asunto 'Pedido entregado', e indicando en el cuerpo del email los datos del pedido y el detalle de productos.
-
Y, por último, un email con el asunto 'Encuesta de satisfacción', incluyendo en el cuerpo del email un texto breve que lo invite a completar una encuesta.
link:stored-procedures/enviar_mail.sql[role=include]En este archivo .go se incluye el código correspondiente a la función CreateDatabase(), el cual crea la base de datos "nissero_albarracin_melo_banchero_db1". En caso de que ya haya sido creada previamente, la borra y la vuelve a crear.
link:bd/crear_bd.go[role=include]En este archivo .go se incluye el código relacionado con la creación de tablas sobre la base de datos "nissero_albarracin_melo_banchero_db1". Este toma como parámetros una base de datos SQL. A continuación, se nombran las tablas creadas.
-
cliente(id_usuarie int, nombre text, apellido text, dni int, fecha_nacimento date, telefono char(12), email text)
-
direccion_entrega(id_usuarie int, id_direccion_entrega int, direccion varchar(60), localidad varchar(30), codigo_postal char(8))
-
tarifa_entrega(codigo_postal_corto char(4), costo decimal(12,2))
-
producto(id_producto int, nombre text, precio_unitario decimal(12,2), stock_disponible int, stock_reservado int, punto_reposicion int, stock_maximo int)
-
pedido(id_pedido int, f_pedido timestamp, fecha_entrega date, hora_entrega_desde time, hora_entrega_hasta time, id_usuarie int, id_direccion_entrega int, monto_total decimal(12,2), costo_envio decimal(12,2), estado char(10))
-
pedido_detalle(id_pedido int, id_producto int, cantidad int, precio_unitario decimal(12,2))
-
reposicion(id_producto int, fecha_solicitud date, cantidad_a_reponer int, fecha_reposicion date, estado char(12))
-
error(id_error int, id_pedido int, f_pedido timestamp, id_usuarie int, id_direccion_entrega int, direccion varchar(60), localidad varchar(30), codigo_postal char(8), id_producto int, cantidad int, operacion char(12), f_error timestamp,motivo varchar(64))
-
entrada_trx_pedido(id_orden int, operacion char(12), id_usuarie int, id_direccion_entrega int, id_pedido int, id_producto int, cantidad int, fecha_hora_entrega timestamp)
link:bd/crear_tablas.go[role=include]En este archivo .go se incluye el código relacionado con la creación de primary y foreign keys mediante las funciones CreatePK() y CreateFK().
link:bd/crear_pk_fk.go[role=include]En este archivo .go se incluye el código relacionado con la eliminación de primary y foreign keys mediante las funciones DeletePK() y DeleteFK().
link:bd/borrar_pk_fk.go[role=include]En este archivo .go se incluye el código relacionado con mostrar por pantalla el contenido de las tablas dentro de la base de datos mediante las siguientes funciones.
-
VerErrores(*sql.DB)muestra el contenido de la tabla error. Esta función utiliza valores tiposql.NullStringysql.NullInt64, ya que esta tabla tiene valores que pueden ser nulos; por ende, no se pueden guardar en los tipos de datosintostringde Go. -
VerEmails(*sql.DB)muestra el contenido de la tabla emails. Al igual queVerErrores, utiliza valores tiposql.NullStringysql.NullInt64. -
VerReposicion(*sql.DB)muestra el contenido de la tabla reposicion. Similar a las dos funciones anteriores, también utiliza valores tiposql.NullString. -
VerPedidosDetalle(*sql.DB)muestra el contenido de la tabla pedido_detalle. -
VerPedidos(*sql.DB)muestra el contenido de la tabla pedido. De la misma manera queVerErrores, utiliza valores tiposql.NullStringysql.NullInt64. -
VerProductos(*sql.DB)muestra el contenido de la tabla producto. -
VerClientes(*sql.DB)muestra el contenido de la tabla cliente.
link:ver_tablas/selector_de_tablas.go[role=include]En este archivo estan contenidos los structs relacionados a las tablas de la base de datos. Algunos de estas structs contienen tipo de datos de la biblioteca database/sql (sql.NullString, sql.NullInt64 y sql.NullFloat64), esto se debe a que las columnas relacionadas a estos datos tienen valores que pueden ser null, y los tipos de datos int, string y float64 en Go no pueden tener valores null.
link:datajson/strucs.go[role=include]Este archivo se encarga de hacer el unmarshal de los JSON con el contenido de las tablas mediante Unmalvaviscador(), y de insertar el contenido en sus respectivas tablas mediante InsertarClientes(db *sql.DB), InsertarProductos(db *sql.DB), InsertarTarifaEntrega(db *sql.DB), InsertarDireccionesEntrega(db *sql.DB).
Además, realiza las transacciones llamando a los stored procedures en la función InsertarOperaciones(db *sql.DB). Aquí, por cada llamado a algún stored procedure que modifique el stock sobre la tabla producto, tendrá una transacción, junto a las sentencias BEGIN, SELECT stored_procedure(), SELECT pg_sleep(10) (para poder probar el paralelismo desde otra terminal), y finalmente COMMIT para finalizar con la transacción.
No se cambia el nivel de aislamiento, ya que por defecto, PostgreSQL lo pone como READ COMMITTED, lo cual resulta suficiente para evitar condiciones de carrera. Ponerlo como SERIALIZABLE implica perder eficiencia y tener la posibilidad de generar deadlocks.
Luego, en GenerarSolicitudesDeReposicion(db *sql.DB), se generan las solicitudes de reposición mediante el llamado a generar_solicitud_reposicion().
link:datajson/malvaviscador.go[role=include]En el main se encuentra el llamado a las demas funciones, mediante un menu en una CLI, mediante el input del usuario se elijen distintas opciones
-
Opcion 1 se crea la base de datos.
-
Opcion 2 se crean las tablas correspondientes.
-
Opcion 3 se crean las PK’s y FK’s.
-
Opcion 4 se eliminan las PK’s y FK’s
-
Opcion 5 se agregan los stored procedures.
-
Opcion 6 se hace unmarshall de los json’s y se suben los datos a las tablas.
-
Opcion 7 a 13 se muestra el contenido de las tablas
-
Opcion 14 se insertan las operaciones sobre la base de datos.
-
Opcion 15 se generan las solicitudes de reposicion
link:main.go[role=include]En este archivo .go se definen los Strucs correspondientes para la estructura de datos de la base NoSQL.
Además, hay una única función main() que se encarga de
-
Crear la base de datos BoltDB "nissero_albarracin_melo_banchero_db1".
-
Crear los datos hardcodeados en strucs de Cliente, Direccion_entrega, Producto, Pedido.
-
Cargar los clientes, direcciones de entrega, los productos y los pedidos a sus respectivos buckets llamando a
funciones.CrearBucket(db *bolt.DB, bucketName string, key []byte, val []byte). -
Leer los buckets de pedido almacenados llamando a
funciones.LeerBucket(db *bolt.DB, bucketName string, key []byte). -
Mostrar por consola el contenido de cada bucket.
link:bolt-db/bolt-db-main.go[role=include]Este archivo .go se encarga de crear el bucket con los valores que le llegan por parámetro. Asimismo, a través de una transacción de escritura se genera el bucket en caso de que no exista y, luego, cierra la transacción.
link:bolt-db/funciones/crear-bucket.go[role=include]Este archivo .go se encarga de leer el bucket que le llega por parámetro. De esta forma, por medio de una transacción de lectura se ocupa de leer el contenido del respectivo bucket.
link:bolt-db/funciones/leer-bucket.go[role=include]En conclusión, el proceso de desarrollo del trabajo al principio resultó complejo, debido a que no teniamos mucha experiencia en el manejo tanto del lenguaje Go, como de la interacción entre la base de datos SQL y la aplicación CLI. A pesar de ello, realizamos una estructura y diseño del sistema considerando el orden y la eficiencia del mismo. En tal sentido, este trabajo nos proporcionó llevar a la práctica lo visto en las clases previas.
Por un lado, fue interesante combinar lenguaje Go con código SQL e implementar sobre eso el manejo de concurrencia entre transacciones. Por otro lado, si bien el modelo relacional SQL tuvo más desarrollo, pudimos compararlo con un modelo no relacional NoSQL basado en JSON; la base de datos utilizada fue BoltDB. Esto permitió en cierta medida tener conocimiento sobre las diferentes estructuras para almacenar información que se pueden usar, según sea necesario, en nuestro proyecto.
Por último, se recurrió a la herramienta Git la cual facilitó mucho el trabajo en equipo y, también, utilizamos la aplicación Discord para mejorar la comunicación.