JavaRush /Blogue Java /Random-PT /JDBC ou onde tudo começa
Viacheslav
Nível 3

JDBC ou onde tudo começa

Publicado no grupo Random-PT
No mundo moderno, não há como sem armazenamento de dados. E a história do trabalho com bancos de dados começou há muito tempo, com o advento do JDBC. Proponho lembrar algo que nenhuma estrutura moderna construída sobre JDBC pode prescindir. Além disso, mesmo trabalhando com eles, às vezes você pode precisar da oportunidade de “voltar às suas raízes”. Espero que esta revisão ajude como uma introdução ou ajude a refrescar sua memória.
JDBC ou onde tudo começa - 1

Introdução

Um dos principais objetivos de uma linguagem de programação é armazenar e processar informações. Para entender melhor como funciona o armazenamento de dados, vale a pena dedicar um pouco de tempo à teoria e arquitetura das aplicações. Por exemplo, você pode ler a literatura, nomeadamente o livro " Manual do Arquiteto de Software: Torne-se um arquiteto de software de sucesso implementando um arco eficaz... " de Joseph Ingeno. Como dito, existe uma certa camada de dados ou “camada de dados”. Inclui um local para armazenar dados (por exemplo, um banco de dados SQL) e ferramentas para trabalhar com um armazenamento de dados (por exemplo, JDBC, que será discutido). Há também um artigo no site da Microsoft: “ Projetando uma camada de persistência de infraestrutura ”, que descreve a solução arquitetônica de separar uma camada adicional da camada de dados – a camada de persistência. Neste caso, o Data Tier é o nível de armazenamento dos dados em si, enquanto a Persistence Layer é algum nível de abstração para trabalhar com dados do armazenamento do nível Data Tier. A camada de persistência pode incluir o modelo “DAO” ou vários ORMs. Mas ORM é um tema para outra discussão. Como você já deve ter entendido, o Data Tier apareceu primeiro. Desde a época do JDK 1.1, o JDBC (Java DataBase Connectivity - conexão com bancos de dados em Java) apareceu no mundo Java. Este é um padrão para interação de aplicações Java com diversos SGBDs, implementado na forma de pacotes java.sql e javax.sql incluídos no Java SE:
JDBC ou onde tudo começa - 2
Este padrão é descrito pela especificação " JSR 221 JDBC 4.1 API ". Esta especificação nos diz que a API JDBC fornece acesso programático a bancos de dados relacionais a partir de programas escritos em Java. Também informa que a API JDBC faz parte da plataforma Java e, portanto, está incluída no Java SE e no Java EE. A API JDBC é fornecida em dois pacotes: java.sql e javax.sql. Vamos conhecê-los então.
JDBC ou onde tudo começa - 3

Início do trabalho

Para entender o que é a API JDBC em geral, precisamos de um aplicativo Java. É mais conveniente usar um dos sistemas de montagem do projeto. Por exemplo, vamos usar Gradle . Você pode ler mais sobre o Gradle em uma breve revisão: " Uma Breve Introdução ao Gradle ". Primeiro, vamos inicializar um novo projeto Gradle. Como a funcionalidade do Gradle é implementada por meio de plug-ins, precisamos usar “ Gradle Build Init Plugin ” para inicialização:
gradle init --type java-application
Depois disso, vamos abrir o script de construção - o arquivo build.gradle , que descreve nosso projeto e como trabalhar com ele. Estamos interessados ​​​​no bloco " dependências ", onde são descritas as dependências - ou seja, aquelas bibliotecas/frameworks/api, sem as quais não podemos trabalhar e das quais dependemos. Por padrão, 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 que estamos vendo isso aqui? Estas são as dependências do nosso projeto que o Gradle gerou automaticamente para nós ao criar o projeto. E também porque goiaba é uma biblioteca separada que não está incluída no Java SE. JUnit também não está incluído no Java SE. Mas temos o JDBC pronto para uso, ou seja, faz parte do Java SE. Acontece que temos JDBC. Ótimo. O que mais nós precisamos? Existe um diagrama tão maravilhoso:
JDBC ou onde tudo começa – 4
Como podemos ver, e isso é lógico, o banco de dados é um componente externo que não é nativo do Java SE. Isso é explicado de forma simples - há um grande número de bancos de dados e você pode trabalhar com qualquer um. Por exemplo, existe PostgreSQL, Oracle, MySQL, H2. Cada um desses bancos de dados é fornecido por uma empresa separada chamada de fornecedores de bancos de dados. Cada banco de dados é escrito em sua própria linguagem de programação (não necessariamente Java). Para poder trabalhar com o banco de dados a partir de uma aplicação Java, o provedor do banco de dados escreve um driver especial, que é seu próprio adaptador de imagem. Esses compatíveis com JDBC (ou seja, aqueles que possuem um driver JDBC) também são chamados de “banco de dados compatível com JDBC”. Aqui podemos fazer uma analogia com dispositivos de computador. Por exemplo, em um bloco de notas existe um botão "Imprimir". Cada vez que você pressiona, o programa informa ao sistema operacional que o aplicativo bloco de notas deseja imprimir. E você tem uma impressora. Para ensinar seu sistema operacional a se comunicar uniformemente com uma impressora Canon ou HP, você precisará de drivers diferentes. Mas para você, como usuário, nada mudará. Você ainda pressionará o mesmo botão. O mesmo acontece com JDBC. Você está executando o mesmo código, mas diferentes bancos de dados podem estar sendo executados nos bastidores. Acho que esta é uma abordagem muito clara. Cada driver JDBC é algum tipo de artefato, biblioteca ou arquivo jar. Esta é a dependência do nosso projeto. Por exemplo, podemos selecionar o banco de dados “ H2 Database ” e então precisamos adicionar uma dependência como esta:
dependencies {
    implementation 'com.h2database:h2:1.4.197'
Como encontrar uma dependência e como descrevê-la está indicado nos sites oficiais do provedor do banco de dados ou no " Maven Central ". O driver JDBC não é um banco de dados, como você entende. Mas ele é apenas um guia para isso. Mas existe algo como " Bancos de dados em memória ". Esses são bancos de dados que existem na memória durante a vida útil do seu aplicativo. Normalmente, isso é frequentemente usado para fins de teste ou treinamento. Isso permite evitar a instalação de um servidor de banco de dados separado na máquina. O que é muito adequado para conhecermos o JDBC. Então nossa sandbox está pronta e começamos.
JDBC ou onde tudo começa – 5

Conexão

Então, temos um driver JDBC, temos uma API JDBC. Como lembramos, JDBC significa Java DataBase Connectivity. Portanto, tudo começa com a Conectividade – a capacidade de estabelecer uma conexão. E conexão é conexão. Vamos voltar ao texto da especificação JDBC e dar uma olhada no índice. No capítulo " CAPÍTULO 4 Visão geral " (visão geral) passamos para a seção " 4.1 Estabelecendo uma conexão " (estabelecendo uma conexão) é dito que existem duas maneiras de se conectar ao banco de dados:
  • Através do DriverManager
  • Através da fonte de dados
Vamos lidar com DriverManager. Como dito, DriverManager permite que você se conecte ao banco de dados na URL especificada e também carrega drivers JDBC encontrados no CLASSPATH (e antes, antes do JDBC 4.0, você mesmo tinha que carregar a classe do driver). Há um capítulo separado “CAPÍTULO 9 Conexões” sobre conexão com o banco de dados. Estamos interessados ​​em como obter uma conexão através do DriverManager, por isso estamos interessados ​​na seção "9.3 A classe DriverManager". Indica como podemos acessar o banco de dados:
Connection con = DriverManager.getConnection(url, user, passwd);
Os parâmetros podem ser retirados do site do banco de dados que escolhemos. No nosso caso, é H2 - " Folha de dicas H2 ". Vamos passar para a classe AppTest preparada pelo Gradle. Ele contém testes JUnit. Um teste JUnit é um método marcado com uma anotação @Test. Os testes unitários não são o tema desta revisão, portanto nos limitaremos simplesmente a entender que se trata de métodos descritos de uma determinada forma, cujo objetivo é testar algo. De acordo com a especificação JDBC e o site H2, verificaremos se recebemos uma conexão com o banco de dados. Vamos escrever um método para obter uma conexão:
private Connection getNewConnection() throws SQLException {
	String url = "jdbc:h2:mem:test";
	String user = "sa";
	String passwd = "sa";
	return DriverManager.getConnection(url, user, passwd);
}
Agora vamos escrever um teste para este método que irá verificar se a conexão está realmente estabelecida:
@Test
public void shouldGetJdbcConnection() throws SQLException {
	try(Connection connection = getNewConnection()) {
		assertTrue(connection.isValid(1));
		assertFalse(connection.isClosed());
	}
}
Este teste, quando executado, irá verificar se a conexão resultante é válida (criada corretamente) e se não está fechada. Ao usar try-with-resources, liberaremos recursos quando não precisarmos mais deles. Isso nos protegerá de conexões flácidas e vazamentos de memória. Como qualquer ação com o banco de dados requer uma conexão, vamos fornecer os métodos de teste restantes marcados como @Test com uma Conexão no início do teste, que lançaremos após o teste. Para isso, precisamos de duas anotações: @Before e @After Vamos adicionar um novo campo à classe AppTest que armazenará a conexão JDBC para testes:
private static Connection connection;
E vamos adicionar novos métodos:
@Before
public void init() throws SQLException {
	connection = getNewConnection();
}
@After
public void close() throws SQLException {
	connection.close();
}
Agora, qualquer método de teste tem garantia de ter uma conexão JDBC e não precisa criá-la todas as vezes.
JDBC ou onde tudo começa – 6

Declarações

A seguir estamos interessados ​​em declarações ou expressões. Eles estão descritos na documentação do capítulo " CAPÍTULO 13 Declarações ". Em primeiro lugar, diz que existem vários tipos ou tipos de declarações:
  • Instrução: expressão SQL que não contém parâmetros
  • PreparedStatement: instrução SQL preparada contendo parâmetros de entrada
  • CallableStatement: Expressão SQL com capacidade de obter um valor de retorno de SQL Stored Procedures.
Assim, tendo uma conexão, podemos executar alguma solicitação dentro dessa conexão. Portanto, é lógico que inicialmente obtenhamos uma instância da expressão SQL do Connection. Você precisa começar criando uma tabela. Vamos descrever a solicitação de criação de tabela como uma variável String. Como fazer isso? Vamos usar algum tutorial como " sqltutorial.org ", " sqlbolt.com ", " postgresqltutorial.com ", " codecademy.com ". Vamos usar, por exemplo, um exemplo do curso de SQL em khanacademy.org . Vamos adicionar um método para executar uma expressão no banco de dados:
private int executeUpdate(String query) throws SQLException {
	Statement statement = connection.createStatement();
	// Для Insert, Update, Delete
	int result = statement.executeUpdate(query);
	return result;
}
Vamos adicionar um método para criar uma tabela de teste usando o 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);
}
Agora vamos testar isso:
@Test
public void shouldCreateCustomerTable() throws SQLException {
	createCustomerTable();
	connection.createStatement().execute("SELECT * FROM customers");
}
Agora vamos executar a requisição, e ainda com um 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);
}
O JDBC não oferece suporte a parâmetros nomeados para PreparedStatement, portanto, os próprios parâmetros são especificados por perguntas e, ao especificar o valor, indicamos o índice da pergunta (começando em 1, não em zero). No último teste recebemos true como uma indicação se há resultado. Mas como o resultado da consulta é representado na API JDBC? E é apresentado como um ResultSet.
JDBC ou onde tudo começa – 7

Conjunto de resultados

O conceito de ResultSet é descrito na especificação da API JDBC no capítulo "CAPÍTULO 15 Conjuntos de Resultados". Em primeiro lugar, diz que ResultSet fornece métodos para recuperar e manipular os resultados de consultas executadas. Ou seja, se o método execute retornou verdadeiro para nós, então podemos obter um ResultSet. Vamos mover a chamada do método createCustomerTable() para o método init, que está marcado como @Before. Agora vamos finalizar nosso teste shouldSelectData:
@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);
}
É importante notar aqui que a seguir está um método que move o chamado “cursor”. O cursor no ResultSet aponta para alguma linha. Assim, para ler uma linha, você precisa colocar o cursor sobre ela. Quando o cursor é movido, o método de movimentação do cursor retorna verdadeiro se o cursor for válido (correto, correto), ou seja, aponta para dados. Se retornar falso, então não há dados, ou seja, o cursor não está apontando para os dados. Se tentarmos obter dados com um cursor inválido, obteremos o erro: Nenhum dado está disponível. Também é interessante que através do ResultSet você pode atualizar ou até mesmo inserir linhas:
@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 linhas

Além do ResultSet, o JDBC introduz o conceito de RowSet. Você pode ler mais aqui: " Noções básicas de JDBC: usando objetos RowSet ". Existem diversas variações de uso. Por exemplo, o caso mais simples pode ser assim:
@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 você pode ver, RowSet é semelhante a uma simbiose de instrução (especificamos o comando por meio dela) e o comando executado. Através dele controlamos o cursor (chamando o próximo método) e obtemos dados dele. Não só esta abordagem é interessante, mas também possíveis implementações. Por exemplo, CachedRowSet. Está "desconectado" (ou seja, não utiliza uma conexão persistente com o banco de dados) e requer sincronização explícita com o banco de dados:
CachedRowSet jdbcRsCached = new CachedRowSetImpl();
jdbcRsCached.acceptChanges(connection);
Você pode ler mais no tutorial no site da Oracle: " Usando CachedRowSetObjects ".
JDBC ou onde tudo começa – 8

Metadados

Além das consultas, uma conexão com o banco de dados (ou seja, uma instância da classe Connection) fornece acesso aos metadados – dados sobre como nosso banco de dados está configurado e organizado. Mas primeiro, vamos mencionar alguns pontos-chave: A URL para conexão com nosso banco de dados: “jdbc:h2:mem:test”. test é o nome do nosso banco de dados. Para a API JDBC, este é um diretório. E o nome estará em maiúscula, ou seja, TEST. O esquema padrão para H2 é PUBLIC. Agora, vamos escrever um teste que mostre todas as tabelas do usuário. Por que personalizado? Porque os bancos de dados contêm não apenas tabelas de usuários (aquelas que nós mesmos criamos usando expressões de criação de tabela), mas também tabelas de sistema. Eles são necessários para armazenar informações do sistema sobre a estrutura do banco de dados. Cada banco de dados pode armazenar essas tabelas de sistema de maneira diferente. Por exemplo, em H2 eles são armazenados no esquema " INFORMATION_SCHEMA ". Curiosamente, o INFORMATION SCHEMA é uma abordagem comum, mas a Oracle seguiu um caminho diferente. Você pode ler mais aqui: " INFORMATION_SCHEMA e Oracle ". Vamos escrever um teste que receba metadados nas tabelas do usuário:
@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 ou onde tudo começa – 9

Conjunto de conexões

O pool de conexões na especificação JDBC possui uma seção chamada "Capítulo 11 Pool de conexões". Também fornece a principal justificativa para a necessidade de um pool de conexões. Cada Coonection é uma conexão física com o banco de dados. Sua criação e fechamento é um trabalho bastante “caro”. JDBC fornece apenas uma API de pool de conexões. Portanto, a escolha da implementação continua sendo nossa. Por exemplo, tais implementações incluem HikariCP . Conseqüentemente, precisaremos adicionar um pool à dependência do nosso projeto:
dependencies {
    implementation 'com.h2database:h2:1.4.197'
    implementation 'com.zaxxer:HikariCP:3.3.1'
    testImplementation 'junit:junit:4.12'
}
Agora precisamos usar esse pool de alguma forma. Para fazer isso, você precisa inicializar a fonte de dados, também conhecida 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;
}
E vamos escrever um teste para receber uma conexão do pool:
@Test
public void shouldGetConnectionFromDataSource() throws SQLException {
	DataSource datasource = getDatasource();
	try (Connection con = datasource.getConnection()) {
		assertTrue(con.isValid(1));
	}
}
JDBC ou onde tudo começa – 10

Transações

Uma das coisas mais interessantes sobre o JDBC são as transações. Na especificação JDBC, eles recebem o capítulo "CAPÍTULO 10 Transações". Antes de mais nada, vale entender o que é uma transação. Uma transação é um grupo de operações sequenciais combinadas logicamente sobre dados, processadas ou canceladas como um todo. Quando uma transação começa ao usar JDBC? Conforme afirma a especificação, isso é tratado diretamente pelo driver JDBC. Mas normalmente, uma nova transação começa quando a instrução SQL atual exige uma transação e a transação ainda não foi criada. Quando termina a transação? Isso é controlado pelo atributo auto-commit. Se a confirmação automática estiver habilitada, a transação será concluída após a instrução SQL ser "concluída". O que significa "concluído" depende do tipo de expressão SQL:
  • Linguagem de manipulação de dados, também conhecida como DML (Insert, Update, Delete)
    A transação é concluída assim que a ação é concluída
  • Selecione Instruções
    A transação é concluída quando o ResultSet é fechado ( ResultSet#close )
  • CallableStatement e expressões que retornam vários resultados
    Quando todos os ResultSets associados foram fechados e toda a saída foi recebida (incluindo o número de atualizações)
É exatamente assim que a API JDBC se comporta. Como sempre, vamos escrever um teste para isso:
@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);
}
É simples. Mas isso é verdade desde que tenhamos apenas uma transação. O que fazer quando existem vários deles? Eles precisam estar isolados um do outro. Portanto, vamos falar sobre os níveis de isolamento de transações e como o JDBC lida com eles.
JDBC ou onde tudo começa – 11

Níveis de isolamento

Vamos abrir a subseção "10.2 Níveis de isolamento de transação" da especificação JDBC. Aqui, antes de prosseguir, gostaria de lembrar algo como ACID. ACID descreve os requisitos para um sistema transacional.
  • Atomicidade:
    Nenhuma transação será parcialmente comprometida com o sistema. Todas as suas suboperações serão executadas ou nenhuma será executada.
  • Consistência:
    Cada transação bem-sucedida, por definição, registra apenas resultados válidos.
  • Isolamento:
    enquanto uma transação está em execução, as transações simultâneas não devem afetar seu resultado.
  • Durabilidade:
    Se uma transação for concluída com sucesso, as alterações feitas nela não serão desfeitas devido a qualquer falha.
Ao falar sobre níveis de isolamento de transações, estamos falando sobre o requisito de “Isolamento”. O isolamento é um requisito caro, portanto, em bancos de dados reais, existem modos que não isolam completamente uma transação (níveis de isolamento de leitura repetível e inferiores). A Wikipedia tem uma excelente explicação sobre os problemas que podem surgir ao trabalhar com transações. Vale a pena ler mais aqui: “ Problemas de acesso paralelo utilizando transações ”. Antes de escrevermos nosso teste, vamos alterar um pouco nosso Gradle Build Script: adicionar um bloco com propriedades, ou seja, com as configurações do nosso projeto:
ext {
    h2Version = '1.3.176' // 1.4.177
    hikariVersion = '3.3.1'
    junitVersion = '4.12'
}
A seguir, usamos isso nas versões:
dependencies {
    implementation "com.h2database:h2:${h2Version}"
    implementation "com.zaxxer:HikariCP:${hikariVersion}"
    testImplementation "junit:junit:${junitVersion}"
}
Você deve ter notado que a versão h2 ficou mais baixa. Veremos o porquê mais tarde. Então, como você aplica níveis de isolamento? Vejamos imediatamente um pequeno exemplo prático:
@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, este teste pode falhar em um fornecedor que não suporta TRANSACTION_READ_UNCOMMITTED (por exemplo, sqlite ou HSQL). E o nível de transação pode simplesmente não funcionar. Lembra que indicamos a versão do driver do banco de dados H2? Se aumentarmos para h2Version = '1.4.177' e superior, então READ UNCOMMITTED irá parar de funcionar, embora não tenhamos alterado o código. Isso prova mais uma vez que a escolha do fornecedor e da versão do driver não é apenas letras, na verdade determinará como suas solicitações serão executadas. Você pode ler sobre como corrigir esse comportamento na versão 1.4.177 e como ele não funciona em versões superiores aqui: “ Suporte ao nível de isolamento READ UNCOMMITTED no modo MVStore ”.
JDBC ou onde tudo começa – 12

Resultado final

Como podemos ver, JDBC é uma ferramenta poderosa nas mãos do Java para trabalhar com bancos de dados. Espero que esta breve revisão ajude a fornecer um ponto de partida ou a refrescar sua memória. Bem, para um lanche, alguns materiais adicionais: #Viacheslav
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION