# SQLite no React Native com Expo O SQLite no React Native funciona como um banco de dados local dentro do dispositivo, armazenando dados em um arquivo .db. Você pode executar queries SQL para criar tabelas, inserir, atualizar, excluir e recuperar dados. O Expo facilita esse processo através do pacote expo-sqlite, que fornece métodos simples para interagir com o banco de dados sem necessidade de configuração adicional. ## 🚀 **Instalando SQLite no Expo** ``` npx expo install expo-sqlite ``` ## 📌 **Inicialização do Banco de Dados** Para começar a trabalhar com o SQLite, precisamos criar ou abrir um banco de dados. Inicialmente crie uma pasta `database` para guardar o iniciliazador e as funções referentes ao banco de dados A função `InitializeDatabase` é responsável por preparar o ambiente do banco assim que a aplicação inicia. 📝 Código: A função recebe um objeto do tipo SQLiteDatabase e executa uma instrução SQL para criar a tabela users caso ela ainda não exista: ```typescript! // initializeDatabase.ts import { type SQLiteDatabase } from "expo-sqlite"; export async function InitializeDatabase(database: SQLiteDatabase) { try { await database.execAsync(` CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, email TEXT NOT NULL, password TEXT NOT NULL ); `); } catch (error) { console.log(error); } } ``` ## 📌 **Configuração do Provider no App.tsx** A partir da versão SDK que inclui o módulo SQLite, o Expo disponibiliza um provider para encapsular o contexto do banco de dados e torná-lo acessível em toda a árvore de componentes. No `App.tsx`, o provider é configurado da seguinte forma: ```typescript! // App.tsx import Routes from './src/routes'; import './global.css'; import { SQLiteProvider } from "expo-sqlite" import { InitializeDatabase } from './src/database/initializeDatabase'; export default function App() { return ( <SQLiteProvider databaseName='databaseEstudos.db' onInit={InitializeDatabase}> <Routes/> </SQLiteProvider> ); } ``` **SQLiteProvider**: Ao envolver os componentes com o `SQLiteProvider`, todas as telas e hooks dentro da árvore terão acesso ao contexto do banco de dados. **Propriedades Importantes**: * **`databaseName`**: Define o nome do arquivo do banco de dados local. * **`onInit`**: Recebe a função de inicialização (`InitializeDatabase`), que é chamada assim que o provider configura o banco, garantindo que a tabela `users` esteja pronta para uso. ## 📌 **Funções de operações no banco de dados** Para separar a lógica de acesso ao banco de dados e facilitar o reuso, crie um hook chamado `useUsersDatabase`. Esse hook utiliza o contexto provido pelo `SQLiteProvider` (através do `useSQLiteContext`) e expõe funções para criar, consultar e deletar usuários. 📝 **Definição do Tipo de Usuário** Primeiramente, define-se um tipo para padronizar a estrutura dos dados dos usuários: ```typescript! export type UserDatabase = { id: number; name: string; email: string; password: string; }; ``` 🚀 **Estrutura do Hook** No final, o hook retornará as funções que podem ser utilizadas em qualquer componente que precise interagir com a tabela users. Para utilizarmos as funções de operação, é necessário a instância do `useSQLiteContext` ```typescript! export function useUsersDatabase() { const database = useSQLiteContext(); // Função Create // Função Get // Função Delete return { createUsers, getUsers, deleteUser, }; } ``` ### ✍️ Criação (Create) A função createUsers prepara uma instrução SQL com placeholders para inserir um novo usuário, evitando problemas de injeção de SQL: ```typescript! async function createUsers(data: Omit<UserDatabase, "id">) { const statement = await database.prepareAsync( "INSERT INTO users (name, email, password) VALUES ($name, $email, $password);" ); try { const result = await statement.executeAsync({ $name: data.name, $email: data.email, $password: data.password, }); const insertedId = result.lastInsertRowId.toLocaleString(); return { insertedId }; } catch (error) { throw error; } finally { await statement.finalizeAsync(); } } ``` **🔹 Prepare e Execute**: Primeiro, o statement é preparado com prepareAsync e, em seguida, executado com executeAsync passando os valores dos parâmetros. **🔹 Finalização**: Após a execução, o statement é finalizado com finalizeAsync para liberar recursos. **🔹 Retorno**: Retorna o insertedId para que seja possível referenciar o novo registro inserido. ### 🔎 Consulta (Read) A função getUsers executa uma consulta simples para recuperar todos os registros da tabela users ```typescript! async function getUsers() { try { const query = "SELECT * FROM users"; const response = await database.getAllAsync<UserDatabase>(query); return response; } catch (error) { throw error; } } ``` **🔹 Consulta Assíncrona**: Usa `getAllAsync` para obter todos os registros e retorna um array de objetos do tipo `UserDatabase`. ### 🔎 Consulta por ID (Read) ```typescript! async function getUserById(id: number) { try { return await database.getFirstAsync<UserDatabase>("SELECT * FROM users WHERE id = $id", { $id: id }); } catch (error) { throw error; } } ``` ### 🔎 Consulta por atributo: Busca usuários por nome de forma inclusa, ou seja, pode ser escrito usuario ou USUARIO. ```typescript! async function getUsersByName(name: string) { try { const query = "SELECT * FROM users WHERE name LIKE ?" const response = await database.getAllAsync<UserDatabase>( query, `%${name}%` ) return response } catch (error) { throw error } } ``` ### 🔄 Atualização (Update) ```typescript! async function updateUser(id: number, data: Partial<UserDatabase>) { const statement = await database.prepareAsync( "UPDATE users SET name = $name, email = $email, password = $password WHERE id = $id" ); try { await statement.executeAsync({ $id: id, $name: data.name, $email: data.email, $password: data.password, }); } finally { await statement.finalizeAsync(); } } ``` ### 🗑️ Exclusão (Delete) A função deleteUser prepara um comando para deletar um usuário específico com base no seu ID: ```typescript! async function deleteUser(id: number) { const statement = await database.prepareAsync( "DELETE FROM users WHERE id = $id" ); try { await statement.executeAsync({ $id: id }); } catch (error) { throw error; } finally { await statement.finalizeAsync(); } } ``` ### Diferença entre Statement e Query no SQLite Ao trabalhar com SQLite (e bancos de dados em geral), podemos executar comandos SQL de duas maneiras principais: utilizando statements preparados (prepared statements) ou executando queries diretas. **1️⃣ Statement (Prepared Statement)** 🔹 Processo de Preparação e Execução: Com um prepared statement, você primeiro prepara a instrução SQL e, em seguida, a executa. Esse processo envolve chamar métodos como `prepareAsync`, seguido de `executeAsync` e, por fim, `finalizeAsync` para liberar os recursos. **✅ Vantagens**: * **Segurança**: Permite o uso de placeholders (por exemplo, `$name`, `$email`), o que ajuda a evitar injeção de SQL, já que os valores são tratados separadamente da instrução. * **Reutilização**: Ideal para executar a mesma instrução diversas vezes com parâmetros diferentes, economizando processamento por não precisar reanalisar a query a cada execução. * **Controle de Erros e Recursos**: Permite gerenciar de forma mais precisa o ciclo de vida do comando, garantindo a finalização correta com `finalizeAsync`. **2️⃣ Query Direta** **🔹 Execução Imediata**: Ao utilizar métodos como `execAsync` ou `getAllAsync`, você passa a query diretamente e ela é executada de forma imediata, sem a necessidade de preparar e depois finalizar explicitamente o comando. **✅ Vantagens**: **Simplicidade**: É ideal para operações simples ou consultas que são executadas apenas uma vez, onde não há necessidade de reutilização da instrução. **Menor Complexidade no Código**: Menos chamadas de métodos (preparar, executar, finalizar), o que torna o código mais direto em casos de operações únicas. ✔️ **Resumindo** - **Statement (Prepared Statement)**: É útil quando você precisa executar uma mesma instrução várias vezes com diferentes parâmetros, oferecendo maior segurança e controle sobre a execução. Ideal para operações de escrita (inserções, atualizações, deleções) que se beneficiam da reutilização e proteção contra injeção de SQL. - **Query Direta**: É mais simples e rápida para operações únicas, especialmente para consultas de leitura onde a complexidade adicional de preparar e finalizar um statement não é necessária. # Drizzle ORM no react native O Drizzle ORM é um ORM (Object-Relational Mapping) leve e otimizado para TypeScript e JavaScript. Ele é conhecido por sua abordagem tipada e segura, proporcionando melhor experiência com consultas SQL e maior segurança no código. No contexto do React Native com Expo, ele pode ser usado para gerenciar um banco de dados SQLite de maneira eficiente. ## 🚀 Instalar Dependências ``` npm i drizzle-orm expo-sqlite npm i -D drizzle-kit ``` ``` npm install babel-plugin-inline-import ``` ## 📌 Configurar Dependências Necessário configurar `babel.config.js`, `metro.config.js` e `drizzle.config.ts` ```typescript! //babel.config.js module.exports = function(api) { api.cache(true); return { presets: ['babel-preset-expo'], plugins: [["inline-import", { "extensions": [".sql"] }]] // <-- Adicionar esta linha }; }; ``` ```typescript! // metro.config.js const { getDefaultConfig } = require('expo/metro-config'); /** @type {import('expo/metro-config').MetroConfig} */ const config = getDefaultConfig(__dirname); config.resolver.sourceExts.push('sql'); // <--- Adicionar esta linha module.exports = config; ``` ```typescript! // drizzle.config.ts import type { Config } from 'drizzle-kit'; export default { schema: './src/database/schemas/schema.ts', // <-- caminho dos schemas out: './drizzle', dialect: 'sqlite', driver: 'expo', } satisfies Config; ``` ## Definindo o Schema do Banco de Dados - **Criar a pasta e o arquivo para o schema** Com o `drizzle.config.ts` configurado com o caminho do schema, é possível criar diferentes arquivos de schema ou um arquivo com todos os schemas dentro. ![image](https://hackmd.io/_uploads/r186_ZHjyl.png) No caso de criar diferentes arquivos de schema, é necessário definir no `drizzle.config.ts` ```typescript schema: './src/database/schemas/*' ``` - Schema de User ```typescript! import { sqliteTable, integer, text} from "drizzle-orm/sqlite-core"; export const user = sqliteTable("Users", { id: integer("id").primaryKey(), name: text("name").notNull(), email: text("email").notNull().unique(), password: text("password").notNull(), }); ``` ## 📌 Executar Migrações ``` npx drizzle-kit generate ``` ## 📌 Configurar a Conexão com SQLite - (Opcional) Crie um arquivo db.ts ```typescript! import { drizzle } from "drizzle-orm/expo-sqlite"; import { openDatabaseSync } from "expo-sqlite"; export const DATABASE_NAME = "databaseDrizzle.db"; export const expoDB = openDatabaseSync(DATABASE_NAME); export const db = drizzle(expoDB); ``` - Dentro de app.tsx ```typescript! import "react-native-gesture-handler"; import { View, Text, ActivityIndicator } from "react-native"; import Routes from "./src/routes"; import "./global.css"; import { useDrizzleStudio } from "expo-drizzle-studio-plugin"; import { DATABASE_NAME, db, expoDB } from "./src/database/db"; import { useMigrations } from "drizzle-orm/expo-sqlite/migrator"; import migrations from "./drizzle/migrations"; import { SQLiteProvider } from "expo-sqlite"; export default function App() { const { success, error } = useMigrations(db, migrations); useDrizzleStudio(expoDB) if (error) { <View className="flex-1 justify-center items-center"> <Text>{error.message}</Text> </View>; } if (!success) { return ( <View className="flex-1 justify-center items-center"> <ActivityIndicator size="large" /> </View> ); } return ( <SQLiteProvider databaseName={DATABASE_NAME}> <Routes /> </SQLiteProvider> ); } ``` - SQLiteProvide como provider para utilizar as funções do banco dentro dos componentes - `useMigrations(db, migrations)` Para verificar se o banco está sendo chamado corretamente ## Criar um service para as entidades Para organizar melhor o código e seguir boas práticas, podemos criar uma pasta `services` dentro de `src` e dentro dela um arquivo `userService.ts` para encapsular todas as funções CRUD relacionadas à entidade - **Criar a Pasta e o Arquivo userService.ts** ```css src/ ├── services/ │ ├── userService.ts <-- Criar esse arquivo ``` - **Implementar as Funções CRUD no userService.ts** ```typescript! import { Alert } from "react-native"; import { drizzle } from "drizzle-orm/expo-sqlite"; import { eq } from "drizzle-orm"; import { useSQLiteContext } from "expo-sqlite"; import * as UserSchema from "../database/schemas/userSchema"; export type UserDatabase = { id: number; name: string; email: string; password: string; }; export function useUserService() { const database = useSQLiteContext(); const db = drizzle(database, { schema: UserSchema }); async function getUsers() { try { return await db.query.user.findMany(); } catch (error) { console.error("Erro ao buscar usuários:", error); return []; } } async function createUser(name: string, email: string, password: string) { try { const response = await db .insert(UserSchema.user) .values({ name, email, password }); Alert.alert("Usuário cadastrado com ID: " + response.lastInsertRowId); return response; } catch (error) { console.error("Erro ao cadastrar usuário:", error); } } async function deleteUser(id: number) { try { await db.delete(UserSchema.user).where(eq(UserSchema.user.id, id)); Alert.alert("Usuário deletado com sucesso!"); } catch (error) { console.error("Erro ao deletar usuário:", error); } } return { getUsers, createUser, deleteUser }; } ``` - **Utilizar o userService.ts em algum Componente** ```typescript! import { Alert, FlatList, Text, TextInput, TouchableOpacity, View, } from "react-native"; import { useEffect, useState } from "react"; import { UserDatabase, useUserService } from "../../services/userService"; export default function DrizzleORM({ navigation }) { const [name, setName] = useState(""); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [users, setUsers] = useState<UserDatabase[]>([]); const { getUsers, createUser, deleteUser } = useUserService(); async function loadUsers() { const data = await getUsers(); setUsers(data); } async function handleCreateUser() { await createUser(name, email, password); setName(""); setEmail(""); setPassword(""); loadUsers(); // Atualiza a lista após criar um novo usuário } async function handleDeleteUser(id: number) { await deleteUser(id); loadUsers(); // Atualiza a lista após deletar um usuário } useEffect(() => { loadUsers(); }, []); return ( <View className="flex-1 justify-start py-5"> <Text className="text-2xl text-center mb-10"> Banco de Dados Drizzle ORM - Usuário </Text> <TextInput className="border w-4/5 flex mx-auto rounded-xl px-2 mb-5" placeholder="Nome" onChangeText={setName} value={name} /> <TextInput className="border w-4/5 flex mx-auto rounded-xl px-2 mb-5" placeholder="Email" onChangeText={setEmail} value={email} /> <TextInput className="border w-4/5 flex mx-auto rounded-xl px-2 mb-5" placeholder="Senha" onChangeText={setPassword} value={password} /> <TouchableOpacity className="flex w-1/2 bg-green-500 p-3 my-5 m-auto rounded-xl" onPress={handleCreateUser} > <Text className="text-center color-white">Cadastrar</Text> </TouchableOpacity> <View> <Text className="text-center text-xl">Usuários</Text> <FlatList data={users} keyExtractor={(item) => String(item.id)} renderItem={({ item }) => ( <View className="flex flex-row items-center justify-evenly w-4/5 mx-auto border p-3 my-2 rounded-xl"> <View className="flex flex-col"> <Text>ID: {item.id}</Text> <Text>Nome: {item.name}</Text> <Text>Email: {item.email}</Text> <Text>Senha: {item.password}</Text> </View> <TouchableOpacity onPress={() => handleDeleteUser(item.id)} > <Text style={{ fontSize: 20 }}>🗑️</Text> </TouchableOpacity> </View> )} /> </View> <TouchableOpacity className="flex w-1/2 bg-slate-700 p-3 my-5 m-auto rounded-xl" onPress={() => navigation.replace("Home")} > <Text className="text-center color-white">Voltar</Text> </TouchableOpacity> </View> ); } ``` ## PLugin Drizzle Studio Para visualizar o banco de dados sqlite do drizzle e fazer alterações nos dados https://www.npmjs.com/package/expo-drizzle-studio-plugin/v/0.0.1 - Instalar Dependência ``` npm i expo-drizzle-studio-plugin ``` - Configurar o drizzle studio no app.tsx ```typescript! import { useDrizzleStudio } from "expo-drizzle-studio-plugin"; export default function App() { const { success, error } = useMigrations(db, migrations); useDrizzleStudio(expoDB) // Restante do código } ``` No terminal, no menu de utilidades do expo, use `Shift + m` e selecione o `Open expo-drizzle-studio-plugin`