Skip to content

Baze de date

Construiești un backend care are nevoie să stocheze ceva? Fiecare aplicație reală o face până la urmă - todos, utilizatori, scoruri. Baze de date îți oferă o mică bază de date administrată PostgreSQL găzduită pe DojoCode: o creezi cu un singur clic, o explorezi și o interoghezi într-un editor de date integrat și te conectezi la ea din proiectele tale exact așa cum ai face-o în producție - printr-o variabilă de mediu.

Postgres real, zero configurare

Fiecare bază de date este o bază de date PostgreSQL reală, cu propriile credențiale generate și un rol izolat - niciun alt utilizator nu se poate conecta la ea. Nu trebuie să instalezi nimic și te poți conecta și de pe propria mașină folosind orice client Postgres.

Crearea unei baze de date

Mergi la My Databases și dă clic pe New database. Alege un nume de afișare (numele real al bazei de date Postgres și credențialele sunt generate pentru tine) - crearea este instantanee.

Fiecare bază de date își afișează statusul, utilizarea spațiului de stocare raportată la cotă și acțiunile pentru deschiderea editorului de date sau ștergerea acesteia.

Ștergerea este permanentă

Ștergerea unei baze de date o elimină din cluster împreună cu toate datele sale, iar orice proiect care o folosește va pierde conexiunea. Această acțiune nu poate fi anulată.

Editorul de date

Deschide o bază de date pentru a ajunge în editorul său de date - un spațiu de lucru pe toată lățimea ecranului, cu două taburi:

  • Query - un editor SQL (cu evidențiere de sintaxă și Ctrl/⌘ + Enter pentru rulare) care se execută pe baza ta de date. Sunt acceptate scripturi cu instrucțiuni multiple; rezultatele, numărul de rânduri și timpii de execuție sunt afișați mai jos.
  • Tables - o vizualizare în timp real a schemei tale: fiecare tabel cu coloanele și tipurile sale, plus o previzualizare la un singur clic (Preview) care rulează o interogare SELECT * … LIMIT 20 pentru tine.

Dacă dai clic pe un tabel în tabul Tables, o interogare SELECT gata pregătită este introdusă în editor, astfel încât navigarea prin datele existente nu necesită scriere:

Interogările se execută sub rolul izolat al propriei baze de date, având o limită de timp pentru instrucțiuni (statement timeout) și o limită pentru numărul de rezultate - interogările care rulează prea mult sau scapă de sub control sunt oprite automat.

Un script cu instrucțiuni multiple rulează ca o singură tranzacție: dacă vreo instrucțiune eșuează, întregul script este anulat (rolled back) și nimic nu rămâne aplicat pe jumătate. Instrucțiunile pe care Postgres refuză să le execute în interiorul unei tranzacții (VACUUM, CREATE INDEX CONCURRENTLY) funcționează în continuare atunci când le rulezi separat.

Detalii de conectare

Panoul Connection arată tot ce are nevoie un client Postgres: gazdă, port, bază de date, utilizator, parolă și un URL postgres:// gata de lipit - fiecare cu copiere printr-un singur clic. Parola și URL-ul sunt mascate până când le dezvălui și sunt preluate doar atunci când le soliciți.

  • Doar tu poți vedea parola. Există într-un singur loc - acest panou, în propria ta bază de date. Nu este afișată niciodată altor utilizatori sau asistentului AI.
  • Schimbă parola oricând. Rotate password generează o parolă nouă și deconectează fiecare client care o folosește pe cea veche. Proiectele la care este atașată baza de date vor prelua automat noua parolă la următoarea lor rulare, iar orice site publicat care o folosește este actualizat pe loc - nu este necesară republicarea.
  • Conectează-te de oriunde. Șirul de conexiune funcționează și de pe propriul laptop - psql, TablePlus, DBeaver sau orice driver Postgres.

Utilizarea unei baze de date într-un proiect

Șabloanele de proiect server-side au un tab Database lângă cel de Secrets. Atașează una dintre bazele tale de date acolo și mediul de execuție va injecta conexiunea ca variabile de mediu:

VariabilăValoare
DATABASE_URLȘirul de conexiune complet postgres://user:password@host:port/db
DATABASE_HOST / DATABASE_PORTGazdă și port
DATABASE_NAME / DATABASE_USER / DATABASE_PASSWORDPărți individuale

Variabilele sunt injectate prin aceeași conductă ca și secretele, așa că mecanismul de citire este cel pe care șablonul tău îl folosește deja pentru secrete - process.env.NAME în șabloanele Node, os.environ["NAME"] în Python și așa mai departe (vezi tabelul complet per șablon). Un secret explicit numit DATABASE_URL suprascrie valoarea bazei de date atașate.

Doar șabloane server-side

Tabul Database apare în NodeJS, NodeTS, NestJS, Fastify, Hono, Python, PHP, Java, C, C++, Ruby, Rust, C# și Go. Șabloanele de frontend, full-stack, mobile și SQL-playground rulează în întregime în previzualizarea din browser, care nu are socket TCP pentru a accesa Postgres - deci nu au tabul Database. Pentru a pune o bază de date în spatele unei aplicații de browser, construiește API-ul în unul dintre șabloanele de server de mai sus și apelează-l din frontend.

Șabloane JavaScript de backend

NodeJS, NodeTS, Fastify, Hono și NestJS rulează prin intermediul butonului Run, deci se conectează cu adevărat. Adaugă mai întâi pachetul pg în panoul Dependencies. Toate exemplele presupun că baza de date atașată are tabelul todos din secțiunea dedicată editorului de date.

Previzualizarea din editor și API tester ajung de asemenea la ea

Previzualizarea din browser nu are socketuri TCP, așa că DojoCode răspunde la require("pg") acolo cu un înlocuitor integrat: codul tău funcționează neschimbat și fiecare interogare este retransmisă prin DojoCode către propria bază de date atașată. Acesta acoperă API-ul de zi cu zi (Pool, Client, query cu substituenți $n, callback-uri sau promisiuni); fiecare instrucțiune rulează în propria tranzacție, așa că un flux cu instrucțiuni multiple BEGIN/COMMIT necesită utilizarea butonului Run sau a aplicației publicate, care folosesc driverul real.

js
const { Client } = require("pg");

async function main() {
  if (!process.env.DATABASE_URL) {
    console.log("No database attached - attach one in the Database panel.");
    return;
  }

  const client = new Client({ connectionString: process.env.DATABASE_URL });
  await client.connect();
  const { rows } = await client.query("SELECT * FROM todos ORDER BY id");
  console.log(rows);
  await client.end();
}

main().catch((error) => console.error("Database error:", error.message));
js
const Fastify = require("fastify");
const { Pool } = require("pg");

const app = Fastify();
const pool = process.env.DATABASE_URL
  ? new Pool({ connectionString: process.env.DATABASE_URL })
  : null;

app.get("/todos", async () => {
  if (!pool) return { error: "No database attached." };
  const { rows } = await pool.query("SELECT * FROM todos ORDER BY id");
  return rows;
});

app.listen({ port: 3000, host: "0.0.0.0" });
js
const { Hono } = require("hono");
const { Pool } = require("pg");

const app = new Hono();
const pool = process.env.DATABASE_URL
  ? new Pool({ connectionString: process.env.DATABASE_URL })
  : null;

app.get("/todos", async (context) => {
  if (!pool) return context.json({ error: "No database attached." });
  const { rows } = await pool.query("SELECT * FROM todos ORDER BY id");
  return context.json(rows);
});

module.exports = app;
ts
// todos.service.ts
import { Injectable } from "@nestjs/common";
import { Pool } from "pg";

@Injectable()
export class TodosService {
  private readonly pool = process.env.DATABASE_URL
    ? new Pool({ connectionString: process.env.DATABASE_URL })
    : null;

  async findAll() {
    if (!this.pool) return { error: "No database attached." };
    const { rows } = await this.pool.query("SELECT * FROM todos ORDER BY id");
    return rows;
  }
}

Python

Rulează și prin intermediul butonului Run. Adaugă psycopg2-binary în panoul Dependencies:

python
import os
import psycopg2

url = os.environ.get("DATABASE_URL")
if not url:
    print("No database attached - attach one in the Database panel.")
else:
    connection = psycopg2.connect(url)
    with connection.cursor() as cursor:
        cursor.execute("SELECT * FROM todos ORDER BY id")
        for row in cursor.fetchall():
            print(row)
    connection.close()

Și previzualizarea din browser o poate accesa

Proiectele Python pot, de asemenea, să activeze web preview și API tester, care rulează codul tău în browser prin intermediul WebAssembly. Acel runtime nu are un socket TCP, așa că DojoCode oferă un înlocuitor integrat pentru psycopg2 acolo: codul tău import psycopg2 funcționează neschimbat, iar fiecare interogare este retransmisă prin DojoCode către propria bază de date atașată. Terminalul previzualizării ar putea menționa că psycopg2-binary nu are un build pentru browser - acest lucru este de așteptat; dependența este pentru Run și aplicația publicată, iar previzualizarea folosește înlocuitorul de fiecare dată când o bază de date este atașată.

Retransmiterea acoperă tiparele uzuale de interogare (connect, cursor, execute cu substituenți %s, fetches, RealDictCursor); fiecare instrucțiune rulează ca o tranzacție proprie, așa că commit și rollback sunt acceptate ca operațiuni fără efect (no-ops). Butonul Run și proiectul publicat se conectează direct folosind driverul real în schimb.

Limbaje compilate și alte limbaje

Ruby, Go, Rust, PHP, Java, C#, C și C++ rulează, de asemenea, prin intermediul butonului Run, astfel încât variabilele de conexiune sunt prezente în mediul de rulare. Fiecare are nevoie mai întâi ca driverul său Postgres să fie adăugat în panoul Dependencies - numele driverului este menționat deasupra fiecărui exemplu de mai jos.

Majoritatea driverelor folosesc direct DATABASE_URL. JDBC pentru Java și Npgsql pentru C# folosesc în schimb propriile formate de șiruri de conexiune, așa că acestea două construiesc șirul din variabilele separate DATABASE_HOST / DATABASE_PORT / DATABASE_NAME / DATABASE_USER / DATABASE_PASSWORD.

ruby
require 'pg'

url = ENV['DATABASE_URL'].to_s
if url.empty?
  puts 'No database attached - attach one in the Database panel.'
else
  conn = PG.connect(url)
  conn.exec('SELECT id, title FROM todos ORDER BY id') do |rows|
    rows.each { |row| puts "##{row['id']} - #{row['title']}" }
  end
  conn.close
end
go
package main

import (
	"database/sql"
	"fmt"
	"os"

	_ "github.com/lib/pq"
)

func main() {
	url := os.Getenv("DATABASE_URL")
	if url == "" {
		fmt.Println("No database attached - attach one in the Database panel.")
		return
	}

	db, err := sql.Open("postgres", url)
	if err != nil {
		fmt.Println("open error:", err)
		return
	}
	defer db.Close()

	rows, err := db.Query("SELECT id, title FROM todos ORDER BY id")
	if err != nil {
		fmt.Println("query error:", err)
		return
	}
	defer rows.Close()

	for rows.Next() {
		var id int
		var title string
		rows.Scan(&id, &title)
		fmt.Printf("#%d - %s\n", id, title)
	}
}
rust
use native_tls::TlsConnector;
use postgres::Client;
use postgres_native_tls::MakeTlsConnector;
use std::env;

fn main() {
    let url = env::var("DATABASE_URL").unwrap_or_default();
    if url.is_empty() {
        println!("No database attached - attach one in the Database panel.");
        return;
    }

    // The database accepts only encrypted connections, so `NoTls` cannot connect.
    let connector = MakeTlsConnector::new(TlsConnector::new().expect("tls setup failed"));
    let mut client = Client::connect(&url, connector).expect("connection failed");
    for row in client
        .query("SELECT id, title FROM todos ORDER BY id", &[])
        .expect("query failed")
    {
        let id: i32 = row.get(0);
        let title: String = row.get(1);
        println!("#{} - {}", id, title);
    }
}
php
<?php
$url = getenv('DATABASE_URL');
if ($url === false || $url === '') {
    echo "No database attached - attach one in the Database panel.\n";
    return;
}

$conn = pg_connect($url);
$result = pg_query($conn, 'SELECT id, title FROM todos ORDER BY id');
while ($row = pg_fetch_assoc($result)) {
    echo '#' . $row['id'] . ' - ' . $row['title'] . "\n";
}
pg_close($conn);
java
import java.sql.*;

public class HelloWorld {
    public static void main(String[] args) throws Exception {
        String host = System.getenv("DATABASE_HOST");
        if (host == null || host.isEmpty()) {
            System.out.println("No database attached - attach one in the Database panel.");
            return;
        }

        // JDBC has its own URL format - build it from the separate variables.
        String jdbcUrl = "jdbc:postgresql://" + host + ":" + System.getenv("DATABASE_PORT")
            + "/" + System.getenv("DATABASE_NAME");

        try (Connection conn = DriverManager.getConnection(
                 jdbcUrl, System.getenv("DATABASE_USER"), System.getenv("DATABASE_PASSWORD"));
             Statement st = conn.createStatement();
             ResultSet rs = st.executeQuery("SELECT id, title FROM todos ORDER BY id")) {
            while (rs.next()) {
                System.out.println("#" + rs.getInt("id") + " - " + rs.getString("title"));
            }
        }
    }
}
csharp
using System;
using Npgsql;

var host = Environment.GetEnvironmentVariable("DATABASE_HOST");
if (string.IsNullOrEmpty(host))
{
    Console.WriteLine("No database attached - attach one in the Database panel.");
    return;
}

// Npgsql uses key/value connection strings, not the DATABASE_URL format.
var connString =
    $"Host={host};Port={Environment.GetEnvironmentVariable("DATABASE_PORT")};" +
    $"Database={Environment.GetEnvironmentVariable("DATABASE_NAME")};" +
    $"Username={Environment.GetEnvironmentVariable("DATABASE_USER")};" +
    $"Password={Environment.GetEnvironmentVariable("DATABASE_PASSWORD")}";

using var conn = new NpgsqlConnection(connString);
conn.Open();
using var cmd = new NpgsqlCommand("SELECT id, title FROM todos ORDER BY id", conn);
using var reader = cmd.ExecuteReader();
while (reader.Read())
{
    Console.WriteLine($"#{reader.GetInt32(0)} - {reader.GetString(1)}");
}
c
#include <stdio.h>
#include <stdlib.h>
#include <libpq-fe.h>

int main() {
    const char *url = getenv("DATABASE_URL");
    if (url == NULL || url[0] == '\0') {
        printf("No database attached - attach one in the Database panel.\n");
        return 0;
    }

    PGconn *conn = PQconnectdb(url);
    if (PQstatus(conn) != CONNECTION_OK) {
        printf("connection error: %s\n", PQerrorMessage(conn));
        PQfinish(conn);
        return 1;
    }

    PGresult *res = PQexec(conn, "SELECT id, title FROM todos ORDER BY id");
    if (PQresultStatus(res) != PGRES_TUPLES_OK) {
        printf("query error: %s\n", PQerrorMessage(conn));
    } else {
        for (int i = 0; i < PQntuples(res); i++) {
            printf("#%s - %s\n", PQgetvalue(res, i, 0), PQgetvalue(res, i, 1));
        }
    }

    PQclear(res);
    PQfinish(conn);
    return 0;
}
cpp
#include <cstdlib>
#include <iostream>
#include <string>

#include <libpq-fe.h>

int main() {
    const char *url = std::getenv("DATABASE_URL");
    if (url == nullptr || url[0] == '\0') {
        std::cout << "No database attached - attach one in the Database panel.\n";
        return 0;
    }

    PGconn *conn = PQconnectdb(url);
    if (PQstatus(conn) != CONNECTION_OK) {
        std::cout << "connection error: " << PQerrorMessage(conn) << "\n";
        PQfinish(conn);
        return 1;
    }

    PGresult *res = PQexec(conn, "SELECT id, title FROM todos ORDER BY id");
    if (PQresultStatus(res) != PGRES_TUPLES_OK) {
        std::cout << "query error: " << PQerrorMessage(conn) << "\n";
    } else {
        for (int i = 0; i < PQntuples(res); i++) {
            std::cout << "#" << PQgetvalue(res, i, 0)
                      << " - " << PQgetvalue(res, i, 1) << "\n";
        }
    }

    PQclear(res);
    PQfinish(conn);
    return 0;
}

Limbajele C și C++ partajează același pachet libpq și același API C - diferența constă doar în modul în care scrii: printf și getenv în C, std::cout și std::getenv în C++.

Fiecare vine cu propria bază de date

Atașarea este per utilizator, per proiect - exact ca secretele. Atunci când altcineva deschide (sau face fork la) proiectul tău, nu primește baza ta de date: atașează una proprie în aceeași filă Database. Datele și acreditările tale nu sunt niciodată partajate.

Bazele de date și publicarea

O bază de date atașată nu te împiedică să publici - valorile DATABASE_* sunt variabile de mediu la runtime, nu sunt niciodată incluse în bundle-ul publicat. Publicarea este blocată doar atunci când proiectul folosește secrete.

Publică o aplicație de server (NestJS, Fastify, Hono sau un proiect Python cu testerul de API pornit) cu o bază de date atașată, iar containerul acesteia va porni cu aceleași variabile DATABASE_* pe care ți le oferă editorul - site-ul live comunică cu o bază de date reală, așa că codul pe care l-ai testat cu Run este codul care este livrat.

În editor, backend-urile Python și Node interoghează baza de date prin releul DojoCode (vezi secțiunile lor de mai sus); containerul publicat este locul în care codul tău comunică pentru prima dată cu Postgres direct, printr-un socket real cu driverul real.

Două lucruri de știut despre aplicația publicată:

  • Rulează pe baza ta de date. Un site publicat este o singură instanță partajată, așa că solicitarea fiecărui vizitator ajunge la baza de date atașată a proprietarului - spre deosebire de editor, unde fiecare persoană rulează pe propria instanță. Nu publica o aplicație care le permite vizitatorilor să scrie date pe care ai prefera să le păstrezi curate.
  • Modificările ajung la ea fără republicare. Atașarea, detașarea sau rotirea parolei actualizează și site-ul live - următoarea solicitare pornește un container nou care conține noile valori, așa că acordă un moment pentru acea primă solicitare în timp ce pornește.

Bazele de date și Alex (asistentul AI)

Alex știe dacă proiectul are o bază de date atașată și cum se numește - niciodată parola. Poate lista bazele tale de date, le poate inspecta tabelele și coloanele și poate scrie cod care citește corect DATABASE_URL pentru șablonul tău, previne erorile în cazul unei atașări lipsă și se potrivește cu schema ta reală în loc să ghicească. Detaliile de conexiune pe care le preia sunt întotdeauna cenzurate; acreditările ajung la codul tău doar prin intermediul mediului de rulare.

Limite și lucruri utile de știut

  • Până la 3 baze de date per cont, cu o cotă de stocare de 50 MB fiecare (soft - după o limită maximă, sunt acceptate doar interogări de citire până când eliberezi spațiu).
  • O bază de date complet nouă nu are 0 octeți. Postgres livrează fiecare bază de date cu propriile cataloage de sistem, așa că una goală raportează deja în jur de 7 MB din cotă înainte de a crea un singur tabel - aceasta este baza, nu o scurgere de memorie (leak), și îți lasă restul pentru a lucra.
  • Numele bazelor de date sunt doar nume de afișare; identificatorii și acreditările reale Postgres sunt generate și nu conțin niciodată date introduse de utilizator.
  • Interogările din editor au un timp de expirare (statement timeout) și o limită de conexiuni per rol - o interogare blocată nu îți va bloca baza de date.
  • Conexiunile sunt criptate - șirul de conexiune conține ?sslmode=require, iar conexiunile în text simplu sunt refuzate. Păstrează acest parametru dacă construiești singur URL-ul.
  • Bazele de date intră în repaus atunci când sunt inactive și se activează la următoarea conexiune, așa că prima interogare după o perioadă de inactivitate poate dura un moment în plus. Într-o aplicație publicată, preferă un pool de conexiuni cu un timp scurt de expirare pentru inactivitate în locul unui singur client pe care îl ții deschis la nesfârșit.
  • Noile valori DATABASE_* sunt preluate la următoarea rulare după atașare, detașare sau rotire - nu este necesară o reîncărcare manuală.
  • Bazele de date sunt disponibile doar pe șabloanele de pe partea de server. Locurile de joacă (playgrounds) pentru frontend, full-stack (Next.js, Astro, SvelteKit, Remix), mobile și PGlite / SQLite nu au o filă Database: codul lor rulează în previzualizarea din browser, care nu poate deschide o conexiune Postgres.