JavaRush /Blog Java /Random-ES /JDBC o donde todo comienza
Viacheslav
Nivel 3

JDBC o donde todo comienza

Publicado en el grupo Random-ES
En el mundo moderno, no hay solución sin almacenamiento de datos. Y la historia del trabajo con bases de datos comenzó hace mucho tiempo, con la llegada de JDBC. Propongo recordar algo de lo que ningún marco moderno construido sobre JDBC puede prescindir. Además, incluso cuando trabajas con ellos, en ocasiones puedes necesitar la oportunidad de “volver a tus raíces”. Espero que esta reseña le sirva como introducción o le ayude a refrescar la memoria.
JDBC o donde todo comienza - 1

Introducción

Uno de los principales propósitos de un lenguaje de programación es almacenar y procesar información. Para comprender mejor cómo funciona el almacenamiento de datos, vale la pena dedicar un poco de tiempo a la teoría y arquitectura de las aplicaciones. Por ejemplo, puede leer la literatura, concretamente el libro " Manual del arquitecto de software: conviértase en un arquitecto de software exitoso implementando un arco efectivo... " de Joseph Ingeno. Como se dijo, existe un determinado nivel de datos o “capa de datos”. Incluye un lugar para almacenar datos (por ejemplo, una base de datos SQL) y herramientas para trabajar con un almacén de datos (por ejemplo, JDBC, que se discutirá más adelante). También hay un artículo en el sitio web de Microsoft: " Diseño de una capa de persistencia de infraestructura ", que describe la solución arquitectónica de separar una capa adicional del nivel de datos: la capa de persistencia. En este caso, la capa de datos es el nivel de almacenamiento de los datos en sí, mientras que la capa de persistencia es algún nivel de abstracción para trabajar con datos del almacenamiento desde el nivel de la capa de datos. La capa de persistencia puede incluir la plantilla "DAO" o varios ORM. Pero ORM es un tema para otra discusión. Como ya habrás comprendido, el nivel de datos apareció primero. Desde la época de JDK 1.1, JDBC (Java DataBase Connectivity - conexión a bases de datos en Java) ha aparecido en el mundo Java. Este es un estándar para la interacción de aplicaciones Java con varios DBMS, implementado en forma de paquetes java.sql y javax.sql incluidos en Java SE:
JDBC o donde todo comienza - 2
Este estándar se describe en la especificación " JSR 221 JDBC 4.1 API ". Esta especificación nos dice que la API JDBC proporciona acceso programático a bases de datos relacionales desde programas escritos en Java. También indica que la API JDBC es parte de la plataforma Java y, por lo tanto, está incluida en Java SE y Java EE. La API JDBC se proporciona en dos paquetes: java.sql y javax.sql. Conozcámoslos entonces.
JDBC o donde todo comienza - 3

comienzo del trabajo

Para comprender qué es la API JDBC en general, necesitamos una aplicación Java. Lo más conveniente es utilizar uno de los sistemas de montaje del proyecto. Por ejemplo, usemos Gradle . Puede leer más sobre Gradle en una breve reseña: " Una breve introducción a Gradle ". Primero, inicialicemos un nuevo proyecto Gradle. Dado que la funcionalidad de Gradle se implementa a través de complementos, necesitamos usar el " Complemento Gradle Build Init " para la inicialización:
gradle init --type java-application
Después de esto, abramos el script de compilación: el archivo build.gradle , que describe nuestro proyecto y cómo trabajar con él. Nos interesa el bloque " dependencias ", donde se describen las dependencias, es decir, aquellas bibliotecas/frameworks/api sin las cuales no podemos trabajar y de las que dependemos. Por defecto veremos algo como:
dependencies {
    // This dependency is found on compile classpath of this component and consumers.
    implementation 'com.google.guava:guava:26.0-jre'
    // Use JUnit test framework
    testImplementation 'junit:junit:4.12'
}
¿Por qué estamos viendo esto aquí? Estas son las dependencias de nuestro proyecto que Gradle generó automáticamente para nosotros al crear el proyecto. Y también porque guava es una biblioteca independiente que no está incluida en Java SE. JUnit tampoco está incluido con Java SE. Pero tenemos JDBC listo para usar, es decir, es parte de Java SE. Resulta que tenemos JDBC. Excelente. ¿Que más necesitamos? Hay un diagrama tan maravilloso:
JDBC o donde todo comienza - 4
Como vemos, y esto es lógico, la base de datos es un componente externo que no es nativo de Java SE. Esto se explica de forma sencilla: existe una gran cantidad de bases de datos y se puede trabajar con cualquiera. Por ejemplo, existen PostgreSQL, Oracle, MySQL, H2. Cada una de estas bases de datos es suministrada por una empresa independiente llamada proveedores de bases de datos. Cada base de datos está escrita en su propio lenguaje de programación (no necesariamente Java). Para poder trabajar con la base de datos desde una aplicación Java, el proveedor de la base de datos escribe un controlador especial, que es su propio adaptador de imagen. Las que son compatibles con JDBC (es decir, las que tienen un controlador JDBC) también se denominan “bases de datos compatibles con JDBC”. Aquí podemos hacer una analogía con los dispositivos informáticos. Por ejemplo, en un bloc de notas hay un botón "Imprimir". Cada vez que lo presionas, el programa le dice al sistema operativo que la aplicación del bloc de notas quiere imprimir. Y tienes una impresora. Para enseñarle a su sistema operativo a comunicarse de manera uniforme con una impresora Canon o HP, necesitará controladores diferentes. Pero para ti, como usuario, nada cambiará. Seguirás presionando el mismo botón. Lo mismo con JDBC. Está ejecutando el mismo código, solo que es posible que se estén ejecutando diferentes bases de datos bajo el capó. Creo que este es un enfoque muy claro. Cada uno de estos controladores JDBC es algún tipo de artefacto, biblioteca o archivo jar. Esta es la dependencia de nuestro proyecto. Por ejemplo, podemos seleccionar la base de datos " H2 Database " y luego necesitamos agregar una dependencia como esta:
dependencies {
    implementation 'com.h2database:h2:1.4.197'
Cómo encontrar una dependencia y cómo describirla se indica en los sitios web oficiales del proveedor de la base de datos o en " Maven Central ". El controlador JDBC no es una base de datos, como comprenderá. Pero él es sólo una guía para ello. Pero existe algo llamado " Bases de datos en memoria ". Estas son bases de datos que existen en la memoria durante la vida útil de su aplicación. Por lo general, esto se utiliza con fines de prueba o capacitación. Esto le permite evitar instalar un servidor de base de datos independiente en la máquina. Lo cual es muy adecuado para que nos familiaricemos con JDBC. Entonces nuestra caja de arena está lista y comenzamos.
JDBC o donde todo comienza - 5

Conexión

Entonces, tenemos un controlador JDBC, tenemos una API JDBC. Como recordamos, JDBC significa Java DataBase Connectivity. Por lo tanto, todo comienza con la Conectividad: la capacidad de establecer una conexión. Y conexión es Conexión. Volvamos nuevamente al texto de la especificación JDBC y miremos el índice. En el capítulo " CAPÍTULO 4 Descripción general " (descripción general) pasamos a la sección " 4.1 Estableciendo una conexión " (estableciendo una conexión) se dice que existen dos formas de conectarse a la base de datos:
  • A través de DriverManager
  • A través de fuente de datos
Tratemos con DriverManager. Como se dijo, DriverManager le permite conectarse a la base de datos en la URL especificada y también carga los controladores JDBC que encontró en CLASSPATH (y antes, antes de JDBC 4.0, tenía que cargar la clase de controlador usted mismo). Hay un capítulo separado “CAPÍTULO 9 Conexiones” sobre la conexión a la base de datos. Estamos interesados ​​en cómo conseguir una conexión a través de DriverManager, por eso nos interesa la sección "9.3 La clase DriverManager". Nos indica cómo podemos acceder a la base de datos:
Connection con = DriverManager.getConnection(url, user, passwd);
Los parámetros los podemos tomar de la web de la base de datos que hayamos elegido. En nuestro caso, este es H2 - " Hoja de referencia H2 ". Pasemos a la clase AppTest preparada por Gradle. Contiene pruebas JUnit. Una prueba JUnit es un método que está marcado con una anotación @Test. Las pruebas unitarias no son el tema de esta revisión, por lo que simplemente nos limitaremos a entender que estos son métodos descritos de cierta manera, cuyo propósito es probar algo. Según la especificación JDBC y el sitio web de H2, comprobaremos que hemos recibido una conexión a la base de datos. Escribamos un método para obtener una conexión:
private Connection getNewConnection() throws SQLException {
	String url = "jdbc:h2:mem:test";
	String user = "sa";
	String passwd = "sa";
	return DriverManager.getConnection(url, user, passwd);
}
Ahora escribamos una prueba para este método que verificará que la conexión esté realmente establecida:
@Test
public void shouldGetJdbcConnection() throws SQLException {
	try(Connection connection = getNewConnection()) {
		assertTrue(connection.isValid(1));
		assertFalse(connection.isClosed());
	}
}
Esta prueba, cuando se ejecute, verificará que la conexión resultante sea válida (creada correctamente) y que no esté cerrada. Al utilizar try-with-resources, liberaremos recursos una vez que ya no los necesitemos. Esto nos protegerá de conexiones caídas y pérdidas de memoria. Dado que cualquier acción con la base de datos requiere una conexión, proporcionemos los métodos de prueba restantes marcados @Test con una Conexión al comienzo de la prueba, que publicaremos después de la prueba. Para hacer esto, necesitamos dos anotaciones: @Before y @After. Agreguemos un nuevo campo a la clase AppTest que almacenará la conexión JDBC para las pruebas:
private static Connection connection;
Y agreguemos nuevos métodos:
@Before
public void init() throws SQLException {
	connection = getNewConnection();
}
@After
public void close() throws SQLException {
	connection.close();
}
Ahora, se garantiza que cualquier método de prueba tendrá una conexión JDBC y no es necesario crearla él mismo cada vez.
JDBC o donde todo comienza - 6

Declaraciones

A continuación nos interesan las Declaraciones o expresiones. Se describen en la documentación en el capítulo " CAPÍTULO 13 Declaraciones ". En primer lugar, dice que existen varios tipos o tipos de afirmaciones:
  • Declaración: expresión SQL que no contiene parámetros
  • PreparedStatement: declaración SQL preparada que contiene parámetros de entrada
  • CallableStatement: Expresión SQL con capacidad de obtener un valor de retorno de Procedimientos Almacenados SQL.
Entonces, al tener una conexión, podemos ejecutar alguna solicitud dentro del marco de esta conexión. Por tanto, es lógico que inicialmente obtengamos una instancia de la expresión SQL de Connection. Debes comenzar creando una tabla. Describamos la solicitud de creación de tabla como una variable de cadena. ¿Cómo hacerlo? Usemos algún tutorial como " sqltutorial.org ", " sqlbolt.com ", " postgresqltutorial.com ", " codecademy.com ". Usemos, por ejemplo, un ejemplo del curso SQL en khanacademy.org . Agreguemos un método para ejecutar una expresión en la base de datos:
private int executeUpdate(String query) throws SQLException {
	Statement statement = connection.createStatement();
	// Для Insert, Update, Delete
	int result = statement.executeUpdate(query);
	return result;
}
Agreguemos un método para crear una tabla de prueba usando el método anterior:
private void createCustomerTable() throws SQLException {
	String customerTableQuery = "CREATE TABLE customers " +
                "(id INTEGER PRIMARY KEY, name TEXT, age INTEGER)";
	String customerEntryQuery = "INSERT INTO customers " +
                "VALUES (73, 'Brian', 33)";
	executeUpdate(customerTableQuery);
	executeUpdate(customerEntryQuery);
}
Ahora probemos esto:
@Test
public void shouldCreateCustomerTable() throws SQLException {
	createCustomerTable();
	connection.createStatement().execute("SELECT * FROM customers");
}
Ahora ejecutemos la solicitud, e incluso con un parámetro:
@Test
public void shouldSelectData() throws SQLException {
 	createCustomerTable();
 	String query = "SELECT * FROM customers WHERE name = ?";
	PreparedStatement statement = connection.prepareStatement(query);
	statement.setString(1, "Brian");
	boolean hasResult = statement.execute();
	assertTrue(hasResult);
}
JDBC no admite parámetros con nombre para PreparedStatement, por lo que los parámetros en sí se especifican mediante preguntas y, al especificar el valor, indicamos el índice de la pregunta (comenzando desde 1, no desde cero). En la última prueba recibimos verdadero como indicación de si hay un resultado. Pero, ¿cómo se representa el resultado de la consulta en la API JDBC? Y se presenta como un ResultSet.
JDBC o donde todo comienza - 7

Conjunto resultante

El concepto de ResultSet se describe en la especificación API JDBC en el capítulo "CAPÍTULO 15 Conjuntos de resultados". En primer lugar, dice que ResultSet proporciona métodos para recuperar y manipular los resultados de las consultas ejecutadas. Es decir, si el método de ejecución nos devolvió verdadero, entonces podemos obtener un ResultSet. Movamos la llamada al método createCustomerTable() al método init, que está marcado como @Before. Ahora finalicemos nuestra prueba de selección de datos:
@Test
public void shouldSelectData() throws SQLException {
	String query = "SELECT * FROM customers WHERE name = ?";
	PreparedStatement statement = connection.prepareStatement(query);
	statement.setString(1, "Brian");
	boolean hasResult = statement.execute();
	assertTrue(hasResult);
	// Обработаем результат
	ResultSet resultSet = statement.getResultSet();
	resultSet.next();
	int age = resultSet.getInt("age");
	assertEquals(33, age);
}
Vale la pena señalar aquí que el siguiente es un método que mueve el llamado "cursor". El cursor en ResultSet apunta a alguna fila. Por lo tanto, para leer una línea, es necesario colocar este mismo cursor sobre ella. Cuando se mueve el cursor, el método de movimiento del cursor devuelve verdadero si el cursor es válido (correcto, correcto), es decir, apunta a datos. Si devuelve falso, entonces no hay datos, es decir, el cursor no apunta a los datos. Si intentamos obtener datos con un cursor no válido, obtendremos el error: No hay datos disponibles. También es interesante que a través de ResultSet puedes actualizar o incluso insertar filas:
@Test
public void shouldInsertInResultSet() throws SQLException {
	Statement statement = connection.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE);
	ResultSet resultSet = statement.executeQuery("SELECT * FROM customers");
	resultSet.moveToInsertRow();
	resultSet.updateLong("id", 3L);
	resultSet.updateString("name", "John");
	resultSet.updateInt("age", 18);
	resultSet.insertRow();
	resultSet.moveToCurrentRow();
}

Conjunto de filas

Además de ResultSet, JDBC introduce el concepto de RowSet. Puede leer más aquí: " Conceptos básicos de JDBC: uso de objetos RowSet ". Hay varias variaciones de uso. Por ejemplo, el caso más sencillo podría verse así:
@Test
public void shouldUseRowSet() throws SQLException {
 	JdbcRowSet jdbcRs = new JdbcRowSetImpl(connection);
 	jdbcRs.setCommand("SELECT * FROM customers");
	jdbcRs.execute();
	jdbcRs.next();
	String name = jdbcRs.getString("name");
	assertEquals("Brian", name);
}
Como puede ver, RowSet es similar a una simbiosis de una declaración (especificamos un comando a través de ella) y un comando ejecutado. A través de él controlamos el cursor (llamando al siguiente método) y obtenemos datos de él. No sólo es interesante este enfoque, sino también sus posibles implementaciones. Por ejemplo, CachedRowSet. Está "desconectado" (es decir, no utiliza una conexión persistente a la base de datos) y requiere sincronización explícita con la base de datos:
CachedRowSet jdbcRsCached = new CachedRowSetImpl();
jdbcRsCached.acceptChanges(connection);
Puede leer más en el tutorial en el sitio web de Oracle: " Uso de CachedRowSetObjects ".
JDBC o donde todo comienza - 8

Metadatos

Además de las consultas, una conexión a la base de datos (es decir, una instancia de la clase Conexión) proporciona acceso a metadatos: datos sobre cómo está configurada y organizada nuestra base de datos. Pero primero, mencionemos algunos puntos clave: La URL para conectarse a nuestra base de datos: “jdbc:h2:mem:test”. test es el nombre de nuestra base de datos. Para la API JDBC, este es un directorio. Y el nombre estará en mayúsculas, es decir, TEST. El esquema predeterminado para H2 es PÚBLICO. Ahora, escribamos una prueba que muestre todas las tablas de usuarios. ¿Por qué personalizado? Porque las bases de datos contienen no solo tablas de usuario (aquellas que nosotros mismos creamos usando expresiones de creación de tablas), sino también tablas del sistema. Son necesarios para almacenar información del sistema sobre la estructura de la base de datos. Cada base de datos puede almacenar dichas tablas del sistema de forma diferente. Por ejemplo, en H2 se almacenan en el esquema " INFORMACIÓN_SCHEMA ". Curiosamente, el ESQUEMA DE INFORMACIÓN es un enfoque común, pero Oracle tomó un camino diferente. Puedes leer más aquí: " INFORMACIÓN_SCHEMA y Oracle ". Escribamos una prueba que reciba metadatos en tablas de usuarios:
@Test
public void shoudGetMetadata() throws SQLException {
	// У нас URL = "jdbc:h2:mem:test", где test - название БД
	// Название БД = catalog
	DatabaseMetaData metaData = connection.getMetaData();
	ResultSet result = metaData.getTables("TEST", "PUBLIC", "%", null);
	List<String> tables = new ArrayList<>();
	while(result.next()) {
		tables.add(result.getString(2) + "." + result.getString(3));
	}
	assertTrue(tables.contains("PUBLIC.CUSTOMERS"));
}
JDBC o donde todo comienza - 9

Grupo de conexiones

El grupo de conexiones en la especificación JDBC tiene una sección llamada "Capítulo 11 Agrupación de conexiones". También proporciona la justificación principal de la necesidad de un grupo de conexiones. Cada Coonection es una conexión física a la base de datos. Su creación y cierre es un trabajo bastante "caro". JDBC solo proporciona una API de agrupación de conexiones. Por tanto, la elección de la implementación sigue siendo nuestra. Por ejemplo, tales implementaciones incluyen HikariCP . En consecuencia, necesitaremos agregar un grupo a la dependencia de nuestro proyecto:
dependencies {
    implementation 'com.h2database:h2:1.4.197'
    implementation 'com.zaxxer:HikariCP:3.3.1'
    testImplementation 'junit:junit:4.12'
}
Ahora necesitamos usar este grupo de alguna manera. Para hacer esto, necesita inicializar la fuente de datos, también conocida como Datasource:
private DataSource getDatasource() {
	HikariConfig config = new HikariConfig();
	config.setUsername("sa");
	config.setPassword("sa");
	config.setJdbcUrl("jdbc:h2:mem:test");
	DataSource ds = new HikariDataSource(config);
	return ds;
}
Y escribamos una prueba para recibir una conexión del grupo:
@Test
public void shouldGetConnectionFromDataSource() throws SQLException {
	DataSource datasource = getDatasource();
	try (Connection con = datasource.getConnection()) {
		assertTrue(con.isValid(1));
	}
}
JDBC o donde todo empieza - 10

Actas

Una de las cosas más interesantes de JDBC son las transacciones. En la especificación JDBC se les asigna el capítulo "CAPÍTULO 10 Transacciones". En primer lugar, conviene entender qué es una transacción. Una transacción es un grupo de operaciones secuenciales combinadas lógicamente sobre datos, procesadas o canceladas en su conjunto. ¿Cuándo comienza una transacción cuando se utiliza JDBC? Como indica la especificación, esto lo maneja directamente el controlador JDBC. Pero normalmente, una nueva transacción comienza cuando la declaración SQL actual requiere una transacción y la transacción aún no se ha creado. ¿Cuándo finaliza la transacción? Esto está controlado por el atributo de confirmación automática. Si la confirmación automática está habilitada, la transacción se completará después de que se "completa" la declaración SQL. Lo que significa "hecho" depende del tipo de expresión SQL:
  • Lenguaje de manipulación de datos, también conocido como DML (Insertar, Actualizar, Eliminar).
    La transacción se completa tan pronto como se completa la acción.
  • Seleccionar declaraciones
    La transacción se completa cuando se cierra el ResultSet ( ResultSet#close )
  • CallableStatement y expresiones que devuelven múltiples resultados
    Cuando todos los ResultSets asociados se han cerrado y se han recibido todos los resultados (incluido el número de actualizaciones)
Así es exactamente como se comporta la API JDBC. Como de costumbre, escribamos una prueba para esto:
@Test
public void shouldCommitTransaction() throws SQLException {
	connection.setAutoCommit(false);
	String query = "INSERT INTO customers VALUES (1, 'Max', 20)";
	connection.createStatement().executeUpdate(query);
	connection.commit();
	Statement statement = connection.createStatement();
 	statement.execute("SELECT * FROM customers");
	ResultSet resultSet = statement.getResultSet();
	int count = 0;
	while(resultSet.next()) {
		count++;
	}
	assertEquals(2, count);
}
Es sencillo. Pero esto es cierto siempre que solo tengamos una transacción. ¿Qué hacer cuando son varios? Necesitan estar aislados unos de otros. Por lo tanto, hablemos de los niveles de aislamiento de transacciones y de cómo JDBC los maneja.
JDBC o donde todo comienza - 11

Niveles de aislamiento

Abramos la subsección "10.2 Niveles de aislamiento de transacciones" de la especificación JDBC. Aquí, antes de continuar, me gustaría recordar algo como ACID. ACID describe los requisitos para un sistema transaccional.
  • Atomicidad:
    Ninguna transacción se comprometerá parcialmente con el sistema. O se realizarán todas sus suboperaciones o no se realizará ninguna.
  • Consistencia:
    cada transacción exitosa, por definición, registra solo resultados válidos.
  • Aislamiento:
    mientras se ejecuta una transacción, las transacciones simultáneas no deberían afectar su resultado.
  • Durabilidad:
    Si una transacción se completa con éxito, los cambios realizados en ella no se desharán debido a ningún fallo.
Cuando hablamos de niveles de aislamiento de transacciones, nos referimos al requisito de "Aislamiento". El aislamiento es un requisito costoso, por lo que en las bases de datos reales existen modos que no aíslan completamente una transacción (niveles de aislamiento de lectura repetible e inferiores). Wikipedia tiene una excelente explicación de los problemas que pueden surgir al trabajar con transacciones. Vale la pena leer más aquí: " Problemas de acceso paralelo mediante transacciones ". Antes de escribir nuestra prueba, cambiemos ligeramente nuestro Gradle Build Script: agreguemos un bloque con propiedades, es decir, con la configuración de nuestro proyecto:
ext {
    h2Version = '1.3.176' // 1.4.177
    hikariVersion = '3.3.1'
    junitVersion = '4.12'
}
A continuación, usamos esto en versiones:
dependencies {
    implementation "com.h2database:h2:${h2Version}"
    implementation "com.zaxxer:HikariCP:${hikariVersion}"
    testImplementation "junit:junit:${junitVersion}"
}
Es posible que hayas notado que la versión h2 se ha vuelto más baja. Veremos por qué más adelante. Entonces, ¿cómo se aplican los niveles de aislamiento? Veamos ahora un pequeño ejemplo práctico:
@Test
public void shouldGetReadUncommited() throws SQLException {
	Connection first = getNewConnection();
	assertTrue(first.getMetaData().supportsTransactionIsolationLevel(Connection.TRANSACTION_READ_UNCOMMITTED));
	first.setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED);
	first.setAutoCommit(false);
	// Транзакиця на подключение. Поэтому первая транзакция с ReadUncommited вносит изменения
	String insertQuery = "INSERT INTO customers VALUES (5, 'Max', 15)";
	first.createStatement().executeUpdate(insertQuery);
	// Вторая транзакция пытается их увидеть
	int rowCount = 0;
	JdbcRowSet jdbcRs = new JdbcRowSetImpl(getNewConnection());
	jdbcRs.setCommand("SELECT * FROM customers");
	jdbcRs.execute();
	while (jdbcRs.next()) {
		rowCount++;
	}
	assertEquals(2, rowCount);
}
Curiosamente, esta prueba puede fallar en un proveedor que no admite TRANSACTION_READ_UNCOMMITTED (por ejemplo, sqlite o HSQL). Y es posible que el nivel de transacción simplemente no funcione. ¿Recuerda que indicamos la versión del controlador de la base de datos H2? Si lo elevamos a h2Version = '1.4.177' y superior, entonces READ UNCOMMITTED dejará de funcionar, aunque no hayamos cambiado el código. Esto demuestra una vez más que la elección del proveedor y la versión del controlador no son solo letras, sino que en realidad determinará cómo se ejecutarán sus solicitudes. Puede leer sobre cómo solucionar este comportamiento en la versión 1.4.177 y cómo no funciona en versiones superiores aquí: " Soporte LEER nivel de aislamiento NO COMPROMETIDO en modo MVStore ".
JDBC o donde todo comienza - 12

Línea de fondo

Como podemos ver, JDBC es una poderosa herramienta en manos de Java para trabajar con bases de datos. Espero que esta breve reseña le ayude a tener un punto de partida o le ayude a refrescar su memoria. Pues para la merienda, algunos materiales adicionales: #viacheslav
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION