Simple ejemplo de como usar la mezcla de express con mongoose (mongoDB librería) para interactuar de manera CRUD con un objeto.
Quiero hacer un stress en la idea que esto es una sugerencia de como atacar el problema de interactuar de manera CRUD con un objeto usando las liberías de node.sj expressjs y mongoosejs; No un "Ud siempre debe hacerlo asi"
Para que el ejemplo funcione debemos tener los puntos descritos abajo en nuestra maquina de desarrollo (idealmente en sus últimas versiones). Se dejan las respectivas instalaciones como ejercicio al lector.
- Node.js
- MongoDB
- ExpressJS
- Mongoose
Una manera rapida de partir es definiendo el vector solución en dos dimensiones: La definición del objeto (modelo) y las rutas web que queremos usar para "hablar" con él.
En este ejemplo usaré productos de un supermercado, cada producto tendrá un nombre, descripción y precio, y sus tipos de datos serán respectivamente: nombre, descripción y precio.
Creemos un archivo /models/producto.js (Hint: Todos nuestras representaciones de objectos de datos irán en este directorio)
Usemos el siguiente código:
var Schema = require('mongoose').Schema
var producto_schema = new Schema({
nombre : String,
descripcion : String,
precio : Number
})
module.exports = producto_schema
En sintesís, le estamos diciendo a nodejs que vamos a crear un objeto de mongoose de tipo Schema
donde cada documento usa los campos nombre
, descripcion
y producto
, junto a sus respectivos tipos. La última línea es vital para que podamos usar el código del archivo fuera de este (i.e.: Que este sea llamado por el programa app.js que lo va a ejecutar).
Como estamos testeando, démosle algunos documentos a mongoDB vía su consola, nuestra base de datos se llamará supermercado.
$ mongo
MongoDB shell version: 2.0.4
connecting to: test
> use supermercado
switched to db supermercado
> db.productos.update({},{ nombre: 'Papas Fritas', descripcion: 'Crujientes, sabor mediterraneo', precio: 2.5 }, true, false)
Aquí una pequeña explicación: La función update() en mongoDB es muy poderosa en el sentido que podemos hacer muchas cosas con ella. En este caso, le estamos diciendo: "Actualiza la colección productos, en todo su scope o todos sus elementos (primer parámetro {}
), con el siguiente objeto ( { nombre...}, si no existe, creálo (true), y no lo hagas multiples veces (false)." Lo de las multiples veces, no tiene sentido acá, si tiene sentido cuando le digo "actualiza todas las papas fritas, incrementando su precio en +1".
Comprobemos si nuestro producto se guardó:
> db.productos.findOne()
{
"_id" : ObjectId("4fe6454a8c136cf49721359f"),
"nombre" : "Papas Fritas",
"descripcion" : "Crujientes, sabor mediterraneo",
"precio" : 2.5
}
Ese campo _id es nativo de mongoDB.
Ahora que ya tenemos un objeto, y un documento que lo describe, es hora de diseñar rápidamente las rutas que queremos para operar en nuestra aplicación web. Empezemos por los conceptos: "Con mi aplicación web, yo a los productos quiero:"
- Listarlos
- Escoger uno y ve su descripción en detalle
- Editar un producto
- Eliminar un producto
- Crear uno nuevo
En verdad, podríamos hacer un diseño mayor, pero tenemos ideas claras de un modelo CRUD acá (Create, Read, Update, Delete), por lo que, y para hacer el tutorial breve, trabajaremos así.
Lo que sigue es gestar las rutas que harán estas actividades reales:
- Listarlos
GET /
- Escoger uno y ver su descripción en detalle
GET /producto/:id
- Editar un producto
GET /producto/:id
(usaremos la misma ruta, pero guardaremos con...)
- Guardar un producto
POST /producto/:id
- Eliminar un producto
GET /delete-producto/:id
- Crear uno nuevo (Obtener UI)
GET /nuevo-producto
- Enviar el objecto creado a la Base de Datos
POST /nuevo-producto
Para los avanzados en Verbos HTTP, me podrían decir que la creación puede ser hecha con PUT, y el borrado con DELETE, en este tutorial, iremos muy básico, solo con GET y POST. Se invita desde luego toda pull request para hacer este tutorial más firme.
Con nuestras rutas claras, solo nos queda escribir el código, y es lo que haremos.
El lector astuto dirá "Partamos por las rutas, ya que las definimos". La instalación de express tiene estas dos lineas en su archivo app.js
:
var routes = require('./routes');
//...
app.get('/', routes.index);
Aquí le decimos a la aplicación, al principio, "Usa como módulo el archivo /routes/index.js (implícito) y referenciemoslo con la variable routes
", luego le decimos "Cuando el usuario haga un GET, carga la función index
en el módulo routes
".
En esta parte empieza mi sugerencia propiamente tal, ya que borraremos estas líneas y crearemos nuestras funciones en `controladores' de manera de abrazar el modelo MVC (Model, View y Controller).
Crearemos, primero el archivo /controllers/producto.js
. Este tendrá todos los handlers (manejadores) de las rutas diseñadas, Escribamos las siguientes líneas para entender como funciona este sistema:
- En /controllers/producto.js
exports.index = function (req, res, next) {
res.send('Funciona!')
}
- Y, en /app.js
// Al principio
var producto = require('./controllers/producto')
//... Y despues del comentario // Routes
app.get('/', producto.index)
Nosotros, le estamos diciendo a express "Carga el módulo producto que está en /controllers/producto.js
" y "Cuando el usuario haga un GET a /
utiliza la función index
la que vive en producto.js
".
Está función no hace más que devolver el texto 'Funciona!'. Detengamos la ejecución de app.js, reiniciemosla y hagamos la consulta:
Lo primero que haremos es finiquitar nuestro trabajo en app.js
escribiendo todas las rutas y sus handlers, y luego nos concentraremos en manejar las peticiones a las distintas rutas.
Escribamos entonces en app.js
las siguientes rutas:
// Routes
app.get('/', producto.index)
app.get('/producto/:id', producto.show_edit)
app.post('/producto/:id', producto.update)
app.get('/delete-producto/:id', producto.remove)
app.get('/nuevo-producto', producto.create)
app.post('/nuevo-producto', producto.create)
Debemos escribir los handlers en producto.js, responderemos, como placeholders, el nombre de la función. Más adelante, escribirimos codigo adecuado para cada una. Por ejemplo para show_edit
:
exports.show_edit = function (req, res, next) {
res.send('show_edit')
}
Queremos mostrar una tabla con la lista de productos que tenemos en nuestra base de datos. Esto, en html es algo asi como:
<html>
<body>
<h2>Tabla de Productos</h2>
<table border="1">
<tr>
<th>Producto</th>
<th>Descripcion</th>
<th>Precio</th>
</tr>
<tr>
<td><a href="/super8/">Super 8</a></td>
<td>Barra de Chocolate</td>
<td>3.50</td>
</tr>
<tr>
<td><a href="/papas-fritas/">Papas Fritas</a></td>
<td>Crujientes del Mediterraneo</td>
<td>4.50</td>
</tr>
<tr>
<td><a href="/coca-cola/">Coca Cola</a></td>
<td>Agua Carbonatada con Azucar</td>
<td>2.80</td>
</tr>
</table>
</body>
</html>
Lo cual se renderea a
Para lograr esto usaremos un template de jade
, librería que viene incluída en expressJS. Si observamos, en la carpeta views
tenemos todo lo que necesitamos: Un archivo layout.jade
, el cual no tocaremos, y un archivo index.js
el cual editaremos para llegar al html
descrito arriba. Abramos el archivo index.js
para incluir el siguiente código de template jade
:
h2 Tabla de Productos
table(border='1')
tr
th Producto
th Descripción
th Precio
tr
td
a(href="/super8") Super 8
td Barra de Chocolate
td 3.50
tr
td
a(href='/papas-fritas') Papas Fritas
td Crujientes del Mediterraneo
td 4.50
tr
td
a(href='/coca-cola') Coca Cola
td Agua Carbonatada con Azucar
td 2.80
Jade puede ser extraño al principio, pero es muy poderoso, ya que podemos insertar código de javascript, hacer loops y un montón de cosas que iremos viendo. Lo importante es que aumenta nuestra productividad. Ahora debemos decirle al controlador que cargue este template. Iremos al archivo controllers/producto.js
modificando la función exports.index
así:
exports.index = function (req, res, next) {
res.render('index', { title: 'Lista de Productos'})
}
Para probar nuestro cambios, no olvidemos de detener express (CTRl+C
) e iniciar la aplicación de nuevo ($ node app.js
). El resultado al llamar a http://localhost:3000
debería ser:
Sin embargo, estos datos son dummy. Liberemos el poder de la base de datos MongoDB
, recordemos que introdujimos este documento:
> db.productos.findOne()
{
"_id" : ObjectId("4fe6454a8c136cf49721359f"),
"nombre" : "Papas Fritas",
"descripcion" : "Crujientes, sabor mediterraneo",
"precio" : 2.5
}
¿Cómo conectamos nuestra base de datos MongoDB usando node.js? Usando la librería mongoose.
El momento de la base de datos llegó, recordemos que al principio creamos el archivo models/producto.js
:
- models/producto.js
var Schema = require('mongoose').Schema
var producto_schema = new Schema({
nombre : String,
descripcion : String,
precio : Number
})
var Producto = module.exports = producto_schema
... Y, Modifiquemos nuestros archivos de la siguiente manera:
- package.json
{
"name": "herman-mongoose-suggestion"
, "version": "1.0.0"
, "dependencies": {
"express" : "2.5.8"
, "jade" : "0.25.x"
, "mongoose" : "2.5.10"
}
}
Una vez modificado package.json
, no olvidemos de actualizar nuestro modulos de node via npm install -f
en el directorio de nuestra aplicación.
- controllers/producto.js
// Creación de la Conexión
var mongoose = require('mongoose')
, db_lnk = 'mongodb://localhost/supermercado'
, db = mongoose.createConnection(db_lnk)
// Creación de variables para cargar el modelo
var producto_schema = require('../models/producto')
, Producto = db.model('Producto', producto_schema)
Ahora, existe, por supuesto la posibilidad de montar de una manera general la conexión para toda la aplicación. No la tocaremos sin embargo en este tutorial.
Modificamos la función exports.index
, siempre dentro de controllers/producto.js
para que recoja los productos:
- controllers/producto.js
exports.index = function (req, res, next) {
Producto.find(gotProducts)
// NOTA: Creo que es bueno usar verbos en inglés para las funciones,
// por lo cómodo que son en estos casos (get, got; find, found)
function gotProducts (err, productos) {
if (err) {
console.log(err)
return next()
}
return res.render('index', {title: 'Lista de Productos', productos: productos})
}
}
Nótese la estructura de callbacks: Cuando se pide la lista de productos, sólo al llegar la respuesta invocamos a gotProducts
, el cual llama a la renderización a través de res.render
de la página. Para los que vienen de otros lenguajes esta manera de modelar puede ser confusa al principio. Pero tiene sus ventajas en el escenario web, el cual, a mi parecer es completamente orientado al evento.
Tenemos listo el back-end! Ya tenemos una lista de productos, ejecutamos? Aún no, ya que res.render
cargaría el template jade, pero no le está insertando los datos. Por lo que modificaremos views/index.jade
para tal efecto:
- views/index.jade
h2 Tabla de Productos
table(border='1')
tr
th Producto
th Descripción
th Precio
- if (productos)
- each producto in productos
tr
td
a(href="/producto/" + producto._id.toString()) #{producto.nombre}
td #{producto.descripcion}
td #{producto.precio}
Bien. CTRL+C
, $ node app.js
y veamos el resultado:
Si clickamos en el link del primer producto obtenidos, tendremos un mensaje que no podemos ver el producto. Tomaremos las medidas para ello.
En general, se pueden dar buenos argumentos para no usar la misma id de producto de la base de datos como indicador id
para la ruta. Pero recordemos que estamos en un ejemplo didáctico. Necesitaremos desarrollar (Y aquí veremos lo atractivo que es el paradigma MVC), una función de controlador y una vista, no necesitaremos en este ejemplo hacer funciones de modelos, dado que mongoose nos entrega todo. En la medida que veamos más complejidad, es necesario encapsular las funciones de mongoose y las lógicas que necesitemos en funciones de modelo.
- controllers/producto.js
exports.show_edit = function (req, res, next) {
// Obtención del parámetro id desde la url
var id = req.params.id
Producto.findById(id, gotProduct)
function gotProduct (err, producto) {
if (err) {
console.log(err)
return next(err)
}
return res.render('show_edit', {title: 'Ver Producto', producto: producto})
}
}
Mongoose nos ahorra muchos problemas de desarrollo, tiene la función findById
, (Ver documento), la que, dado un id
en string, devuelve el objecto correspondiente (o null, si no existe)
Necesitamos renderizar el objecto. Acá usaremos la misma plantilla para edición y mostrar el producto:
- /views/show_edit.jade
h2 #{title}
form(method='post')
p
label(for="nombre") Nombre:
input(type='text', name='nombre', value=producto.nombre)
p
label(for="descripcion") Descripción:
input(type='text', name='descripcion', size=100, value=producto.descripcion)
p
label(for="precio") Precio:
input(type='text', name='precio', value=producto.precio)
p
input(type='submit', value='Guardar')
Obtenemos la siguiente pantalla:
Sin embargo si presionamos el botón guardar cambios, nada ocurre. Es lo que habilitaremos en el siguiente apartado...
Este es un trabajo completo sólo en el controlador (ya que tenemos la vista y modelo en mongoose):
- /controllers/producto.js
exports.update = function (req, res, next) {
var id = req.params.id
var nombre = req.body.nombre || ''
var descripcion = req.body.descripcion || ''
var precio = req.body.precio || ''
// Validemos que nombre o descripcion no vengan vacíos
if ((nombre=== '') || (descripcion === '')) {
console.log('ERROR: Campos vacios')
return res.send('Hay campos vacíos, revisar')
}
// Validemos que el precio sea número
if (isNaN(precio)) {
console.log('ERROR: Precio no es número')
return res.send('Precio no es un número !!!!!')
}
Producto.findById(id, gotProduct)
function gotProduct (err, producto) {
if (err) {
console.log(err)
return next(err)
}
if (!producto) {
console.log('ERROR: ID no existe')
return res.send('ID Inválida!')
} else {
producto.nombre = nombre
producto.descripcion = descripcion
producto.precio = precio
producto.save(onSaved)
}
}
function onSaved (err) {
if (err) {
console.log(err)
return next(err)
}
return res.redirect('/producto/' + id)
}
}
Controlamos los errores de no ID y parámetros en blanco. A next()
le estamos dando un parámetros. En iteraciones posteriores debemos configurar que si next
recibe parámetros, entregarle un error 500 al usuario. Pasaremos de esta funcionalidad por ahora.
Cómo se mencionó arriba, se podría haber usado el verbo DELETE (haciendo override de método). Para hacer más simple el tutorial, se implementa en GET
.
Debemos agregar los links para el eliminado en la lista de productos, es decir en el template de jade:
- views/index.jade
h2 Tabla de Productos
table(border='1')
tr
th Producto
th Descripción
th Precio
th
- if (productos)
- each producto in productos
tr
td
a(href="/producto/" + producto._id.toString()) #{producto.nombre}
td #{producto.descripcion}
td #{producto.precio}
td
a(href="/delete-producto/" + producto._id.toString()) Borrar
Y la funcionalidad correspondiente en el controlador:
- /controllers/producto.js
exports.remove = function (req, res, next) {
var id = req.params.id
Producto.findById(id, gotProduct)
function gotProduct (err, producto) {
if (err) {
console.log(err)
return next(err)
}
if (!producto) {
return res.send('Invalid ID. (De algún otro lado la sacaste tú...)')
}
// Tenemos el producto, eliminemoslo
producto.remove(onRemoved)
}
function onRemoved (err) {
if (err) {
console.log(err)
return next(err)
}
return res.redirect('/')
}
}
Nótese (con algo de humor por supuesto), como reaccionamos ante una id que no encontramos. Si bien asumimos que esta función es llamada dentro de la página de índice, es posible que los valores quieran ser ingresados directamente (ala REST). El desarrollador debe preveer esta conducta y crear los flujos adecuados.
Cosas que podemos agregar: Hacer una función js de cliente para que despliegue un confirmador (está seguro?) y enviar vía AJAX la llamada a borrar el producto; Podemos cerciorarnos además que quien de la orden esté dentro de una sesión; Podemos agregar un token contra "Cross Site Request Forgery", entre otros.
Finalmente, la funcionalidad de agregar productos.
La primera idea es que la función asociada a la ruta, exports.create
, nos arroje un html con los campos en blanco:
- /controllers/producto.js
exports.create = function (req, res, next) {
return res.render('show_edit', {title: 'Ver Producto', producto: {}})
}
Eso fue sencillo. Quisieramos agregar un link a esta misma página en la página de inicio:
- /views/index.jade
p
a(href='/nuevo-producto') Nuevo Producto
Y la función de controlador exports.create
debe ser modificada, crearemos un desvío según el metodo HTTP que ocupemos (GET o POST)
exports.create = function (req, res, next) {
if (req.method === 'GET') {
return res.render('show_edit', {title: 'Nuevo Producto', producto: {}})
} else if (req.method === 'POST') {
// Obtenemos las variables y las validamos
var nombre = req.body.nombre || ''
var descripcion = req.body.descripcion || ''
var precio = req.body.precio || ''
// Validemos que nombre o descripcion no vengan vacíos
if ((nombre=== '') || (descripcion === '')) {
console.log('ERROR: Campos vacios')
return res.send('Hay campos vacíos, revisar')
}
// Validemos que el precio sea número
if (isNaN(precio)) {
console.log('ERROR: Precio no es número')
return res.send('Precio no es un número !!!!!')
}
// Creamos el documento y lo guardamos
var producto = new Producto({
nombre : nombre
, descripcion : descripcion
, precio : precio
})
producto.save(onSaved)
function onSaved (err) {
if (err) {
console.log(err)
return next(err)
}
return res.redirect('/')
}
}
}
Podemos hacer algunas pruebas:
Y eso sería todo por este tutorial. Insisto, se pueden hacer muchas cosas más, pero el objetivo es introducir al lector en estas tecnologías. Personalmente hubiese hecho algún trabajo para manejar los errores y devolver un error 500, desarrollo de usuarios y sesiones, más javascript de cliente y otros. Para el futuro. Muchas gracias.