🧭 Estructura de un proyecto con JDBC¶
Para las tareas de clase vamos a seguir una estructura que iremos perfilando basada en el MVC (modelo - vista - controlador). La idea principal es permitir independizar la lógica y la parte visual y la procedencia de los datos, usando un controlador que administra los procesos sirviendo de puente entre estos.
En el IntelliJ, crearemos un nuevo proyecto con la siguiente distribución de paquetes:
1️⃣ - Creando la BBDD¶
Vamos a crear la base de datos en MySql. Esta puede ser creada previamente o ser creada en el mismo programa al inicio de la sesión comprobando si existe o no existe.
create schema if not exists `mi_DB`;
use `mi_DB`;
create table if not exists `Persona`(
`id` int not null auto_increment,
`dni` varchar(9) not null,
`nombre` varchar(50) not null,
`apellido` varchar(50) not null,
`edad` int not null,
primary key(`id`),
unique key(`dni`)
);
INSERT INTO Persona (dni, nombre, apellido, edad)
VALUES ('12345678A', 'Juan', 'Pérez', 30),
('87654321B', 'María', 'González', 25),
('55555555C', 'Pedro', 'Martínez', 40),
('99999999D', 'Laura', 'López', 35),
('11111111E', 'Ana', 'Sánchez', 28);
2️⃣ - Clase Connection
¶
Para el paquete db
vamos a crear la clase DBConnection
, esta clase mantendrá una única conexión hacia la base de datos utilizando el patrón Singleton.
Patrón Singleton¶
El patrón Singleton es un patrón de diseño de software que garantiza que una clase tenga solo una instancia y proporciona un punto de acceso global a esa instancia. Esto es útil cuando solo se necesita una instancia de una clase para coordinar acciones en todo el sistema, como por ejemplo, un objeto de conexión a una base de datos, un registro de eventos, un gestor de configuración, entre otros.
Para evitar la creación de nuevas instancias, se crea un constructor privado y se mantiene un variable de instancia static a una única instancia.
public class DBConnection {
// URL de conexión a la base de datos MySQL
private static final String URL = "jdbc:mysql://192.168.18.50:3366/mi_DB2";
private static final String USERNAME = "root";
private static final String PASSWORD = "root123456";
private static Connection connection;
// Constructor privado para evitar instancias directas
private DBConnection() {}
// Método estático para obtener la instancia única de la conexión
public static Connection getConnection() {
if (connection == null) {
// Bloqueo sincronizado para evitar concurrencia
synchronized (DBConnection.class) {
if (connection == null) {
try {
// Establecer la conexión
connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);
crearTabla(connection);
crearDatosEjemplo();
} catch ( SQLException e) {
e.printStackTrace();
}
}
}
}
return connection;
}
// Método para cerrar la conexión
public static void closeConnection() {
if (connection != null) {
try {
connection.close();
connection = null;
} catch (SQLException e) {
e.printStackTrace();
}
}
}
//creamos la tabla si no existe
private static void crearTabla(Connection conexion) throws SQLException {
try (Statement statement = conexion.createStatement()) {
statement.executeUpdate(
//Sentencia SQL para crear la tabla si no existe
PersonaDAO.CREATE_TABLE_PERSONA
);
// System.out.println("Tabla Persona creada correctamente.");
}
}
//Crea unos datos de ejemplo
public static void crearDatosEjemplo() throws SQLException{
PersonaDAO personaDAO = PersonaDAO.getInstance();
try {
//si no tenemos datos, cramos unos ejemplos
if(personaDAO.totalPersonas()==0) {
personaDAO.insertPersona(new Persona("12345678A", "Juan", "Pérez", 25));
personaDAO.insertPersona(new Persona("98765432B", "María", "Gómez", 30));
personaDAO.insertPersona(new Persona("55555555C", "Carlos", "López", 22));
personaDAO.insertPersona(new Persona("11111111D", "Ana", "Martínez", 28));
personaDAO.insertPersona(new Persona("99999999E", "Pedro", "Sánchez", 35));
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
3️⃣ - Creando el modelo¶
El modelo contiene una representación de los datos que maneja la aplicación y su lógica de negocio. Permitirá mediante clases POJO identificar y asociar el modelo relacional con los objetos que manejará la aplicación
Para el ejemplo, creamos una clase persona
que debe contener los atributos que contiene la tabla pesona
como variables de la clase. Normalmente los modelos de la clase se encuentran en un paquete llamado model.
public class Persona {
private int id;
private String dni;
private String nombre;
private String apellido;
private int edad;
//este constructor nos permite leer los datos de la base de datos
public Persona(int id,String dni, String nombre, String apellido, int edad) {
this.id=id;
this.dni = dni;
this.nombre = nombre;
this.apellido = apellido;
this.edad = edad;
}
//este contructor nos permite crear una Persona en la base de datos.
//el id lo genera automático Mysql
public Persona(String dni, String nombre, String apellido, int edad) {
this.dni = dni;
this.nombre = nombre;
this.apellido = apellido;
this.edad = edad;
}
//gette y setter
}
4️⃣ - Patrón DAO. Clases para la manipulación de la base de datos¶
El patrón DAO (Data Access Object) es un patrón de diseño que se utiliza para separar la lógica de acceso a los datos de la lógica de negocio en una aplicación. Su objetivo principal es abstraer y encapsular todas las operaciones de acceso a la base de datos en un conjunto de clases dedicadas, permitiendo así una mayor flexibilidad, reutilización de código y mantenibilidad del sistema.
Vamos a mantener una única instancia mediante el patrón Singleton
Permitirá la sustitución del Gestor de Base de Datos sin que afecte al resto del programa, ya que las capas superiores no trabajan con las conexiones a la base de datos.
En el ejemplo tenemos una clase en el paquete db
que realiza el CRUD de la información referente a la tabla persona
.
/**
* Clase PersonaDAO que gestiona el acceso a la base de datos para la entidad Persona.
* Implementa el patrón Singleton para asegurar una única instancia.
*/
public class PersonaDAO {
// Instancia única de PersonaDAO
private static PersonaDAO instance;
// Conexión a la base de datos
private Connection connection;
//Sentencia SQL para crear la si no existe
public static final String CREATE_TABLE_PERSONA = """
CREATE TABLE IF NOT EXISTS `Persona`(
`id` int not null auto_increment,
`dni` VARCHAR(9) NOT NULL,
`nombre` VARCHAR(50) NOT NULL,
`apellido` VARCHAR(50) NOT NULL,
`edad` int NOT NULL,
primary key(`id`),
unique key(`dni`)
);
""";
// Consultas SQL predefinidas para operaciones CRUD
private static final String INSERT_QUERY = "INSERT INTO Persona (dni, nombre, apellido, edad) VALUES (?, ?, ?, ?)";
private static final String SELECT_ALL_QUERY = "SELECT * FROM Persona";
private static final String SELECT_BY_DNI_QUERY = "SELECT * FROM Persona WHERE dni = ?";
private static final String UPDATE_QUERY = "UPDATE Persona SET nombre = ?, apellido = ?, edad = ? WHERE dni = ?";
private static final String DELETE_QUERY = "DELETE FROM Persona WHERE dni = ?";
private static final String TOTAL_PERSONAS_QUERY = "SELECT COUNT(*) FROM Persona";
/**
* Constructor privado para evitar instanciación externa.
* Obtiene la conexión a la base de datos desde DBConnection.
*/
private PersonaDAO() {
this.connection = DBConnection.getConnection();
}
/**
* Método estático para obtener la única instancia de PersonaDAO.
* @return instancia única de PersonaDAO.
*/
public static synchronized PersonaDAO getInstance() {
if (instance == null) {
instance = new PersonaDAO();
}
return instance;
}
/**
* Inserta una nueva persona en la base de datos.
* @param persona Objeto Persona a insertar.
* @throws SQLException Si ocurre un error en la base de datos.
*/
public void insertPersona(Persona persona) throws SQLException {
try (PreparedStatement statement = connection.prepareStatement(INSERT_QUERY)) {
statement.setString(1, persona.getDni());
statement.setString(2, persona.getNombre());
statement.setString(3, persona.getApellido());
statement.setInt(4, persona.getEdad());
statement.executeUpdate();
}
}
/**
* Obtiene todas las personas almacenadas en la base de datos.
* @return Lista de objetos Persona.
* @throws SQLException Si ocurre un error en la base de datos.
*/
public List<Persona> getAllPersonas() throws SQLException {
List<Persona> personas = new ArrayList<>();
try (PreparedStatement statement = connection.prepareStatement(SELECT_ALL_QUERY)) {
ResultSet resultSet = statement.executeQuery();
while (resultSet.next()) {
personas.add(resultSetToPersona(resultSet));
}
}
return personas;
}
/**
* Obtiene una persona a partir de su DNI.
* @param dni Identificador único de la persona.
* @return Objeto Persona si se encuentra, null si no.
* @throws SQLException Si ocurre un error en la base de datos.
*/
public Persona getPersonaByDni(String dni) throws SQLException {
Persona persona = null;
try (PreparedStatement statement = connection.prepareStatement(SELECT_BY_DNI_QUERY)) {
statement.setString(1, dni);
ResultSet resultSet = statement.executeQuery();
if (resultSet.next()) {
persona = resultSetToPersona(resultSet);
}
}
return persona;
}
/**
* Actualiza los datos de una persona en la base de datos.
* @param persona Objeto Persona con los datos actualizados.
* @throws SQLException Si ocurre un error en la base de datos.
*/
public void updatePersona(Persona persona) throws SQLException {
try (PreparedStatement statement = connection.prepareStatement(UPDATE_QUERY)) {
statement.setString(1, persona.getNombre());
statement.setString(2, persona.getApellido());
statement.setInt(3, persona.getEdad());
statement.setString(4, persona.getDni());
statement.executeUpdate();
}
}
/**
* Elimina una persona de la base de datos por su DNI.
* @param dni Identificador único de la persona a eliminar.
* @throws SQLException Si ocurre un error en la base de datos.
*/
public void deletePersonaByDni(String dni) throws SQLException {
try (PreparedStatement statement = connection.prepareStatement(DELETE_QUERY)) {
statement.setString(1, dni);
statement.executeUpdate();
}
}
/**
* Convierte un ResultSet en un objeto Persona.
* @param resultSet Resultado de la consulta SQL.
* @return Objeto Persona con los datos del ResultSet.
* @throws SQLException Si ocurre un error en la conversión.
*/
private Persona resultSetToPersona(ResultSet resultSet) throws SQLException {
return new Persona(
resultSet.getInt("id"),
resultSet.getString("dni"),
resultSet.getString("nombre"),
resultSet.getString("apellido"),
resultSet.getInt("edad"));
}
/**
* Obtiene el total de personas almacenadas en la base de datos.
* @return Número total de personas.
* @throws SQLException Si ocurre un error en la base de datos.
*/
public int totalPersonas() throws SQLException {
int total = 0;
try (PreparedStatement statement = connection.prepareStatement(TOTAL_PERSONAS_QUERY)) {
ResultSet resultSet = statement.executeQuery();
if (resultSet.next()) {
total = resultSet.getInt(1);
}
}
return total;
}
}
Patrón Repository
Actualmente, el acceso puede venir de varias fuentes de datos, como puede ser una base de datos por un lado y un servicio web por otro. El patrón DAO, ha evolucionado al patrón Repository que permite seleccionar la fuente de datos aislando del problema a las capas superiores
5️⃣ - Vista¶
En el paquete view
mantenemos las clases encargadas para mostrar y recibir la información del usuario. Nosotros vamos a simplificar el programa por consola, pero una aplicación web o un programa JavaFX, manejará la interacción de eventos visuales con el usuario y avisará de ello al Controller
Simplificando nuestro programa a consola, podemos tener la siguiente interacción con el usuario.
public class VistaPersona {
public Scanner scanner;
public VistaPersona() {
scanner = new Scanner(System.in);
}
public void mostrarPersonas(List<Persona> personas) {
System.out.println("Lista de Personas:");
for (Persona persona : personas) {
System.out.println(persona);
}
}
public Persona crearPersona() {
System.out.println("Introduce el DNI:");
String dni = scanner.nextLine();
System.out.println("Introduce el nombre:");
String nombre = scanner.nextLine();
System.out.println("Introduce el apellido:");
String apellido = scanner.nextLine();
System.out.println("Introduce la edad:");
int edad = Integer.parseInt(scanner.nextLine());
return new Persona(dni, nombre, apellido, edad);
}
public String obtenerDniAEliminar() {
System.out.println("Introduce el DNI de la persona a eliminar:");
return scanner.nextLine();
}
public Persona obtenerDatosActualizados() {
System.out.println("Introduce el DNI:");
String dni = scanner.nextLine();
System.out.println("Introduce el nuevo nombre:");
String nombre = scanner.nextLine();
System.out.println("Introduce el nuevo apellido:");
String apellido = scanner.nextLine();
System.out.println("Introduce la nueva edad:");
int edad = Integer.parseInt(scanner.nextLine());
return new Persona(dni, nombre, apellido, edad);
}
public void mostrarMensaje(String mensaje) {
System.out.println(mensaje);
}
public String obtenerDni() {
System.out.println("Introduce el DNI de la persona:");
return scanner.nextLine();
}
//muestra los datos de la persona
public void mostrarPersona(Persona persona) {
if(persona!=null)
System.out.println(persona);
else
System.out.println("La persona no existe");
}
// Otros métodos de la vista...
}
6️⃣ - Controller¶
El `Controller'(controlador) es una parte fundamental en la arquitectura del patrón Modelo-Vista-Controlador (MVC). Su tarea principal es actuar como intermediario entre el modelo (que maneja los datos de la aplicación) y la vista (que muestra la interfaz de usuario). El controlador interpreta las acciones del usuario desde la vista, realiza las operaciones necesarias en el modelo y actualiza la vista en consecuencia. En resumen, el controlador coordina la interacción entre el modelo y la vista para garantizar que la aplicación funcione correctamente y responda a las acciones del usuario de manera adecuada.
En nuestro caso, mantenemos en la clase, instancias del DAO y VIEW de forma simplificada que permiten manejar el CRUD mediante el DAO con los datos obtenidos en la VIEW
public class ControllerPersona {
private PersonaDAO personaDAO;
private VistaPersona vistaPersona;
public ControllerPersona() {
// Crear conexión a la base de datos
personaDAO = PersonaDAO.getInstance();
vistaPersona = new VistaPersona();
}
public void mostrarTodasLasPersonas() {
try {
List<Persona> personas = personaDAO.getAllPersonas();
vistaPersona.mostrarPersonas(personas);
} catch (SQLException e) {
e.printStackTrace();
}
}
public void mostraPersonaDNI() {
try {
String dni = vistaPersona.obtenerDni();
Persona persona = personaDAO.getPersonaByDni(dni);
vistaPersona.mostrarPersona(persona);
} catch (SQLException e) {
e.printStackTrace();
}
}
public void crearPersona() {
try {
Persona persona=vistaPersona.crearPersona();
personaDAO.insertPersona(persona);
} catch (SQLException e) {
e.printStackTrace();
}
}
public void actualizarPersona() {
try {
Persona persona=vistaPersona.obtenerDatosActualizados();
personaDAO.updatePersona(persona);
} catch (SQLException e) {
e.printStackTrace();
}
}
public void eliminarPersona() {
try {
String dni=vistaPersona.obtenerDniAEliminar();
personaDAO.deletePersonaByDni(dni);
} catch (SQLException e) {
e.printStackTrace();
}
}
// Otros métodos del controlador...
}
Podemos tener una clase principal con un menú que realmente podría ser parte del View, pero para simplificar vamos a crear como inicio de la aplicación. Mostrará un menú que permitirá llamar al controller en diferentes acciones
public class Main {
public static void main(String[] args) {
//VistaPersona vista = new VistaPersona();
ControllerPersona controlador = new ControllerPersona();
Scanner scanner = new Scanner(System.in);
int opcion;
do {
System.out.println("\nMenú:");
System.out.println("1. Mostrar todas las personas");
System.out.println("2. Crear persona");
System.out.println("3. Actualizar persona");
System.out.println("4. Eliminar persona");
System.out.println("5. Mostrar persona por DNI");
System.out.println("6. Salir");
System.out.print("Elige una opción: ");
opcion = Integer.parseInt(scanner.nextLine());
switch (opcion) {
case 1:
controlador.mostrarTodasLasPersonas();
break;
case 2:
controlador.crearPersona();
System.out.println("Persona creada correctamente.");
break;
case 3:
controlador.actualizarPersona();
System.out.println("Persona actualizada correctamente.");
break;
case 4:
controlador.eliminarPersona();
System.out.println("Persona eliminada correctamente.");
break;
case 5:
controlador.mostraPersonaDNI();
break;
case 6:
System.out.println("Saliendo...");
break;
default:
System.out.println("Opción no válida.");
}
} while (opcion != 6);
}
}
Fichero README¶
Readme: el propio nombre, léeme, indica su propósito: {++ser leído++}. El archivo readme es el primer archivo que un desarrollador debe mirar antes de embarcarse en un proyecto, por lo que también es esencial saber cómo escribir un buen archivo readme, para que toda la información relevante se presente de forma compacta.
Consejo
El nombre del archivo se escribe README en mayúsculas. De este modo, los sistemas que diferencian entre mayúsculas y minúsculas listarán el archivo antes que todos los demás archivos que empiezan con minúsculas.
¿Qué suelen incluir los ficheros README?¶
Suelen incluir información sobre:
- Una descripción general del sistema o proyecto.
- El estado del proyecto, que es particularmente importante si el proyecto está todavía en desarrollo. En él se mencionan los cambios planeados y la dirección de desarrollo del proyecto, y se especifica directamente si un proyecto está terminado.
- Los requisitos del entorno de desarrollo para la integración.
- Una lista de las tecnologías utilizadas y, cuando proceda, enlaces con más información.
- Bugs conocidos y posibles correcciones de errores.
- Sección de preguntas frecuentes con todas las preguntas planteadas hasta la fecha.
- Información sobre derechos de autor y licencias.
Cómo escribir un fichero README¶
El contenido del fichero README debe estar en inglés.
Exportar la BBDD de MySQL¶
En MySQL workbench seleccionamos Server → Data Export
Selecciono el esquema de BBDD que quiero exportar y hago click en Start export
Workbench me muestra dónde se ha generado el fichero: