bd
This commit is contained in:
parent
69822c180a
commit
02531f0861
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
@ -29,4 +29,5 @@ pre:has(code) {
|
|||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
margin-top: var(--paragraph-spacing);
|
margin-top: var(--paragraph-spacing);
|
||||||
margin-bottom: var(--paragraph-spacing);
|
margin-bottom: var(--paragraph-spacing);
|
||||||
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
@ -33,11 +33,7 @@
|
|||||||
<li><a href="/estrutura_de_arquivos">Estrutura de arquivos</a></li>
|
<li><a href="/estrutura_de_arquivos">Estrutura de arquivos</a></li>
|
||||||
<li><a href="/endpoints_1">Criando e executando endpoints</a></li>
|
<li><a href="/endpoints_1">Criando e executando endpoints</a></li>
|
||||||
<li><a href="/endpoints_2">Definindo os endpoints da Biblioteca</a></li>
|
<li><a href="/endpoints_2">Definindo os endpoints da Biblioteca</a></li>
|
||||||
<li>Conectando com o banco de dados</li>
|
<li><a href="/bd">Conectando com o banco de dados</a></li>
|
||||||
<li>Mantendo livros</li>
|
|
||||||
<li>Emprestando livros</li>
|
|
||||||
<li>Sobre o protocolo HTTP</li>
|
|
||||||
<li>A arquitetura REST</li>
|
|
||||||
</ol>
|
</ol>
|
||||||
</ol>
|
</ol>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@ -1,3 +1,7 @@
|
|||||||
|
<script>
|
||||||
|
import VscUsingMySqlConnector from "../../assets/img/49_vsc_using_mysqlconnector.png";
|
||||||
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>Conectando com um banco de dados</title>
|
<title>Conectando com um banco de dados</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
@ -10,42 +14,769 @@
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Os endpoints que escrevemos no capítulo anterior não fazem nada além de exibir os dados que você envia na requisição.
|
Os endpoints que escrevemos no capítulo anterior não fazem nada além de devolver os dados exatamente como você os envia na requisição.
|
||||||
Para que possamos fazer com que eles <em>persistam</em> os dados enviados, vamos utilizar um banco de dados.
|
Para que possamos fazer com que estes dados sejam <em>persistidos</em> (ou seja, registrados em algum lugar onde possamos consultá-los futuramente), vamos utilizar um banco de dados.
|
||||||
Neste capítulo, vamos criar a comunicação entre o nosso projeto e esse banco de dados.
|
Neste capítulo, vamos criar a comunicação entre o nosso projeto e um gerenciador de banco de dados <a href="https://mariadb.org/" target="_blank">MariaDB</a>,
|
||||||
|
um fork open-source do MySQL criado e mantido pelos seus desenvolvedores originais.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Vamos utilizar um banco de dados <code>MySQL</code>, acessível através das credenciais:
|
Vamos utilizar um banco de dados previamente criado para persistir e consultar livros.
|
||||||
|
Para acessá-lo a partir do nosso projeto, precisamos de uma biblioteca. Como o MariaDB é um fork do
|
||||||
|
MySQL, os dois são normalmente intercambiáveis. Bibliotecas desenvolvidas para o MySQL quase sempre irão funcionar com o MariaDB, com exceção de algumas
|
||||||
|
funcionalidades específicas. A <a href="https://mariadb.com/kb/en/other-net-connectors/" target="_blank">documentação do MariaDB</a> recomenda a utilização da
|
||||||
|
biblioteca <a href="https://mysqlconnector.net/" target="_blank">MySqlConnector</a>, portanto, iremos instalá-la no nosso projeto.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<ul>
|
|
||||||
<li>Usuário: <code>sistemasdistribuidos.aluno</code></li>
|
|
||||||
<li>Senha: <code>eW03avS7M8kOUL1A9bZWW2RTIfzEI1Di</code></li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<h2>MySqlConnector</h2>
|
<h2>MySqlConnector</h2>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Para gerenciar conexões e realizar consultas, vamos utilizar a biblioteca <a href="https://mysqlconnector.net/" target="_blank">MySqlConnector</a>. Abra
|
Abra o terminal no seu projeto, e execute o comando:
|
||||||
o terminal no seu projeto, e execute o comando:
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<pre><code>{`cd Biblioteca
|
<pre><code>{`cd Biblioteca
|
||||||
dotnet add package MySqlConnector`}</code></pre>
|
dotnet add package MySqlConnector`}</code></pre>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Note que o comando precisa ser executado <em>no diretório onde existe o arquivo <code>Biblioteca.csproj</code></em>,
|
Note que o comando precisa ser executado <em>no diretório onde existe o arquivo <code>Biblioteca.csproj</code></em>. Para confirmar se a biblioteca foi adicionada corretamente,
|
||||||
por isso o comando <code>cd Biblioteca</code>.
|
adicione a seguinte linha no topo do seu <code>Program.cs</code>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<pre><code>{`using MySqlConnector;`}</code></pre>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Se o Visual Studio Code não mostra nenhum erro, então a instalação foi bem sucedida.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<img src="{VscUsingMySqlConnector}" alt="">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>Modelo de dados</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
O banco de dados que vamos utilizar possui uma tabela com o nome <code>Livro</code>. Esta tabela possui os seguintes campos:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<pre><code>{`Isbn VARCHAR(255) PRIMARY KEY NOT NULL,
|
||||||
|
Titulo VARCHAR(512) NOT NULL,
|
||||||
|
Autor TEXT NOT NULL,
|
||||||
|
Genero TEXT NOT NULL,
|
||||||
|
Descricao TEXT NOT NULL,
|
||||||
|
Foto TEXT NOT NULL,
|
||||||
|
Keywords TEXT NOT NULL,
|
||||||
|
Ativo BOOLEAN NOT NULL DEFAULT 0,
|
||||||
|
CriadoEm DATETIME NOT NULL,
|
||||||
|
AtualizadoEm DATETIME NOT NULL,`}</code></pre>
|
||||||
|
|
||||||
|
<h2>Classe de modelo e repositório</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Vamos criar duas novas classes no projeto: uma classe <code>Livro</code> que irá espelhar a tabela no banco de dados, e uma classe <code>LivroRepository</code> que irá possuir
|
||||||
|
métodos para a consulta e manipulação dos registros nessa tabela. Mas antes disso, vamos limpar o <code>Program.cs</code>, removendo partes do código que escrevemos anteriormente apenas para testar, e que
|
||||||
|
não vamos precisar mais, deixando-o pronto para utilizarmos as novas classes que iremos criar.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Apague todo o conteúdo do seu <code>Program.cs</code> e deixe-o exatamente assim:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<pre><code>{`var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
// Obtém uma lista com os livros registrados.
|
||||||
|
app.MapGet("/livros", () =>
|
||||||
|
{
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cria um novo livro.
|
||||||
|
app.MapPost("/livros", () =>
|
||||||
|
{
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
// Edita um livro.
|
||||||
|
app.MapPut("/livros/{isbn}", () =>
|
||||||
|
{
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
// Obtém os dados de um livro individual.
|
||||||
|
app.MapGet("/livros/{isbn}", () =>
|
||||||
|
{
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove um livro.
|
||||||
|
app.MapDelete("/livros/{isbn}", () =>
|
||||||
|
{
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
app.Run();`}</code></pre>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Tendo feito isso, crie uma pasta <code>Biblioteca/Models</code> e, dentro dela, crie um arquivo <code>Livro.cs</code>.
|
||||||
|
Dentro deste arquivo, vamos criar uma classe <code>Livro</code> com campos que refletem os campos na tabela do banco de dados.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<pre><code>{`namespace Biblioteca.Models;
|
||||||
|
|
||||||
|
public class Livro
|
||||||
|
{
|
||||||
|
public string? Isbn { get; set; }
|
||||||
|
public string? Titulo { get; set; }
|
||||||
|
public string? Autor { get; set; }
|
||||||
|
public string? Genero { get; set; }
|
||||||
|
public string? Descricao { get; set; }
|
||||||
|
public string? Foto { get; set; }
|
||||||
|
public string? Keywords { get; set; }
|
||||||
|
public bool Ativo { get; set; }
|
||||||
|
public DateTime CriadoEm { get; set; }
|
||||||
|
public DateTime AtualizadoEm { get; set; }
|
||||||
|
}`}</code></pre>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Agora, crie uma pasta <code>Biblioteca/Repositories</code> e, dentro dela, crie um arquivo <code>LivroRepository.cs</code>,
|
||||||
|
onde existirá a classe <code>LivroRepository</code>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<pre><code>{`namespace Biblioteca.Repositories;
|
||||||
|
|
||||||
|
public class LivroRepository
|
||||||
|
{
|
||||||
|
|
||||||
|
}`}</code></pre>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Nesta classe, vamos precisar de uma constante que vai conter uma <em>string de conexão</em>, que servirá para indicar para o MySqlConnector o endereço e as credenciais do banco de dados que vamos utilizar.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<pre><code>{`namespace Biblioteca.Repositories;
|
||||||
|
|
||||||
|
public class LivroRepository
|
||||||
|
{
|
||||||
|
private const string ConnString = "Server=gbrl.dev;Port=5306;User ID=sistemasdistribuidos.aluno;Password=eW03avS7M8kOUL1A9bZWW2RTIfzEI1Di;Database=sistemasdistribuidos";
|
||||||
|
}`}</code></pre>
|
||||||
|
|
||||||
|
<h2>Listagem de livros</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Vamos escrever nesta classe métodos para as operações de listagem, consulta, inclusão, edição e remoção de livros na base de dados. Vamos começar pela
|
||||||
|
operação de listagem.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<pre><code>{`public async Task<IEnumerable<Livro>> Obter(int pagina)
|
||||||
|
{
|
||||||
|
|
||||||
|
}`}</code></pre>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Este método é público (<code>public</code>), assíncrono (<code>async Task</code>), retorna um <code>IEnumerable<Livro></code>, e recebe
|
||||||
|
como parâmetro um valor inteiro que representa a página da consulta
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Um método <em>público</em> é um método que pode ser executado por qualquer outro método no projeto. O nosso método precisa ser público pois ele vai ser chamado
|
||||||
|
nos endpoints que estão no <code>Program.cs</code>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
O valor de retorno será um <code>IEnumerable<Livro></code>. Um <code>IEnumerable</code> é uma interface do dotnet que representa
|
||||||
|
qualquer sequência <em>iterável</em> de objetos. Por exemplo, um objeto <code>List<T></code> ou um vetor <code>T[]</code> são iteráveis,
|
||||||
|
portanto podem ser retornados neste método.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Um método <em>assíncrono</em> é um método que pode ser executado <em>assíncronamente</em>, de forma independente do fluxo de execução do método que o chamou.
|
||||||
|
Nós não vamos utilizar nenhuma forma de paralelismo, mas precisamos que nosso método seja assíncrono mesmo assim.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
O parâmetro <code>int pagina</code> recebido indica a página da consulta. Você provavelmente já usou algum site onde interagiu com uma listagem de um conteúdo que era feita
|
||||||
|
através de páginas, a ideia aqui é a mesma. Imagine que temos três milhões de livros na base de dados, nós não queremos que nosso método leia todos eles.
|
||||||
|
Ao invés disso, vamos retornar os livros de forma paginada, com N livros por página.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Agora, vamos implementar o método, que ficará assim:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<pre><code>{`public async Task<IEnumerable<Livro>> Obter(int pagina)
|
||||||
|
{
|
||||||
|
using var conn = new MySqlConnection(ConnString);
|
||||||
|
using var cmd = conn.CreateCommand();
|
||||||
|
|
||||||
|
await conn.OpenAsync();
|
||||||
|
|
||||||
|
var take = 30;
|
||||||
|
var offset = take * Math.Max(pagina-1, 0);
|
||||||
|
var lista = new List<Livro>();
|
||||||
|
|
||||||
|
cmd.CommandText = "SELECT Isbn, Titulo, Autor, Genero, Descricao, Foto, Keywords, Ativo, CriadoEm, AtualizadoEm FROM Livro ORDER BY CriadoEm LIMIT @offset,@take";
|
||||||
|
cmd.Parameters.AddWithValue("offset", offset);
|
||||||
|
cmd.Parameters.AddWithValue("take", take);
|
||||||
|
|
||||||
|
using var reader = await cmd.ExecuteReaderAsync();
|
||||||
|
|
||||||
|
while (await reader.ReadAsync())
|
||||||
|
{
|
||||||
|
lista.Add(new()
|
||||||
|
{
|
||||||
|
Isbn = reader.GetString(0),
|
||||||
|
Titulo = reader.GetString(1),
|
||||||
|
Autor = reader.GetString(2),
|
||||||
|
Genero = reader.GetString(3),
|
||||||
|
Descricao = reader.GetString(4),
|
||||||
|
Foto = reader.GetString(5),
|
||||||
|
Keywords = reader.GetString(6),
|
||||||
|
Ativo = reader.GetBoolean(7),
|
||||||
|
CriadoEm = reader.GetDateTime(8),
|
||||||
|
AtualizadoEm = reader.GetDateTime(9),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return lista;
|
||||||
|
}`}</code></pre>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Vamos analisar cada trecho do método.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<pre><code>{`using var conn = new MySqlConnection(ConnString);
|
||||||
|
using var cmd = conn.CreateCommand();
|
||||||
|
|
||||||
|
await conn.OpenAsync();`}</code></pre>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
As variáveis <code>conn</code> e <code>cmd</code> representam, respectivamente, a conexão com o banco de dados, e o comando que iremos executar.
|
||||||
|
A declaração de ambas é feita com <code>using</code> pois estes dois objetos alocam recursos que <em>devem</em> ser liberados ao fim da execução do método. O
|
||||||
|
<code>using</code> é uma construção da linguagem que garante que isso aconteça sempre.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Com <code>await conn.OpenAsync()</code> fazemos com que a conexão com o banco de dados seja aberta.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Note que <code>conn.OpenAsync()</code> também é um método assíncrono (pois ele retorna <code>Task</code>). Por isso, devemos prefixar a chamada deste método com
|
||||||
|
um <code>await</code>. Isto é uma indicação de que queremos <em>aguardar</em> a conclusão de <code>conn.OpenAsync()</code> antes de prosseguirmos com a execução do nosso método.
|
||||||
|
E para utilizarmos um <code>await</code>, o nosso método deve ser também, obrigatoriamente, assíncrono. Por isso o declaramos como <code>async Task</code>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
O <a href="https://learn.microsoft.com/en-us/dotnet/csharp/asynchronous-programming/" target="_blank">modelo de programação assíncrona</a> é uma característica que
|
||||||
|
é praticamente ubíqua no dotnet, mas não precisamos nos aprofundar nela por enquanto. Basta saber que devemos utilizar <code>await</code> em métodos assíncronos, e
|
||||||
|
que se o utilizarmos, o nosso método deve ser também assíncrono, sendo declarado com <code>async Task</code>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<pre><code>{`var take = 30;
|
||||||
|
var offset = take * Math.Max(pagina-1, 0);
|
||||||
|
var lista = new List<Livro>();`}</code></pre>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Em seguida criamos três variáveis. <code>take</code> é o número de livros que queremos obter por página.
|
||||||
|
<code>offset</code> é um número que indica quantos registros pular para obter a página desejada. Por exemplo, se
|
||||||
|
desejamos a primeira página, devemos obter os <math>n</math> primeiros livros. Já se quisermos a segunda página,
|
||||||
|
vamos pular <math>n</math> livros e depois obter os <math>n</math> próximos.
|
||||||
|
Generalizando, para obter a página <math>p</math> (considerando que as páginas começam do <math>1</math>), com <math>n</math> elementos, precisamos pular <math>n(p-1)</math> livros.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Como <code>int pagina</code> é um valor inteiro que pode assumir valores negativos, fazemos <code>Math.Max(pagina-1, 0)</code> como uma garantia para que <code>offset</code> nunca tenha um valor menor do que zero.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
A variável <code>lista</code> é uma lista de objetos <code>Livro</code> que irá guardar todos os resultados da consulta, e depois será retornada.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<pre><code>{`cmd.CommandText = "SELECT Isbn, Titulo, Autor, Genero, Descricao, Foto, Keywords, Ativo, CriadoEm, AtualizadoEm FROM Livro ORDER BY CriadoEm LIMIT @offset,@take";
|
||||||
|
cmd.Parameters.AddWithValue("offset", offset);
|
||||||
|
cmd.Parameters.AddWithValue("take", take);`}</code></pre>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
A primeira linha adiciona a consulta que queremos executar à variável <code>cmd</code>. As duas linhas seguintes adicionam <code>take</code> e <code>offset</code>
|
||||||
|
como parâmetros da consulta.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<pre><code>{`using var reader = await cmd.ExecuteReaderAsync();`}</code></pre>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Enfim, executamos a consulta através de <code>cmd.ExecuteReaderAsync()</code>. Este método é assíncrono, portanto deve ser prefixado por <code>await</code>, e além disso também aloca
|
||||||
|
recursos que devem ser liberados com <code>using</code>. O retorno desta função é um objeto onde podemos ler os resultados da consulta.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<pre><code>{`while (await reader.ReadAsync())
|
||||||
|
{
|
||||||
|
lista.Add(new()
|
||||||
|
{
|
||||||
|
Isbn = reader.GetString(0),
|
||||||
|
Titulo = reader.GetString(1),
|
||||||
|
Autor = reader.GetString(2),
|
||||||
|
Genero = reader.GetString(3),
|
||||||
|
Descricao = reader.GetString(4),
|
||||||
|
Foto = reader.GetString(5),
|
||||||
|
Keywords = reader.GetString(6),
|
||||||
|
Ativo = reader.GetBoolean(7),
|
||||||
|
CriadoEm = reader.GetDateTime(8),
|
||||||
|
AtualizadoEm = reader.GetDateTime(9),
|
||||||
|
});
|
||||||
|
}`}</code></pre>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Este laço é repetido enquanto <code>await reader.ReadAsync()</code> retornar <code>true</code>, o que significa que existe um resultado da consulta que pode ser lido.
|
||||||
|
Quando isso acontece, podemos ler as colunas do resultado através dos métodos de <code>reader</code>, como <code>reader.GetString(0)</code>. O número passado como
|
||||||
|
parâmetro para estas funções indica a posição da coluna lida. Por exemplo, na consulta que realizamos (<code>SELECT Isbn, Titulo, ...</code>), a coluna <code>Isbn</code> é a primeira,
|
||||||
|
portanto, para ler este valor, devemos executar <code>reader.GetString(0)</code>. <code>Titulo</code> é a segunda, então o seu valor é obtido através de <code>reader.GetString(1)</code>, e assim
|
||||||
|
sucessivamente.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Realizamos a leitura de todas as colunas do resultado, e montamos um objeto do tipo <code>Livro</code>, o qual adicionamos à lista <code>lista</code>, que é o valor de retorno do método de listagem.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<pre><code>{`return lista;`}</code></pre>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Para testarmos este método, vamos chamá-lo no endpoint de listagem em <code>Program.cs</code>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<pre><code>{`// Obtém uma lista com os livros registrados.
|
||||||
|
app.MapGet("/livros", async () =>
|
||||||
|
{
|
||||||
|
var repo = new LivroRepository();
|
||||||
|
var res = await repo.Obter(pagina: 1);
|
||||||
|
|
||||||
|
return res;
|
||||||
|
});`}</code></pre>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Como <code>LivroRepository.Obter(int)</code> é assíncrono, o endpoint também deve ser. Fazemos isso adicionando <code>async</code> na declaração da sua função.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<pre><code>{`app.MapGet("/livros", async () => ...`}</code></pre>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Agora execute este endpoint. Se tudo deu certo, você verá alguns livros cadastrados no retorno.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>Obtendo um livro pelo ISBN</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
O ISBN é a chave primária da tabela. Vamos implementar um método que consulta um único livro a partir deste dado.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<pre><code>{`public async Task<Livro> Obter(string isbn)
|
||||||
|
{
|
||||||
|
using var conn = new MySqlConnection(ConnString);
|
||||||
|
using var cmd = conn.CreateCommand();
|
||||||
|
|
||||||
|
await conn.OpenAsync();
|
||||||
|
|
||||||
|
cmd.CommandText = "SELECT Isbn, Titulo, Autor, Genero, Descricao, Foto, Keywords, Ativo, CriadoEm, AtualizadoEm FROM Livro WHERE Isbn=@isbn";
|
||||||
|
cmd.Parameters.AddWithValue("isbn", isbn);
|
||||||
|
|
||||||
|
using var reader = await cmd.ExecuteReaderAsync();
|
||||||
|
|
||||||
|
var existe = await reader.ReadAsync();
|
||||||
|
|
||||||
|
if (!existe)
|
||||||
|
{
|
||||||
|
throw new Exception($"Livro com ISBN {isbn} não encontrado");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
Isbn = reader.GetString(0),
|
||||||
|
Titulo = reader.GetString(1),
|
||||||
|
Autor = reader.GetString(2),
|
||||||
|
Genero = reader.GetString(3),
|
||||||
|
Descricao = reader.GetString(4),
|
||||||
|
Foto = reader.GetString(5),
|
||||||
|
Keywords = reader.GetString(6),
|
||||||
|
Ativo = reader.GetBoolean(7),
|
||||||
|
CriadoEm = reader.GetDateTime(8),
|
||||||
|
AtualizadoEm = reader.GetDateTime(9),
|
||||||
|
};
|
||||||
|
}`}</code></pre>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
As três primeiras linhas deste método, assim como no de listagem, abre uma conexão com o banco de dados.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<pre><code>{`cmd.CommandText = "SELECT Isbn, Titulo, Autor, Genero, Descricao, Foto, Keywords, Ativo, CriadoEm, AtualizadoEm FROM Livro WHERE Isbn=@isbn";
|
||||||
|
cmd.Parameters.AddWithValue("isbn", isbn);`}</code></pre>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
A consulta é montada na variável <code>cmd</code>, o parâmetro <code>isbn</code> é adicionado ao comando.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<pre><code>{`using var reader = await cmd.ExecuteReaderAsync();
|
||||||
|
|
||||||
|
var existe = await reader.ReadAsync();
|
||||||
|
|
||||||
|
if (!existe)
|
||||||
|
{
|
||||||
|
throw new Exception($"Livro com ISBN {isbn} não encontrado");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
Isbn = reader.GetString(0),
|
||||||
|
Titulo = reader.GetString(1),
|
||||||
|
Autor = reader.GetString(2),
|
||||||
|
Genero = reader.GetString(3),
|
||||||
|
Descricao = reader.GetString(4),
|
||||||
|
Foto = reader.GetString(5),
|
||||||
|
Keywords = reader.GetString(6),
|
||||||
|
Ativo = reader.GetBoolean(7),
|
||||||
|
CriadoEm = reader.GetDateTime(8),
|
||||||
|
AtualizadoEm = reader.GetDateTime(9),
|
||||||
|
};`}</code></pre>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Assim como na listagem, <code>await cmd.ExecuteReaderAsync()</code> é chamado para ler os resultados da consulta.
|
||||||
|
Como esperamos que só haja um registro com aquele ISBN, executamos <code>await reader.ReadAsync()</code> apenas uma vez.
|
||||||
|
Se este método retorna <code>false</code>, é por que nenhum registro foi encontrado com aquele ISBN. Neste caso, lançamos um erro.
|
||||||
|
Do contrário, utilizamos os métodos de leitura do registro lido para montar um objeto <code>Livro</code> que será retornado.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Vamos chamar este novo método ao endpoint de consulta no <code>Program.cs</code>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<pre><code>{`// Obtém os dados de um livro individual.
|
||||||
|
app.MapGet("/livros/{isbn}", async (string isbn) =>
|
||||||
|
{
|
||||||
|
var repo = new LivroRepository();
|
||||||
|
var res = await repo.Obter(isbn);
|
||||||
|
|
||||||
|
return res;
|
||||||
|
});`}</code></pre>
|
||||||
|
|
||||||
|
<h2>Criando livros</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Vamos implementar agora o método de criação de livros:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<pre><code>{`public async Task<Livro> Criar(Livro dados)
|
||||||
|
{
|
||||||
|
|
||||||
|
}`}</code></pre>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Este método recebe um objeto <code>Livro</code> como parâmetro, contendo os dados do novo livro que deve ser inserido no banco de dados. Vamos implementá-lo.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<pre><code>{`public async Task<Livro> Criar(Livro dados)
|
||||||
|
{
|
||||||
|
using var conn = new MySqlConnection(ConnString);
|
||||||
|
using var cmd = conn.CreateCommand();
|
||||||
|
|
||||||
|
await conn.OpenAsync();
|
||||||
|
|
||||||
|
var livro = new Livro
|
||||||
|
{
|
||||||
|
Isbn = dados.Isbn?.Trim() ?? "",
|
||||||
|
Titulo = dados.Titulo?.Trim() ?? "",
|
||||||
|
Autor = dados.Autor?.Trim() ?? "",
|
||||||
|
Genero = dados.Genero?.Trim() ?? "",
|
||||||
|
Descricao = dados.Descricao?.Trim() ?? "",
|
||||||
|
Foto = dados.Foto?.Trim() ?? "",
|
||||||
|
Keywords = dados.Keywords?.Trim() ?? "",
|
||||||
|
Ativo = true,
|
||||||
|
CriadoEm = DateTime.Now,
|
||||||
|
AtualizadoEm = default,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (livro.Isbn == "")
|
||||||
|
{
|
||||||
|
throw new Exception("O ISBN do livro é obrigatório.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (livro.Titulo == "")
|
||||||
|
{
|
||||||
|
throw new Exception("O título do livro é obrigatório.");
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.CommandText =
|
||||||
|
@"
|
||||||
|
INSERT INTO Livro
|
||||||
|
(Isbn, Titulo, Autor, Genero, Descricao, Foto, Keywords, Ativo, CriadoEm, AtualizadoEm)
|
||||||
|
VALUES
|
||||||
|
(@isbn, @titulo, @autor, @genero, @descricao, @foto, @keywords, @ativo, @criadoem, @atualizadoem)
|
||||||
|
";
|
||||||
|
|
||||||
|
cmd.Parameters.AddWithValue("isbn", livro.Isbn);
|
||||||
|
cmd.Parameters.AddWithValue("titulo", livro.Titulo);
|
||||||
|
cmd.Parameters.AddWithValue("autor", livro.Autor);
|
||||||
|
cmd.Parameters.AddWithValue("genero", livro.Genero);
|
||||||
|
cmd.Parameters.AddWithValue("descricao", livro.Descricao);
|
||||||
|
cmd.Parameters.AddWithValue("foto", livro.Foto);
|
||||||
|
cmd.Parameters.AddWithValue("keywords", livro.Keywords);
|
||||||
|
cmd.Parameters.AddWithValue("ativo", livro.Ativo);
|
||||||
|
cmd.Parameters.AddWithValue("criadoem", livro.CriadoEm);
|
||||||
|
cmd.Parameters.AddWithValue("atualizadoem", livro.AtualizadoEm);
|
||||||
|
|
||||||
|
await cmd.ExecuteNonQueryAsync();
|
||||||
|
|
||||||
|
return livro;
|
||||||
|
}`}</code></pre>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
O método se inicia com a abertura da conexão com o banco de dados.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<pre><code>{`var livro = new Livro
|
||||||
|
{
|
||||||
|
Isbn = dados.Isbn?.Trim() ?? "",
|
||||||
|
Titulo = dados.Titulo?.Trim() ?? "",
|
||||||
|
Autor = dados.Autor?.Trim() ?? "",
|
||||||
|
Genero = dados.Genero?.Trim() ?? "",
|
||||||
|
Descricao = dados.Descricao?.Trim() ?? "",
|
||||||
|
Foto = dados.Foto?.Trim() ?? "",
|
||||||
|
Keywords = dados.Keywords?.Trim() ?? "",
|
||||||
|
Ativo = true,
|
||||||
|
CriadoEm = DateTime.Now,
|
||||||
|
AtualizadoEm = default,
|
||||||
|
};`}</code></pre>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Criamos uma variável que será uma <em>cópia</em> do objeto recebido no parâmetro. Fazemos isso para tratar os dados do livro, formatando-os, e assegurando
|
||||||
|
que os seus campos assumam os valores corretos. Este objeto é contém os dados que serão enviados para o banco de dados.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
O símbolo <code>?</code> é o operador de <em>propagação de nulos</em>. Em uma cadeia de expressões de acesso de membros em um objeto, um propagador de nulo faz com que, se um membro for nulo, o resultado de toda a expressão seja nula.
|
||||||
|
Por exemplo, <code>dados.Descricao</code> é um valor do tipo <code>string</code>, que pode assumir um valor nulo. Na expressão <code>dados.Descricao.Trim()</code>, se <code>dados.Descricao</code> for nulo, um
|
||||||
|
<code>NullReferenceException</code> será lançado, pois não é possível executar um método ou acessar uma propriedade em um objeto nulo. Já na expressão <code>dados.Descricao?.Trim()</code>, se <code>dados.Descricao</code>
|
||||||
|
for nulo, como há o operador de propagação de nulos, <code>.Trim()</code> não será executado, e o resultado da expressão será <code>null</code>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
De forma semelhante, o <code>??</code> age sobre dois operandos. Na expressão <code>var x = A ?? B</code>, a variável <code>x</code> irá assumir o valor de <code>A</code> apenas se ela não for nula. Do contrário,
|
||||||
|
<code>x</code> assumirá o valor de <code>B</code>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Portanto, na expressão <code>dados.Descricao?.Trim() ?? ""</code>, se <code>dados.Descricao</code> for nulo, então a expressão ficará <code>null ?? ""</code>, que terá como resultado a string vazia <code>""</code>. Ou seja,
|
||||||
|
Se <code>dados.Descricao</code> for nulo, <code>livro.Descricao</code> será uma string vazia, e nunca assumirá <code>null</code>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Fazemos isso nos campos do objeto <code>livro</code> para garantir que nenhum deles seja nulo, já que no banco de dados, todos os campos da tabela são <code>NOT NULL</code>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<pre><code>{`if (livro.Isbn == "")
|
||||||
|
{
|
||||||
|
throw new Exception("O ISBN do livro é obrigatório.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (livro.Titulo == "")
|
||||||
|
{
|
||||||
|
throw new Exception("O título do livro é obrigatório.");
|
||||||
|
}`}</code></pre>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Fazemos algumas validações básicas, para assegurar que os dados obrigatórios do registro tenham sido preenchidos.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<pre><code>{`cmd.CommandText =
|
||||||
|
@"
|
||||||
|
INSERT INTO Livro
|
||||||
|
(Isbn, Titulo, Autor, Genero, Descricao, Foto, Keywords, Ativo, CriadoEm, AtualizadoEm)
|
||||||
|
VALUES
|
||||||
|
(@isbn, @titulo, @autor, @genero, @descricao, @foto, @keywords, @ativo, @criadoem, @atualizadoem)
|
||||||
|
";
|
||||||
|
|
||||||
|
cmd.Parameters.AddWithValue("isbn", livro.Isbn);
|
||||||
|
cmd.Parameters.AddWithValue("titulo", livro.Titulo);
|
||||||
|
cmd.Parameters.AddWithValue("autor", livro.Autor);
|
||||||
|
cmd.Parameters.AddWithValue("genero", livro.Genero);
|
||||||
|
cmd.Parameters.AddWithValue("descricao", livro.Descricao);
|
||||||
|
cmd.Parameters.AddWithValue("foto", livro.Foto);
|
||||||
|
cmd.Parameters.AddWithValue("keywords", livro.Keywords);
|
||||||
|
cmd.Parameters.AddWithValue("ativo", livro.Ativo);
|
||||||
|
cmd.Parameters.AddWithValue("criadoem", livro.CriadoEm);
|
||||||
|
cmd.Parameters.AddWithValue("atualizadoem", livro.AtualizadoEm);`}</code></pre>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Assim como no método de listagem e da consulta por ISBN, montamos o SQL com um INSERT, e depois adicionamos os parâmetros do comando, com os
|
||||||
|
dados do objeto <code>livro</code>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<pre><code>{`await cmd.ExecuteNonQueryAsync();
|
||||||
|
|
||||||
|
return livro;`}</code></pre>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Em seguida, executamos o insert através de <code>await cmd.ExecuteNonQueryAsync()</code>. Como a operação é de escrita, não há resultado a ser lido.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Adicionando-o ao endpoint de criação, temos:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<pre><code>{`// Cria um novo livro.
|
||||||
|
app.MapPost("/livros", async (Livro livro) =>
|
||||||
|
{
|
||||||
|
var repo = new LivroRepository();
|
||||||
|
var res = await repo.Criar(livro);
|
||||||
|
|
||||||
|
return res;
|
||||||
|
});`}</code></pre>
|
||||||
|
|
||||||
|
<h2>Editando um livro</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
O método de edição é similar ao de criação.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<pre><code>{`public async Task<Livro> Editar(string isbn, Livro dados)
|
||||||
|
{
|
||||||
|
using var conn = new MySqlConnection(ConnString);
|
||||||
|
using var cmd = conn.CreateCommand();
|
||||||
|
|
||||||
|
await conn.OpenAsync();
|
||||||
|
|
||||||
|
var livro = await Obter(isbn);
|
||||||
|
|
||||||
|
livro.Titulo = dados.Titulo?.Trim() ?? "";
|
||||||
|
livro.Autor = dados.Autor?.Trim() ?? "";
|
||||||
|
livro.Genero = dados.Genero?.Trim() ?? "";
|
||||||
|
livro.Descricao = dados.Descricao?.Trim() ?? "";
|
||||||
|
livro.Foto = dados.Foto?.Trim() ?? "";
|
||||||
|
livro.Keywords = dados.Keywords?.Trim() ?? "";
|
||||||
|
livro.AtualizadoEm = DateTime.Now;
|
||||||
|
|
||||||
|
cmd.CommandText =
|
||||||
|
@"
|
||||||
|
UPDATE Livro SET
|
||||||
|
Titulo=@titulo, Autor=@autor, Genero=@genero, Descricao=@descricao, Foto=@foto, Keywords=@keywords, AtualizadoEm=@atualizadoem
|
||||||
|
WHERE Isbn=@isbn
|
||||||
|
";
|
||||||
|
|
||||||
|
cmd.Parameters.AddWithValue("isbn", isbn);
|
||||||
|
cmd.Parameters.AddWithValue("titulo", livro.Titulo);
|
||||||
|
cmd.Parameters.AddWithValue("autor", livro.Autor);
|
||||||
|
cmd.Parameters.AddWithValue("genero", livro.Genero);
|
||||||
|
cmd.Parameters.AddWithValue("descricao", livro.Descricao);
|
||||||
|
cmd.Parameters.AddWithValue("foto", livro.Foto);
|
||||||
|
cmd.Parameters.AddWithValue("keywords", livro.Keywords);
|
||||||
|
cmd.Parameters.AddWithValue("atualizadoem", livro.AtualizadoEm);
|
||||||
|
|
||||||
|
await cmd.ExecuteNonQueryAsync();
|
||||||
|
|
||||||
|
return livro;
|
||||||
|
}`}</code></pre>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
O método recebe dois parâmetros, um isbn que identifica o livro que será editado, e um objeto com os novos dados do livro.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<pre><code>{`var livro = await Obter(isbn);
|
||||||
|
|
||||||
|
livro.Titulo = dados.Titulo?.Trim() ?? "";
|
||||||
|
livro.Autor = dados.Autor?.Trim() ?? "";
|
||||||
|
livro.Genero = dados.Genero?.Trim() ?? "";
|
||||||
|
livro.Descricao = dados.Descricao?.Trim() ?? "";
|
||||||
|
livro.Foto = dados.Foto?.Trim() ?? "";
|
||||||
|
livro.Keywords = dados.Keywords?.Trim() ?? "";
|
||||||
|
livro.AtualizadoEm = DateTime.Now;`}</code></pre>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Chamamos a função <code>Obter(string)</code> que criamos para obter do banco de dados o livro que será editado. Em seguida, atribuímos a este objeto
|
||||||
|
os valores do objeto <code>dados</code>, formatando-os devidamente.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<pre><code>{`cmd.CommandText =
|
||||||
|
@"
|
||||||
|
UPDATE Livro SET
|
||||||
|
Titulo=@titulo, Autor=@autor, Genero=@genero, Descricao=@descricao, Foto=@foto, Keywords=@keywords, AtualizadoEm=@atualizadoem
|
||||||
|
WHERE Isbn=@isbn
|
||||||
|
";
|
||||||
|
|
||||||
|
cmd.Parameters.AddWithValue("isbn", isbn);
|
||||||
|
cmd.Parameters.AddWithValue("titulo", livro.Titulo);
|
||||||
|
cmd.Parameters.AddWithValue("autor", livro.Autor);
|
||||||
|
cmd.Parameters.AddWithValue("genero", livro.Genero);
|
||||||
|
cmd.Parameters.AddWithValue("descricao", livro.Descricao);
|
||||||
|
cmd.Parameters.AddWithValue("foto", livro.Foto);
|
||||||
|
cmd.Parameters.AddWithValue("keywords", livro.Keywords);
|
||||||
|
cmd.Parameters.AddWithValue("atualizadoem", livro.AtualizadoEm);
|
||||||
|
|
||||||
|
await cmd.ExecuteNonQueryAsync();
|
||||||
|
|
||||||
|
return livro;`}</code></pre>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Em seguida, montamos o comando um UPDATE, atribuímos os parâmetros, executamos a consulta e retornamos o livro que foi editado.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Vamos agora chamar este método no seu endpoint.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<pre><code>{`// Edita um livro.
|
||||||
|
app.MapPut("/livros/{isbn}", async (string isbn, Livro livro) =>
|
||||||
|
{
|
||||||
|
var repo = new LivroRepository();
|
||||||
|
var res = await repo.Editar(isbn, livro);
|
||||||
|
|
||||||
|
return res;
|
||||||
|
});`}</code></pre>
|
||||||
|
|
||||||
|
<h2>Removendo um livro</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Nós não iremos remover livros do banco de dados, ao invés disso, a operação de remoção irá
|
||||||
|
<em>editar</em> apenas o campo <code>Ativo</code> de um livro, alterando-o para <code>false</code>.
|
||||||
|
Portanto, o método de remover será basicamente uma edição.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<pre><code>{`public async Task Desativar(string isbn)
|
||||||
|
{
|
||||||
|
using var conn = new MySqlConnection(ConnString);
|
||||||
|
using var cmd = conn.CreateCommand();
|
||||||
|
|
||||||
|
await conn.OpenAsync();
|
||||||
|
|
||||||
|
cmd.CommandText = "UPDATE Livro SET Ativo=false WHERE Isbn=@isbn";
|
||||||
|
cmd.Parameters.AddWithValue("isbn", isbn);
|
||||||
|
|
||||||
|
await cmd.ExecuteNonQueryAsync();
|
||||||
|
}`}</code></pre>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Vamos adicioná-lo agora ao seu endpoint.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<pre><code>{`// Remove um livro.
|
||||||
|
app.MapDelete("/livros/{isbn}", async (string isbn) =>
|
||||||
|
{
|
||||||
|
var repo = new LivroRepository();
|
||||||
|
|
||||||
|
await repo.Desativar(isbn);
|
||||||
|
});`}</code></pre>
|
||||||
|
|
||||||
|
<h2>Finalizando</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Implementamos todas as operações básicas de manipulação dos registros, e os adicionamos aos endpoints da nossa API. Mais ainda precisamos ajustar algumas coisas.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
<li>Limpar Program.cs</li>
|
<li>
|
||||||
<li>criar arquivo Models/Livro.cs</li>
|
O endpoint de listagem está sempre obtendo a primeira página da consulta. Tente fazer seu endpoint receber um parâmetro de url
|
||||||
<li>criar arquivo Repositories/LivroRepository.cs</li>
|
(como <code>/livros?pagina=1</code>), para que seja possível obter as próximas páginas além da primeira.
|
||||||
<li>criar método de inserção</li>
|
</li>
|
||||||
<li>testar método de inserção</li>
|
<li>
|
||||||
<li>criar métodos de consulta</li>
|
Observe que, na listagem, os livros que foram removidos (ou seja, que estão com <code>Ativo=false</code>) ainda aparecem. Não queremos que isso aconteça, queremos
|
||||||
<li>testar métodos de consulta</li>
|
que os livros inativos não apareçam. Como você poderia fazer isso?
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
@ -24,7 +24,6 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li><em>Visualizar</em> os livros da biblioteca disponíveis no sistema.</li>
|
<li><em>Visualizar</em> os livros da biblioteca disponíveis no sistema.</li>
|
||||||
<li><em>Cadastrar</em>, <em>editar</em> e <em>remover</em> livros no sistema.</li>
|
<li><em>Cadastrar</em>, <em>editar</em> e <em>remover</em> livros no sistema.</li>
|
||||||
<li>Registrar quando um livro foi <em>emprestado</em> para um aluno.</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
|
|||||||
@ -19,7 +19,7 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li><em>Visualizar</em> os livros da biblioteca disponíveis no sistema.</li>
|
<li><em>Visualizar</em> os livros da biblioteca disponíveis no sistema.</li>
|
||||||
<li><em>Cadastrar</em>, <em>editar</em> e <em>remover</em> livros no sistema.</li>
|
<li><em>Cadastrar</em>, <em>editar</em> e <em>remover</em> livros no sistema.</li>
|
||||||
<li>Registrar quando um livro foi <em>emprestado</em> para um aluno.</li>
|
<!-- <li>Registrar quando um livro foi <em>emprestado</em> para um aluno.</li> -->
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user