4.2. Datos#

Hasta ahora no nos hemos detenido en las particularidades de los tipos de datos, ya que los que muestran nuestro ejemplo son bastante simples. Sin embargo, la correspondencia entre tipos de datos de Java y de SQL no es exacta, por lo que JDBC implementa los métodos setXXXX de PreparedStatement para guardar datos en la base de datos y los métodos getXXXX de ResultSet para recuperarlos.

En principio (aunque hay excepciones), podemos usar estos métodos y abstraernos de cómo el SGBD lo traduce a un tipo SQL.

4.2.1. Datos simples#

Por datos simples entendemos los que representan caracteres, enteros, números reales (flotantes o de precisión fija) o booleanos y tienen, por lo general, un equivalente propio de Java.

Tipo SQL

Tipo Java[1]

Método JDBC de PreparedStatement

CHAR(n)
VARCHAR(n)

String

setString(int idx, String v)

SMALLINT
INTEGER
BIGINT
setShort(int idx, short v)
setInt(int idx, int v)
setLong(int idx, long v)

BOOLEAN

Boolean

setBoolean(int idx, boolean v)

NUMERIC/DECIMAL[2]

BigDecimal

setBigDecimal(int idx, BigDecimal)
FLOAT, REAL[3]
FLOAT, DOUBLE PRECISION
setFloat(int idx, float v)
setDouble(int idx, double v)

Nota

No hay más que sustituir set por get para obtener los métodos de PreparedStatement necesarios para guardar datos.

Ejemplos de cómo obtener o escribir datos de este tipo ya se han dejado escritos en los ejemplos de apartados anteriores. Sólo BigDecimal no es un tipo primitivo en Java, pero su uso es trivial:

BigDecimal comaFija = new BigDecimal("13.456");

Aclaración

Dado que hemos escogido SQLite para desarrollar la unidad, nos conviene aclarar sus particularidades. Su característica fundamental en lo referente a tipos es que tiene tipado dinámico y cuál sea el tipo que declaremos al crear la tabla es irrelevante, porque el sistema gestor aceptará el dato con independencia de su tipo. Por ejemplo:

CREATE TABLE Persona (
   nombre    VARCHAR(255);
);

INSERT INTO Persona VALUES
   ("Manolo"),   // Consecuente con la definición: no da problemas.
   (4356);       // Inconsecuente, pero da igual: el dato se almacena como entero.

De hecho, SQLite ni siquiera atiende a qué palabra usamos para definir el tipo y creará la tabla, incluso aunque nos inventemos el nombre del tipo:

CREATE TABLE Persona (
   nombre    TIPOINVENTADO   // No da error.
);

Internamente, SQLite sólo dispone de datos de tipo texto, entero (de diverso tamaño), doble y BLOB; y dependiendo del valor que se suministre usará un tipo u otro para el dato. Así, CHAR y VARCHAR se asimilan a texto, INTEGER, BIGINT, SMALLINT y BOOLEAN a enteros, FLOAT, DOBLE y NUMERIC/DECIMAL a dobles (por tanto, se perderá la precisión de este último tipo).

4.2.2. Datos complejos#

Los datos complejos se caracterizan porque el paquete java.sql tiene definidos tipos específicos que se corresponden con los definidos en el estándar SQL.

Tipo SQL

Tipo Java

Método JDBC de PreparedStatement

DATE
TIME
TIMESTAMP
setDate(int idx, java.sql.Date v)
setTime(int idx, java.sql.Time v)
setTimestamp(int idx, java.sql.Timestamp v)

BLOB

setBlob(int idx, java.sql.Blob v)
setBinaryStream(int idx, InputStream v)

CLOB

setClob(int idx, java.sql.Clob v)
setCharacterStream(int idx, Reader v)

JSON

String

setString(int idx, String v)

ARRAY
STRUCT
setArray(int idx, java.sql.Array v)
setStruct(int idx, java.sql.Struct v)

4.2.2.1. Fechas y tiempos#

El estándar SQL define cinco tipos de datos para la expresión de tiempos:

  1. DATE que sirve para definir fechas (p.e. '2014-01-08').

  2. TIME que sirve para definir horas con precisión de segundos (p.e. '08:30:21'), aunque también podría incluirse precisión de hasta el microsegundo, añadiendo decimales al segundo.

  3. TIMESTAMP que combina en un mismo tipo fecha y hora (p.e. '2014-01-08 08:30:21').

  4. TIMESTAMP WITH TIME ZONE que permite almacenar, además, el huso horario (p.e. '2014-01-08 08:30:21+01:00').

  5. INTERVAL para almacenar periodos de tiempo (p.e. 'INTERVAL 2 DAYS' o 'INTERVAL 2 DAYS 10 HOURS').

A través de JDBC sólo se soportan directamente los tres primeros tipos y, además, se requiere saber cómo convertir entre java.sql.Date, java.sql.Time, java.sql.Timestamp y los tipos con los que frecuentemente se trabaja en Java:

import java.time.LocalDate;
import java.time.LocalTime;
import java.time.LocalDateTime;
import java.sql.Date;
import java.sql.Time;
import java.sql.Timestamp;

java.util.Date udate = new java.util.Date(); // Almacena fecha y hora.

// Date --> Date (SQL)
Date date = new Date(udate.getTime());

// Date (SQL) --> Date
udate = new Date(date.getTime());

// Date --> Time (SQL)
Time time = new Time(date.getTime());

// Time (SQL) --> Date
date = new Date(time.getTime());

// Date --> Timestamp (SQL)
Timestamp timestamp = new Timestamp(date.getTime());

// Timestamp (SQL) --> Date
date = new Date(timestamp.getTime());

LocalDate localDate = LocalDate.now();

// LocalDate --> Date (SQL)
date = Date.valueOf(localDate);

// Date (SQL) --> LocalDate
localDate = date.toLocalDate();

LocalTime localTime = LocalTime.now();

// LocalTime --> Time (SQL)
sqltime = Time.valueOf(localTime);

// Time (SQL) --> Localtime
localTime = Time.toLocalTime();

LocalDateTime localDateTime = LocalDateTime.now();

// LocalDateTime --> Timestamp (SQL)
timestamp = Timestamp.valueOf(localDateTime);

// Timestamp (SQL) --> LocalDateTime
localDateTime = timestamp.toLocalDateTime();

4.2.2.2. BLOB y CLOB#

Ambos tipos representan datos de tamaño considerable, BLOB datos binarios (p.e. una imagen) y CLOB un conjunto de caracteres, o sea, un texto mayor que el que se podría almacenar con VARCHAR (cuyo límite depende del SGBD). Al margen de esa diferencia, explicado uno, explicado el otro.

Por ejemplo, si tuviéramos un archivo con una foto que quisiéramos guardar en una base de datos podríamos hacer:

try (
   Connection conn = DriverManager.getConnection(dbUrl);
) {
   try(
      PreparedStatement pstmt = conn.preparedStatement("INSERT INTO Persona (nombre, avatar) VALUES (?, ?)")
   ) {
      Path archivo = Path.of("ruta", "al", "archivo", "jpg");
      try(InputStream st = Files.newInputStream(archivo)) {
         pstmt.setString(1, "Manolito");
         pstmt.setBinaryStream(2, st);
         pstmt.executeUpdate();
      }
   }
}

También podríamos querer guardar un archivo binaro ya cargado en memoria:

byte[] archivo = new byte[] {10, 20, 5, 50, 12, 221, 13}
Blob blob = conn.createBlob();
blob.setBytes(1, archivo); // Agregamos la secuencia de bytes al principio del Blob.

try (
   Connection conn = DriverManager.getConnection(dbUrl);
) {
   try(
      PreparedStatement pstmt = conn.preparedStatement("INSERT INTO Persona (nombre, avatar) VALUES (?, ?)")
   ) {
      pstmt.setString(1, "Manolito");
      pstmt.setBlob(2, blob);
      pstmt.executeUpdate();
   }
   finally {
      blob.free();  // Vaciamos el blob para liberar la memoria.
   }
}

4.2.2.3. JSON#

Desde SQL:2023 el estándar soporta de forma nativa el tipo JSON. Sin embargo, JDBC aún no tiene soporte alguno para ello, así que el único modo de tratarlo es a través de String.

4.2.2.4. ARRAY y STRUCT#

El tipo de dato ARRAY es, simplemente, una secuencia de datos de un mismo tipo, o sea, lo que entenderíamos como array en cualquier lenguaje de programación:

CREATE TABLE Trabaja (
   profesor        INTEGER,
   claustro        INTEGER,
   departamento    INTEGER,
   -- Para poder asignar varios casilleros a un mismo profesor
   casillero       INTEGER ARRAY   NOT NULL,

   /* Restricciones */
);

STRUCT, en cambio, es un tipo de dato que permite incluir como valor de un campo una estructura de datos al modo de las estructuras C o los mapas de Python o Java:

// No pueden definirse restricciones en la definición, así que estas
// (p.e. tipo_via debería incluir un CHECK con varios valores)
// deben definirse en la tabla en la que se incluya este tipo struct.
CREATE TYPE domicilio AS (
   tipo_via       VARCHAR(40),
   nombre_via     VARCHAR(150),
   numero         INTEGER,
   bloque         CHAR(1),
   escalera       CHAR(1),
   piso           INTEGER,
   letra          CHAR(2)
);

Aclaración

SQLite no soporta de forma nativa los datos complejos (ya explicamos al tratar los datos simples cómo funcionan en realidad los tipos en él). En particular:

DATE/TIME/TIMESTAMP

Puede almacenarlos como una cadena (“2024-12-12”), un entero(el tiempo UNIX 1733961600) o un flotante (fecha juliana usada en Astronomía). Para darles soporte añade funciones específicas.

En el caso particular de JDBC, setDate almacenará la fecha como un entero, lo cual nos es indiferente si leemos los campos con getDate, pero quizás no nos guste tanto, si la lectura la hacemos por otros medios (p.e. usando directamente el cliente de SQLite sin echar mano de funciones específicas).

BLOB

Es el único dato complejo que realmente soporta SQLite, así que no tendremos problemas con él.

CLOB

SQLite no le da un tratamiento especial y se trata como cualquier otra cadena, ya que internamente SQLite sólo tiene un tipo para datos que son cadenas. Sin embargo, setClob no está implementado para él, por lo que tendremos que usar setString.

JSON

No tiene soporte nativo sino a través de funciones específicas. En cualquier caso, JDBC tampoco lo tiene con lo que tendrá que usarse setString igual que para el resto de SGBD.

ARRAY/STRUCT

No tienen soporte en SQLite y, además, los métodos setArray y setStruct no están implementados para el driver.

Notas al pie