Objeto: criar um banco de dados numa máquina remota, escrever uma função de pesquisa nessa base de dados e uma página que exiba os resultados a partir da chamada dessa função
Principais termos técnicos: banco de dados, MySql, getServerSideProps, HTML, JavaScript
Requisitos para as instruções funcionarem: instalações atualizadas do node.js/npm
Requisitos para compreensão das instruções: noções básicas de programação, HTML e NextJS, bem como ter concluído textos anteriores. Alguma noção de banco de dados
Como ler essa receita: instruções, dentro dos passos da receita e que requerem ação sua no computador, estarão escritas em cor azul. Comentários sobre os passos estarão em fonte normal, cor preta. Comandos, código-fonte, termos técnicos ou configuração explícita, estarão com fonte diferenciada.
Observação: eventuais instruções de terminal serão comandos Linux. Adapte caso esteja usando outro sistema operacional
E se quiséssemos um sistema parecido com o das receitas anteriores, sendo que, em vez de acessar um site externo que oferece um serviço (no caso, dados de filmes), acessamos uma base de dados criada e mantida por nós mesmos? Vamos usar o getServerSideProps para receber uma requisição, chamar uma função de biblioteca que acessa um bando de dados MySql e construir uma página com base nesses dados. Vai dar um pouco mais de trabalho pois precisamos configurar banco de dados e preencher valores iniciais. Mas a gente consegue. Sugiro que leia (não precisa executar as instruções) a Receita3 para entender melhor essa receita corrente.
Acessar site freedb.tech, cadastrar-se e criar um banco de dados.
Pode ser em qualquer servidor, esse é o menos burocrático que achei.
Guarde as seguintes informações em algum arquivo de texto quando criar seu banco:
Remote Host (deve ser freedb.tech)
Database Name (vai ser algo de sua escolha)
Username (vai ser algo de sua escolha)
Password (vai ser algo de sua escolha)
Tome cuidado. O Username e Password não são o seu login e senha iniciais para entrar no site, são o login e senha do banco que você cria depois de entrar no site.
Nesse instante, você tem um servidor de banco de dados rodando numa máquina remota e capaz de estabelecer conexão com seu aplicativo web. Agora vamos instalar criar o aplicativo web e instalar os pacotes necessários, incluindo o pacote que tem funções e objetos para comunicar-se com um banco de dados MySql.
Criar um diretório e, dentro dele, instalar pacotes necessários
mkdir next-mysql
cd next-mysql
npm i next react react-dom serverless-mysql
Os dados de conexão com nosso banco de dados precisam, claro, constar em algum lugar de nosso aplicativo web.
E precisam estar num lugar seguro, bem longe de um navegador.
Vamos guardar essas informações num lugar seguro.
Criar arquivo .env.local na raiz do projeto CASO esse arquivo ainda não exista
Neste arquivo podemos escrever configuração que estarão visíveis do lado do servidor. Estre essas configurações estarão os dados e conexão com o banco de dados MySql
Abrir para edição o .env.local e acrescentar o seguinte conteúdo, trocando o ME_SUBSTITUA pelos dados que você coletou no Passo 1
HOST=freedb.tech
DATABASE=ME_SUBSTITUA
DBUSERNAME=ME_SUBSTITUA
DBPASSWORD=ME_SUBSTITUA
Precisamos de um arquivo que inicialize nosso banco de dados, crie uma tabela e preencha alguns dados iniciais, pois apenas faremos pesquisa nessa receita. Esse será um arquivo js que chamamos arquivo de migração (de dados). Ele não será executado pelo nosso aplicativo. Será executado por nós mesmos, no prompt de comando, antes de subirmos o aplicativo. Esse código precisará de um pacote a mais para funcionar.
Instalar o pacote dotenv para ajudar na migração
npm install dotenv
Criar um diretório scripts na raiz do projeto
Criar um arquivo migration-db.js dentro do diretório scripts e deixá-lo com o seguinte conteúdo:
const path = require('path')
const envPath = path.resolve(process.cwd(), '.env.local')
const fetch = require('cross-fetch')
require('dotenv').config({ path: envPath })
const mysql = require('serverless-mysql')
const db = mysql({
config: {
host: process.env.HOST,
database: process.env.DATABASE,
user: process.env.DBUSERNAME,
password: process.env.DBPASSWORD,
},
})
async function query(q) {
try {
const results = await db.query(q)
await db.end()
return results
} catch (e) {
throw Error(e.message)
}
}
async function drop(){
try {
await query(`
DROP TABLE Movies
`)
console.log('drop ran successfully')
} catch (e) {
console.error('could not run migration, double check your credentials.')
process.exit(1)
}
}
async function create(){
try {
await query(`
CREATE TABLE Movies(
imdbId VARCHAR(12) PRIMARY KEY,
title VARCHAR(50),
year VARCHAR(6),
poster VARCHAR(250)
);
`)
console.log('create ran successfully')
} catch (e) {
console.error('could not run create.')
console.log(e)
process.exit(1)
}
}
async function insert(){
try {
await query(`
INSERT INTO Movies(imdbId,title,year,poster) VALUES ('tt0033152','The Thief of Bagdad','1940','https://m.media-amazon.com/images/M/MV5BZWFhYjg4NTEtY2IzMS00YTc2LTg1NGUtMTEzNDBlZDIxZTk3XkEyXkFqcGdeQXVyMTMxMTY0OTQ@._V1_SX300.jpg');
`)
await query(
`
INSERT INTO Movies(imdbId,title,year,poster) VALUES ('tt0015400','The Thief of Bagdad','1924','https://m.media-amazon.com/images/M/MV5BNmM0MjdkMDQtMDMwMy00ZjE4LThjMDUtNjA4ZjkxYzM0MWRjXkEyXkFqcGdeQXVyMDI2NDg0NQ@@._V1_SX300.jpg');
`
)
await query(
`
INSERT INTO Movies(imdbId,title,year,poster) VALUES ('tt0041149','Bagdad','1949','https://m.media-amazon.com/images/M/MV5BZjAxYTJmMmItYzZhMS00OWMwLTkxMjEtZjVmNmM3ZGRlZTljXkEyXkFqcGdeQXVyNTk1MTk0MDI@._V1_SX300.jpg');
`
)
await query(
`
INSERT INTO Movies(imdbId,title,year,poster) VALUES ('tt1893256','Redemption','2013','https://m.media-amazon.com/images/M/MV5BMjI0ODc3NjI4NV5BMl5BanBnXkFtZTcwOTc3MzI1OQ@@._V1_SX300.jpg');
`
)
await query(
`
INSERT INTO Movies(imdbId,title,year,poster) VALUES ('tt1781896','The Scorpion King 3: Battle for Redemption','2012','https://m.media-amazon.com/images/M/MV5BMjA3MzU2ODIwOV5BMl5BanBnXkFtZTcwOTA1ODIyNw@@._V1_SX300.jpg');
`
)
await query(
`
INSERT INTO Movies(imdbId,title,year,poster) VALUES ('tt4439620','Redemption Day','2021','https://m.media-amazon.com/images/M/MV5BYjFjOTI3MjItZTAzNS00Yjk4LWEyNDAtZTA4ZDNiM2FiNjVjXkEyXkFqcGdeQXVyMTYxNzI2MTg@._V1_SX300.jpg');
`
)
await query(
`
INSERT INTO Movies(imdbId,title,year,poster) VALUES ('tt1479962','Red Dead Redemption','2010','https://m.media-amazon.com/images/M/MV5BZmYwNjk3OWMtODk2Yi00MjA3LTgzYzctODk1NmRhM2M4ZDVjXkEyXkFqcGdeQXVyNjgzMTIxNzE@._V1_SX300.jpg');
`
)
await query(
`
INSERT INTO Movies(imdbId,title,year,poster) VALUES ('tt6161168','Red Dead Redemption II','2018','https://m.media-amazon.com/images/M/MV5BMThiMGJkNDUtYjIxYy00ZTRhLWE5NmUtNzI4NTJlOGI4ZTMwXkEyXkFqcGdeQXVyNTk1ODMyNjA@._V1_SX300.jpg');
`
)
await query(
`
INSERT INTO Movies(imdbId,title,year,poster) VALUES ('tt0813980','24: Redemption','2008','https://m.media-amazon.com/images/M/MV5BZGQzNTU4M2MtNTliMC00YTliLTlmMmYtZDQ3NTQ4N2YwMDdmXkEyXkFqcGdeQXVyMjUzOTY1NTc@._V1_SX300.jpg');
`
)
await query(
`
INSERT INTO Movies(imdbId,title,year,poster) VALUES ('tt1156466','Undisputed 3: Redemption','2010','https://m.media-amazon.com/images/M/MV5BMTc0YzA4YjQtZGZkMi00ZmRjLWFmM2ItMDcxZTYzZGU3ZTI1XkEyXkFqcGdeQXVyNDQ2MTMzODA@._V1_SX300.jpg');
`
)
await query(
`
INSERT INTO Movies(imdbId,title,year,poster) VALUES ('tt1899353','The Raid: Redemption','2011','https://m.media-amazon.com/images/M/MV5BZGIxODNjM2YtZjA5Mi00MjA5LTk2YjItODE0OWI5NThjNTBmXkEyXkFqcGdeQXVyNzQ1ODk3MTQ@._V1_SX300.jpg');
`
)
await query(
`
INSERT INTO Movies(imdbId,title,year,poster) VALUES ('tt0044389','Babes in Bagdad','1952','https://m.media-amazon.com/images/M/MV5BYjBmNmRlNzAtZDNjYi00ZjQ0LTljYTktZGUxOGNhMjFhZWI1XkEyXkFqcGdeQXVyMTE2NzA0Ng@@._V1_SX300.jpg');
`
)
await query(
`
INSERT INTO Movies(imdbId,title,year,poster) VALUES ('tt0046497','The Veils of Bagdad','1953','https://m.media-amazon.com/images/M/MV5BODdhYzI2NGQtNWJhZS00ZTNjLWJkNDMtODVlZDU5NjJjNTMwXkEyXkFqcGdeQXVyMjU5OTg5NDc@._V1_SX300.jpg');
`
)
await query(
`
INSERT INTO Movies(imdbId,title,year,poster) VALUES ('tt0029833','A-Lad-In Bagdad','1938','https://m.media-amazon.com/images/M/MV5BYjM4MDBiMmQtZmNhYy00ZmFhLTkxMmItOTViYzdlYTk2YTIzXkEyXkFqcGdeQXVyMTU5NjQzMzU@._V1_SX300.jpg');
`
)
await query(
`
INSERT INTO Movies(imdbId,title,year,poster) VALUES ('tt0046322','Siren of Bagdad','1953','https://m.media-amazon.com/images/M/MV5BNGNkN2M5NjgtNmU2Mi00YjY0LWExNWItZTgxMjI5OWVkMzRkXkEyXkFqcGdeQXVyNjEwMTA0NTc@._V1_SX300.jpg');
`
)
await query(
`
INSERT INTO Movies(imdbId,title,year,poster) VALUES ('tt0098746','Bagdad Cafe','1990','https://m.media-amazon.com/images/M/MV5BZTg5NGQ5OTMtOGE2ZS00NWMzLTgwN2YtMTNjMTJjODI4NWJhXkEyXkFqcGdeQXVyMjA0MDQ0Mjc@._V1_SX300.jpg');
`
)
await query(
`
INSERT INTO Movies(imdbId,title,year,poster) VALUES ('tt0044388','Bowery to Bagdad','1955','https://m.media-amazon.com/images/M/MV5BZTMwMTUyZTktOTk4OC00ZGY2LWFmMzAtYjkxMWMxNjdmOTg4XkEyXkFqcGdeQXVyNDY3MzU2MDM@._V1_SX300.jpg');
`
)
await query(
`
INSERT INTO Movies(imdbId,title,year,poster) VALUES ('tt5592230','The Blacklist: Redemption','2017','https://m.media-amazon.com/images/M/MV5BODI2MjI4MzQ1OV5BMl5BanBnXkFtZTgwOTk2MDAyMTI@._V1_SX300.jpg');
`
)
console.log('insert ran successfully')
} catch (e) {
console.error('could not run insert, check commands.')
console.log(e)
process.exit(1)
}
}
async function migrate() {
try {
await drop()
await create()
await insert()
console.log('migration ran successfully')
} catch (e) {
console.error('could not run migration, double check your credentials.')
process.exit(1)
}
}
migrate().then(() => process.exit())
Eu sei, eu sei, esse código não é a coisa mais bonita do mundo.
Mas você não vai ter que andar de mãos dadas com ele na praça nem botar foto com ele no Instagram.
E ele é bastante útil.
Note que esse código apaga uma tabela Movies, se ela existir, e em seguida cria uma tabela Movies com as colunas imdbId, title, year e poster (todas strings). Por fim, o script insere uma ruma de filmes na tabela Movies. As informações foram retiradas do site que acessamos em receitas anteriores. Percebam que no trecho...
const db = mysql({
config: {
host: process.env.HOST,
database: process.env.DATABASE,
user: process.env.DBUSERNAME,
password: process.env.DBPASSWORD,
},
})
... estamos acessando aqueles valores que escrevemos no arquivo .env.local
Vamos registrar esse script em nosso projeto.
Abra o arquivo package.json e crie um atributo scripts (não apague nada):
"scripts": {
"dev": "next dev",
"migrate": "node script/migrate-db.js"
}
Se você está fazendo tudo do zero, o arquivo, como um todo, deve ficar mais ou menos assim:
{
"dependencies": {
"dotenv": "^8.2.0",
"next": "^10.0.7",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"serverless-mysql": "^1.5.4"
},
"scripts": {
"dev": "next dev",
"test": "echo \"Error: no test specified\" && exit 1",
"migrate": "node script/migrate-db.js"
}
}
Agora vamos rodar aquele script "feinho":
npm run migrate
Se tudo der certo, você há de ver, no prompt, as mensagens:
drop ran successfully
create ran successfully
insert ran successfully
migration ran successfully
Você pode dar uma conferida lá no site freedb.tech que vai ter uma tabela Movies e alguns dados no seu banco. Foi o script que fez isso tudo.
- Ah, professor, e eu poderia ter feito tudo aquilo que foi feito no migrate.js pelo site?
Poderia. Mas a emoção não seria a mesma...
Agora já temos nosso banco devidamente criado, com informações cadastradas e doido para ser consultado.
Vamos implementar aplicativo web para isso, mas ainda temos algumas coisas para fazer antes de escrever as páginas.
Precisamos de uma biblioteca com um objeto que "converse" com o banco de dados, algo parecido com o que o migrate-db.js faz, sendo que estará acessível para as páginas de nosso aplicativo web.
Criar um diretório lib na raiz do projeto.
Criar um arquivo db.js dentro de lib
Deixá-lo com o seguinte conteúdo:
import mysql from "serverless-mysql"
export const db = mysql({
config: {
host : process.env.HOST,
database : process.env.DATABASE,
user : process.env.DBUSERNAME,
password : process.env.DBPASSWORD
}
})
export async function allMovies(){
try {
const sql = `SELECT * FROM Movies`
const result = await db.query(sql,[])
await db.end()
return JSON.parse(JSON.stringify(result))
} catch (e) {
throw Error(e.message)
}
}
export async function searchMovies(titleStringSearch){
try {
const sql = `SELECT * FROM Movies WHERE title LIKE ?`
const result = await db.query(sql,[`%${titleStringSearch}%`])
await db.end()
return JSON.parse(JSON.stringify(result))
} catch (e) {
throw Error(e.message)
}
}
export async function createMovie({imdbId,title,year,poster}){
const sql = `INSERT INTO Movies(imdbId,title,year,poster) VALUES (?,?,?,?)`
const values = [imdbId,title,year,poster]
try {
const result = await db.query(sql, values)
await db.end()
return JSON.parse(JSON.stringify(result))
} catch (e) {
throw Error(e.message)
}
}
export async function updateMovie({imdbId,title,year,poster}){
const sql = `UPDATE Movies SET title=?, year=?, poster=? WHERE imdbId=?`
const values = [title,year,poster,imdbId]
try {
const result = await db.query(sql, values)
await db.end()
return JSON.parse(JSON.stringify(result))
} catch (e) {
throw Error(e.message)
}
}
export async function deleteMovie({imdbId}){
const sql = `DELETE FROM Movies WHERE imdbId=?`
const values = [imdbId]
try {
const result = await db.query(sql, values)
await db.end()
return JSON.parse(JSON.stringify(result))
} catch (e) {
throw Error(e.message)
}
}
Lembra do arquivo .env.local? Pois é, estamos acessando novamente os valores configurados lá para conectar-se com o banco (process.env.HOST, process.env.DATABASE etc.). Escrevemos, também, uma função que faz a pesquisa no banco de dados, allMovies(). Se quisermos escrever uma página que exiba todos os livros registrados, chamamos essa função e montamos o HTML da página com os resultados que a função retornar.
Note que já estou dando uma ruma de função pronta. Para apagar, atualizar, criar, pesquisar por título... Nesta receita usaremos apenas uma dessas funções. Você terá oportunidade de testar as demais nos exercícios (elas não devem dar erro).
Agora vamos fazer algo extremamente parecido com o aplicativo da Receita3.
A diferença é que, lá, chamamos uma pesquisa num servidor externo, algo assim:
export async function getServerSideProps(context){
const res = await fetch(`http://www.omdbapi.com/?apikey=ME_SUBSTITUA&s=bagdad`)
const data = await res.json()
return {
props: {
data
}
}
}
Aqui vamos chamar uma função que nós mesmos escrevemos e que acessa o banco de dados.
Escreva o arquivo index.js (na pasta pages) o seguinte conteúdo:
import {allMovies} from "../lib/db"
export default function Movies({movies}){
return (
<div>
<h2>Todos os filmes</h2>
{
movies.map( (movie) => (
<div key={movie.imdbId}>
{movie.title} -- ({movie.year})
<br/>
<img src={movie.poster} width="300"/>
<br/><br/>
</div>
))
}
</div>
)
}
export async function getServerSideProps(context){
const movies = await allMovies()
return {
props:{
movies
}
}
}
A função Movies() desta receita é praticamente a mesma da Receita3 (acrescentei as imagens dos posters dos filmes) e a função getServerSideProps() desta receita é até mais simples que a getServerSideProps() da Receita3. A diferença ficou por conta do objeto que acessa o banco de dados, que tivemos que implementar. É ele que faz o "trabalho sujo". Na Receita3, a gente meio que terceirizava esse trabalho sujo. Fazíamos uma requisição a um site externo e ele já dava pra gente o milho da pipoca.
https://www.npmjs.com/package/serverless-mysql
https://vercel.com/guides/deploying-next-and-mysql-with-vercel
1. Sabem aquele objeto context que o getServerSideProps recebe como parâmetro? Ele pode servir para, entre outras coisas, dar acesso a dados oriendos de um formulário. Tente fazer uma nova página, com um formulário, que em vez de pesquisar os filmes, recebe um string desse formulário e faz uma pesquisa por título. O trabalho sujo (a pesquisa no banco) está implementado já, no arquivo db.js.
2. Tente fazer uma nova página, com um formulário, que em vez de pesquisar os filmes, recebe daados de novos filmes e insere esses dados no banco de dados.