Como de costumbre en estas notas de J2EE me referiré al pdf del curso de KernelPanik impartido por el gurú Max
y a la copia del pdf que guardo en local ;)
Antes de nada hacer una pequeña descripción de lo que es la Arquitectura Modelo, Vista, Controlador. Desde lugeo en la wikipedia hay mucho mas. Pero para lo que nos interesa ahora habrá que decir que:
La arquitectura MVC tiene como objetivo el separar en tres capas las tres funciones básicas de una aplicación (web en este caso).
El modelo consiste en:
Que en este caso serán las JSP
Que es un servlet que recibe las peticiones del cliente y las gestiona.
Para ver como funciona todo esto haremos una aplicación JEE de pruebas.
Haremos una aplicación web que registre niveles de satisfacción de empleados sobre sus empresas. Los usuarios podrán enviar información acerca del nivel de satisfacción que tienen sobre su empresa en diferentes ámbitos. Posteriormente podremos hacer estadísticas sobre ello.
Una de las primeras cosas a realizar es configurar la fuente de datos ( la conexión a la base de datos, o a los archivos xml, o a lo que quieras) para ver como funciona aqui está el tutorial.
Y para ver como configurar una fuente de datos con tomcat 5.5 pues lo miras en su web
Vamos a ver como crear nuestro nuevo proyecto web. Le diremos a Eclipse que queremos crear un nuevo proyecto. pero seleccionaremos “otro” para que nos muestre todas las posibilidades.
Y crearemos un proyecto web dinamico
A continuación le damos nombre a nuestro proyecto:
Y le decimos que cargue las librerías normales :
Y.. atento:
Ya tenemos nuestro proyecto web dinámico en la lista de nuestros proyectos. Ahora debemos añadirlo al servidor tomcat para que al guardarlo lo publique.
Seleccionamos el nombre de nuestro proyecto web y lo añadimos
Y ya está. Ahora para probar que todo funciona vamos a poner una simple página html para comprobar que la vemos:
Ponemos cualquier cosa en la página html y le damos al “play” del servidor. Aunque para publicar cambios en un html no hace falta si que está bien que nos acostumbremos a hacerlo
En caso de que haya algún error al arrancar el servidor es posible de que sea porque ya estuviese arrancado de antes. Simplemente para el tomcat (desde fuera, desde consola) y vuelve a darle al “play”
Lo primero que hay que hacer es preparar la fuente de datos. Para ello hay que preparar la conexión a la base de datos (MySql en este caso). Y preparar tambien la base de datos … te dejo una copia de la que voy a usar yo para esta prueba.
Para ello debemos editar el server.xml de nuestro servidor web y añadir los datos relevantes a nuestra conexión
<Resource auth="Container" type="javax.sql.DataSource" name="jdbc/miDatasource" url="jdbc:mysql://localhost:3306/miBaseDeDades?autoReconnect=true" driverClassName="com.mysql.jdbc.Driver" username="miNombreUsuario" password="miPassword" maxActive="100" maxIdle="30" maxWait="10000" />
Por lo que el server.xml debe verse algo aprecido a esto:
Y una vez que tenemos configurado el server.xml tendremos que crear la referencia al recurso en nuestra aplicación. Para eso editaremos el web.xml de nuestra aplicación web y inlcuiremos la referencia:
<resource-ref> <description>Conexio BBDD</description> <res-ref-name>jdbc/miDataSource</res-ref-name> <res-type>javax.sql.DataSource</res-type> <res-auth>Container</res-auth> </resource-ref>
Dejando algo tal que así
Y por fin, para que esto realmente funcione deberás poner el el jar del conector de mysql ( mysql-connector-java-5.0.8-bin.jar )en un sitio accesible para tomcat. Como por ejemplo la libraría de clases comunes: sudo cp mysql-connector-java-5.0.8-bin.jar /usr/share/tomcat5.5/common/lib/
Un “Data Transfer Object” es un JavaBean que representa una entidad en la aplicación. Es un objeto que almacena información y que será usado para transferir informacion por nuestra aplicación.
En nuestro caso haremos un DTO al que llamaremos opinión y que recogerá:
A su vez este DTO contendrá una lista de grados de satisfacción, uno por cada ámbito del que preguntemos. A tal efecto crearemos otro DTO que contendrá:
A fin de ser limpio y ordenado se recomienda que todos los DTOs estén agrupados en un paquete DTO o OBJETOS o lo que tu quieras.
package net.juantxu.encuesta.dto;
import java.util.List;
public class Opinion {
private String nombre;
private String empresa;
private int tipoContrato;
private List<Satisfaccion> Satisfaccion;
public void setNombre(String nombre) {
this.nombre = nombre;
}
public String getNombre() {
return nombre;
}
public void setEmpresa(String empresa) {
this.empresa = empresa;
}
public String getEmpresa() {
return empresa;
}
public void setTipoContrato(int tipoContrato) {
this.tipoContrato = tipoContrato;
}
public int getTipoContrato() {
return tipoContrato;
}
public void setSatisfaccion(List<Satisfaccion> satisfaccion) {
Satisfaccion = satisfaccion;
}
public List<Satisfaccion> getSatisfaccion() {
return Satisfaccion;
}
}
package net.juantxu.encuesta.dto;
public class Satisfaccion {
private String ambito;
private int grado;
public void setAmbito(String ambito) {
this.ambito = ambito;
}
public String getAmbito() {
return ambito;
}
public void setGrado(int grado) {
this.grado = grado;
}
public int getGrado() {
return grado;
}
}
Los Data Access Objects son los objetos que nos proporcionarán acceso a los DTO. A traves de ellos facilitaremos el acceso y manipulacion de los datos datos en nuestra aplicación.
Fijate que separamos los objetos que contienen los datos DTO de los que nos proporcionarán acceso a ellos DAO de este modo obtenemos una perfecta distinción y facilidad a la hora de añadir nuevas funcionalidades.
A nivel lógico es útil hacer un nuevo paquete DAO que contenga todas estas clases. En este ejemplo tan sólo tenemos uno que guarde los datos de la encuesta de opinión que estamos haciendo:
package net.juantxu.encuesta.dao;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.sql.DataSource;
import net.juantxu.encuesta.dto.Opinion;
import net.juantxu.encuesta.dto.Satisfaccion;
public class OpinionDAO {
private Connection getConnexio() throws NamingException, SQLException {
Connection con;
Context ic = new InitialContext();
DataSource ds = (DataSource) ic.lookup("java:comp/env/jdbc/Encuesta");
con = ds.getConnection();
return con;
}
public void guardarOpinion(Opinion o) throws NamingException, SQLException {
Connection con = null;
PreparedStatement ps = null;
ResultSet rs = null;
long idEmpresa = 0;
long idUsuario = 0;
try {
con = getConnexio();
// primero la empresa
idEmpresa = insertaEmpresa(o,con);
// luego el usuario
idUsuario = insertaUsuario(o, idEmpresa, con);
// ahora el grado de satisfaccion
insertarSatisfacciones(o, idUsuario, idEmpresa, con);
// insertar el tipo de contrato del usuario
insertarTipoContrato(o, idUsuario, idEmpresa,con);
} catch (SQLException e) {
e.printStackTrace();
throw e;
} finally {
if (rs != null)
rs.close();
if (ps != null)
ps.close();
if (con != null)
con.close();
}
}
public HashMap<String, Integer> getSatisfacciones() throws NamingException, SQLException {
HashMap<String, Integer> lista = new HashMap<String, Integer> ();
Connection con = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
con = getConnexio();
ps = con.prepareStatement("select * from ambito" );
rs = ps.executeQuery();
lista = hazMapa(rs);
} catch (SQLException e) {
e.printStackTrace();
throw e;
} finally {
if (rs != null)
rs.close();
if (ps != null)
ps.close();
if (con != null)
con.close();
}
return lista;
}
private HashMap<String, Integer> hazMapa(ResultSet rs)throws SQLException {
HashMap<String, Integer> mapa = new HashMap<String, Integer> ();
while (rs.next()) {
mapa.put(rs.getString("nombre"), (Integer)rs.getInt("id"));
}
return mapa;
}
private void insertarTipoContrato(Opinion o, long idUsuario, long idEmpresa,Connection con)throws NamingException, SQLException{
PreparedStatement ps = null;
try {
ps = con.prepareStatement("insert into contratacion (id, usuario, empresa, tipoContrato ) values (0,?,?,?)" );
ps.setLong(1,idUsuario);
ps.setLong(2, idEmpresa);
ps.setInt(3, o.getTipoContrato() );
ps.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
throw e;
} finally {
if (ps != null)
ps.close();
}
}
private void insertarSatisfacciones(Opinion o, long idUsuario, long idEmpresa,Connection con)throws NamingException, SQLException{
PreparedStatement ps = null;
List<Satisfaccion> satisfacciones = o.getSatisfaccion();
try {
Iterator<Satisfaccion> i = satisfacciones.iterator();
while (i.hasNext()){
Satisfaccion s = (Satisfaccion) i.next();
System.out.println("satisfaccion "+s.getAmbito() );
ps = con.prepareStatement("insert into satisfaccion (usuario, empresa, ambito, satisfaccion) values (?,?,?,?)" );
ps.setLong(1,idUsuario);
ps.setLong(2, idEmpresa);
ps.setInt(3, s.getAmbito() );
ps.setInt(4, s.getGrado() );
ps.executeUpdate();
}
} catch (SQLException e) {
e.printStackTrace();
throw e;
} finally {
if (ps != null)
ps.close();
}
}
private long insertaEmpresa(Opinion o, Connection con)throws NamingException, SQLException{
PreparedStatement ps = null;
long id=0;
try {
ps = con.prepareStatement("insert into empresa (id, nombre) values (0, ?)" );
ps.setString(1,o.getEmpresa().trim().toUpperCase());
ps.executeUpdate();
id = recuperarEmpresa(o.getEmpresa().trim().toUpperCase() , con);
} catch (SQLException e) {
e.printStackTrace();
throw e;
} finally {
if (ps != null)
ps.close();
}
return id;
}
private long recuperarEmpresa(String empresa, Connection con)throws NamingException, SQLException {
PreparedStatement ps = null;
ResultSet rs = null;
long emp = 0;
try {
con = getConnexio();
ps = con.prepareStatement("select id from empresa where upper(nombre) = ?" );
ps.setString(1,empresa.toUpperCase());
rs = ps.executeQuery();
while(rs.next()){
emp = rs.getLong("id");
}
} catch (SQLException e) {
e.printStackTrace();
throw e;
} finally {
if (rs != null)
rs.close();
if (ps != null)
ps.close();
}
return emp;
}
private long insertaUsuario(Opinion o, long idEmpresa,Connection con)throws NamingException, SQLException{
PreparedStatement ps = null;
long id = 0;
try {
ps = con.prepareStatement("insert into usuario (id, nombre, empresa, fecha) values (0, ?,?, NOW())" );
ps.setString(1,o.getNombre().trim().toUpperCase());
ps.setLong(2, idEmpresa);
ps.executeUpdate();
id = recuperarUsuario(o.getNombre().trim().toUpperCase(), idEmpresa, con );
} catch (SQLException e) {
e.printStackTrace();
throw e;
} finally {
if (ps != null)
ps.close();
}
return id;
}
private long recuperarUsuario(String usuario, long empresa, Connection con)throws NamingException, SQLException {
PreparedStatement ps = null;
ResultSet rs = null;
long emp = 0;
try {
con = getConnexio();
ps = con.prepareStatement("select id from usuario where upper(nombre) = ? and empresa = ?" );
ps.setString(1 ,usuario.toUpperCase());
ps.setLong(2, empresa);
rs = ps.executeQuery();
while(rs.next()){
emp = rs.getLong("id");
}
} catch (SQLException e) {
e.printStackTrace();
throw e;
} finally {
if (rs != null)
rs.close();
if (ps != null)
ps.close();
}
return emp;
}
}
Para ello como ves importamos las clases del paquete de mysql que hemos copiado antes en el common libs de tomcat para que estén accesibles:
import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException;
Porque queremos guardar los datos en la base de datos que hemos creado….
Para ello lo primero que vamos a hacer es crear el procedimiento que nos habilitará la conexión con la base de datos..
private Connection getConnexio() throws NamingException, SQLException {
Connection con;
Context ic = new InitialContext();
DataSource ds = (DataSource) ic.lookup("java:comp/env/jdbc/Encuesta");
con = ds.getConnection();
return con;
}
Creamos un objeto conexión, un objeto contexto y le decimos que la fuente de datos se encuentra en el dns que hemos creado al principio java:comp/env/jdbc/Encuesta en mi caso y jdbc/miDataSource en el caso de este ejemplo ya que es la cadena que he usado arriba.
Pues si ya tenemos la conexión ahora vamos a usarla!… Vamos a hacer el procedimiento que recoja los datos y los guarde…
public void guardarOpinion(Opinion o) throws NamingException, SQLException {
Connection con = null;
long idEmpresa = 0;
long idUsuario = 0;
try {
con = getConnexio();
// primero la empresa
idEmpresa = insertaEmpresa(o,con);
// luego el usuario
idUsuario = insertaUsuario(o, idEmpresa, con);
// ahora el grado de satisfaccion
insertarSatisfacciones(o, idUsuario, idEmpresa, con);
// insertar el tipo de contrato del usuario
insertarTipoContrato(o, idUsuario, idEmpresa,con);
} catch (SQLException e) {
e.printStackTrace();
throw e;
} finally {
if (con != null)
con.close();
}
}
Fijate que recibe un DTO que es una opinión y que la manipula. En este caso, como que el ejemplo es un poco largo, pues he subdividido el proceso de guardar una opinión en diferentes subprocesos de modo que sea más facil de manipular. Y como que todo ello se va a hacer seguido pues, abro la conexión al principio, guardo todos los datos con la misma conexión y cierro al final. opinión personal: Creo que de ese modo es mas limpio y rápido al usar todos la misma conexion.
Fijate tambien que hay excepciones que no proceso, por lo que tengo que avisar de ello throws NamingException, SQLException
y no olvides cerrar la Conexión al salir
Vamos a ver que hacen cada uno de esos subrocesos:
private long insertaEmpresa(Opinion o, Connection con)throws NamingException, SQLException{
PreparedStatement ps = null;
long id=0;
try {
ps = con.prepareStatement("insert into empresa (id, nombre) values (0, ?)" );
ps.setString(1,o.getEmpresa().trim().toUpperCase());
ps.executeUpdate();
id = recuperarEmpresa(o.getEmpresa().trim().toUpperCase() , con);
} catch (SQLException e) {
e.printStackTrace();
throw e;
} finally {
if (ps != null)
ps.close();
}
return id;
}
El primero es el que guarda la empresa en cuestión que a si vez debe recuperarla para devolver el id que posteriormente será usado. Recibo el DTO opinion y la conexión previamente abierta por lo que lo único que tengo que hacer es ejecutar… y recuperar
private long insertaEmpresa(Opinion o, Connection con)throws NamingException, SQLException{
PreparedStatement ps = null;
long id=0;
try {
ps = con.prepareStatement("insert into empresa (id, nombre) values (0, ?)" );
ps.setString(1,o.getEmpresa().trim().toUpperCase());
ps.executeUpdate();
id = recuperarEmpresa(o.getEmpresa().trim().toUpperCase() , con);
} catch (SQLException e) {
e.printStackTrace();
throw e;
} finally {
if (ps != null)
ps.close();
}
return id;
}
private long recuperarEmpresa(String empresa, Connection con)throws NamingException, SQLException {
PreparedStatement ps = null;
ResultSet rs = null;
long emp = 0;
try {
con = getConnexio();
ps = con.prepareStatement("select id from empresa where upper(nombre) = ?" );
ps.setString(1,empresa.toUpperCase());
rs = ps.executeQuery();
while(rs.next()){
emp = rs.getLong("id");
}
} catch (SQLException e) {
e.printStackTrace();
throw e;
} finally {
if (rs != null)
rs.close();
if (ps != null)
ps.close();
}
return emp;
}
Para ejecutar uso prepareStatement a fin de evitar en lo posible inyecciones SQL.
La vista es la capa de presentación. Contiene las páginas html y jsp que se muestran al cliente. En esta primera aproximación a la vista no entraremos para nada en las jsp que abordaremos mas adelante. De momento la vista es una simple página html que enviará los datos al controlador.
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <title>Encuesta de Satisfacción Laborar</title> </head> <body> <h1>Encuesta de satisfaccion laboral</h1> <form name="frmEncuesta" action="EncuestaAccion" method="post"> <label>¿Quien eres?</label><input type="text" name="nombre" value="tu nombre/ apodo"><br /> <label>¿Donde Trabajas?</label><input type="text" name="empresa" value="el nombre de tu empresa"><br /> <br /> <label>¿Tipo de Contrato?</label> <select name="tipoContrato"> <option value="1" >Indefinido</option> <option value="2" >Obra y servicio</option> <option value="3" >6 meses</option> <option value="4" >3 meses</option> <option value="5" >12 meses</option> <option value="6" >1/2 jornada indefinido</option> <option value="7" >1/2 jornada Obra y servicio</option> <option value="8" >1/2 jornada 6 meses</option> <option value="9" >1/2 jornada 3 meses</option> <option value="10" >1/2 jornada 12 meses</option> <option value="11" >becario</option> </select> <br /> <div id="satisfaccion"> <h3>Grados de Satisfacción laboral</h3> <p>En una escala del 1 al 10 puntua tus grado de satisfacción laboral</p> <label>Salario</label> <select name="1"> <option value="0" >??</option> <option value="1" >1</option> <option value="2" >2</option> <option value="3" >3</option> <option value="4" >4</option> <option value="5" >5</option> <option value="6" >6</option> <option value="7" >7</option> <option value="8" >8</option> <option value="9" >9</option> <option value="10" >10</option> </select> <br /> <label>Horario</label> <select name="2"> <option value="0" >??</option> <option value="1" >1</option> <option value="2" >2</option> <option value="3" >3</option> <option value="4" >4</option> <option value="5" >5</option> <option value="6" >6</option> <option value="7" >7</option> <option value="8" >8</option> <option value="9" >9</option> <option value="10" >10</option> </select> <br /> <label>Ambiente de trabajo-Jefes</label> <select name="3"> <option value="0" >??</option> <option value="1" >1</option> <option value="2" >2</option> <option value="3" >3</option> <option value="4" >4</option> <option value="5" >5</option> <option value="6" >6</option> <option value="7" >7</option> <option value="8" >8</option> <option value="9" >9</option> <option value="10" >10</option> </select> <br /> <label>Ambiente de trabajo-Compañeros</label> <select name="4"> <option value="0" >??</option> <option value="1" >1</option> <option value="2" >2</option> <option value="3" >3</option> <option value="4" >4</option> <option value="5" >5</option> <option value="6" >6</option> <option value="7" >7</option> <option value="8" >8</option> <option value="9" >9</option> <option value="10" >10</option> </select> <br /> <label>Entorno de trabajo-Herramientas</label> <select name="5"> <option value="0" >??</option> <option value="1" >1</option> <option value="2" >2</option> <option value="3" >3</option> <option value="4" >4</option> <option value="5" >5</option> <option value="6" >6</option> <option value="7" >7</option> <option value="8" >8</option> <option value="9" >9</option> <option value="10" >10</option> </select> <br /> <label>Entorno de trabajo-Instalaciones</label> <select name="6"> <option value="0" >??</option> <option value="1" >1</option> <option value="2" >2</option> <option value="3" >3</option> <option value="4" >4</option> <option value="5" >5</option> <option value="6" >6</option> <option value="7" >7</option> <option value="8" >8</option> <option value="9" >9</option> <option value="10" >10</option> </select> <br /> </div> <input type="submit" value="Enviar" /> </form> </body> </html>
El controlador es quien recibe las peticiones y decide que se hace. Normalmente es un servlet que recibe las peticiones, carga los datos en los DTO y llama al DAO para que haga algo con ello.
En nuestro ejemplo crearemos uno, lo más simple posible que guarde los datos y nos devuelva al índice:
package net.juantxu.encuesta.acciones;
import java.io.IOException;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import javax.naming.NamingException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import net.juantxu.encuesta.dao.OpinionDAO;
import net.juantxu.encuesta.dto.Opinion;
import net.juantxu.encuesta.dto.Satisfaccion;
/**
* Servlet implementation class for Servlet: EncuestaAccion
*
*/
public class EncuestaAccion extends javax.servlet.http.HttpServlet implements javax.servlet.Servlet {
static final long serialVersionUID = 1L;
/* (non-Java-doc)
* @see javax.servlet.http.HttpServlet#HttpServlet()
*/
public EncuestaAccion() {
super();
}
/* (non-Java-doc)
* @see javax.servlet.http.HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
*/
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// TODO Auto-generated method stub
response.sendRedirect("index.html");
}
/* (non-Java-doc)
* @see javax.servlet.http.HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response)
*/
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// Crear nuevo dao
OpinionDAO DAO = new OpinionDAO();
// Crear una nueva opinion
Opinion opinion = new Opinion();
// Creamos la lista de satisfacciones
ArrayList<Satisfaccion> sats = new ArrayList<Satisfaccion>();
// recuperar parametros de la opinion
opinion.setNombre(request.getParameter("nombre"));
opinion.setEmpresa(request.getParameter("empresa"));
// los ambitos
// pero como que lanza excepciones
HashMap<String, Integer> satisfacciones = new HashMap<String, Integer>();
try {
satisfacciones = DAO.getSatisfacciones();
} catch (SQLException e) {
e.printStackTrace();
} catch (NamingException e) {
e.printStackTrace();
}
// las satisfacciones
for (int i=1; i<7; i++){
// Crear una nueva satisfaccion
Satisfaccion sat = new Satisfaccion();
sat.setAmbito(i);
sat.setGrado( Integer.parseInt( request.getParameter(Integer.toString(i) ).trim() ) );
sats.add(sat);
}
opinion.setSatisfaccion(sats);
Integer integ = new Integer(request.getParameter("tipoContrato"));
// los tipos de contratos
opinion.setTipoContrato(integ );
// Abrimos try catch ya que OpinionDAO lanza excepciones
try {
// Guardar la opinion
DAO.guardarOpinion(opinion);
} catch (SQLException e) {
e.printStackTrace();
} catch (NamingException e) {
e.printStackTrace();
}
// Devolvemos el resultado. En este caso volvemos a llamar al índice
response.sendRedirect("index.html");
}
}
Y ahora a probar:
Nota: Recuerda que yo lo estoy haciendo con Eclipse y Tomcat
Click en el botón derecho sobre el proyecto para desplegar el menú contextual y exportar….
Elijes el tipo de export que quieres hacer WAR
Y le das un nombre bonito…
Primera fase concluida.
Hay varias maneras y no quiere decir que esta sea la mejor. En mi caso, como que lo voy a desplegar en otra máquina que está en otro sitio distinto al que he hecho el desarrollo lo tengo que hacer a traves de la interfaz web. Tu podrías simplemente dejar el war en la carpeta webapps del tomcat y él solito lo haría todo. Pero ese no es mi caso.
Entras en la página de administración de tu tomcat y le dices que quieres subir un war
Una vez subido, y como que somos muy buenos programadores y no tendremos problemas, aparecerá en la lista de aplicaciones disponibles. Tan sólo tienes que picar sobre el o ir a la url de la aplicación en MI caso http://www.juantxu.net:8180/Encuesta/ )
Pero eso no es todo…. ¿ Te acuerdas que tocamos el SEVER.XML del servidor locar para hacer la conexión a la base de datos ?
Eso es todo amigos… Si, ya se que es muy soso y que no se ven los datos que hemos introducido…. ¿¡Que te piensas que será el capitulo dos!?
El mio tambien es así de cutre