Zum Inhalt springen
≈ 2 minNeuModellierungArchitekturDatenbankBackendPythonSQL

Technischer Modellierungsprozess

Eine Anwendung wird Schicht für Schicht von unten nach oben modelliert – zuerst die Persistenz, dann Datenzugriff, Geschäftslogik, API und zuletzt die Oberfläche.

In 30 Sekunden

  1. 01

    Die erste Schicht ist die Persistenzschicht – das Datenmodell lebt am längsten und ist am teuersten zu ändern.

  2. 02

    Jede höhere Schicht baut nur auf der direkt darunterliegenden auf – Datenzugriff, Logik, API, UI.

  3. 03

    Jede Schicht hat ein greifbares Artefakt, etwa SQL-Schema, Repository, Service, Endpunkt oder Komponente.

Von unten nach oben

05

Präsentation

Die Oberfläche, die die API konsumiert und Daten darstellt.

KomponentenServicesFormulare
04

API

Der Vertrag nach außen – welche Endpunkte gibt es, welche Daten fließen?

EndpunkteDTOsOpenAPI-Spec
03

Geschäftslogik

Regeln und Abläufe der Fachdomäne – unabhängig von Datenbank und HTTP.

ServicesValidierungUse Cases
02

Datenzugriff

Kapselt SQL hinter einer Schnittstelle, damit die Logik nicht weiß, wie gespeichert wird.

EntitiesRepositoryORM-Mapping
01

PersistenzschichtFundament

Welche Daten gibt es, wie hängen sie zusammen? Tabellen, Schlüssel, Constraints, Indizes.

ER-ModellSQL-SchemaMigrationen
Gebaut wird von unten (01) nach oben – jede Schicht nutzt nur die darunter.

Warum die Persistenz zuerst?

Daten überleben Code

Frontends werden neu geschrieben, Frameworks wechseln – die Daten bleiben oft jahrzehntelang.

Änderungen sind teuer

Ein Feld umbenennen heißt bei Live-Daten: Migration, Rückwärtskompatibilität, Downtime-Risiko.

Fundament für alles

Wer Entitäten und Beziehungen kennt, kann Logik und API sauber darauf abbilden.

Welche Datenbank wann passt und was alles zur Persistenzschicht gehört, erklärt das Unterthema Persistenzschicht und Datenbankwahl.

Ein Beispiel durch alle Schichten

Eine Aufgabenverwaltung mit PostgreSQL, Python (SQLAlchemy + FastAPI) und Angular. Ein User hat viele Tasks, Tasks und Tags stehen in einer n:m-Beziehung.

1 : nn : mapp_userid · emailtaskid · user_id · title · due_datetagid · name
ER-Modell zuerst. Die n:m-Beziehung wird zur Zwischentabelle task_tag.

1 · Persistenz

PostgreSQL-Schema: Schlüssel, Constraints und ein Index für die häufigste Abfrage „offene Tasks eines Users nach Fälligkeit“.

CREATE TABLE app_user (
  id         BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  email      TEXT NOT NULL UNIQUE,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE task (
  id       BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  user_id  BIGINT NOT NULL REFERENCES app_user (id) ON DELETE CASCADE,
  title    TEXT NOT NULL CHECK (length(title) BETWEEN 1 AND 200),
  due_date DATE,
  done     BOOLEAN NOT NULL DEFAULT false
);
CREATE INDEX idx_task_user_due ON task (user_id, due_date);

CREATE TABLE tag (
  id   BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  name TEXT NOT NULL UNIQUE
);

CREATE TABLE task_tag (
  task_id BIGINT REFERENCES task (id) ON DELETE CASCADE,
  tag_id  BIGINT REFERENCES tag (id) ON DELETE CASCADE,
  PRIMARY KEY (task_id, tag_id)
);

Spätere Änderungen laufen als Migration (hier Alembic), nie per Hand:

def upgrade() -> None:
    op.add_column("task", sa.Column("priority", sa.SmallInteger(), nullable=False, server_default="2"))

def downgrade() -> None:
    op.drop_column("task", "priority")

2 · Datenzugriff

Entity + Repository: Das Mapping beschreibt die Tabelle im Code, das Repository kapselt die Abfragen. Dieselbe Schicht in drei Sprachen:

Python · SQLAlchemy

class Task(Base):
    __tablename__ = "task"

    id: Mapped[int] = mapped_column(primary_key=True)
    user_id: Mapped[int] = mapped_column(ForeignKey("app_user.id"))
    title: Mapped[str] = mapped_column(String(200))
    due_date: Mapped[date | None]
    done: Mapped[bool] = mapped_column(default=False)


class TaskRepository:
    def __init__(self, session: Session) -> None:
        self.session = session

    def open_tasks_for(self, user_id: int) -> list[Task]:
        stmt = (
            select(Task)
            .where(Task.user_id == user_id, Task.done.is_(False))
            .order_by(Task.due_date)
        )
        return list(self.session.scalars(stmt))

    def add(self, task: Task) -> Task:
        self.session.add(task)
        self.session.flush()  # vergibt die ID
        return task

Java · Spring Data JPA

@Entity
@Table(name = "task")
public class Task {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    private AppUser user;

    private String title;
    private LocalDate dueDate;   // → Spalte due_date
    private boolean done;
}

public interface TaskRepository extends JpaRepository<Task, Long> {
    // Spring leitet die SQL-Abfrage aus dem Methodennamen ab
    List<Task> findByUserIdAndDoneFalseOrderByDueDateAsc(Long userId);
}

C# · EF Core

// "TaskItem", weil Task in .NET schon System.Threading.Tasks.Task ist
public class TaskItem
{
    public long Id { get; set; }
    public long UserId { get; set; }
    public string Title { get; set; } = "";
    public DateOnly? DueDate { get; set; }
    public bool Done { get; set; }
}

public class TaskRepository(AppDbContext db)
{
    public Task<List<TaskItem>> OpenTasksFor(long userId) =>
        db.Tasks
          .Where(t => t.UserId == userId && !t.Done)
          .OrderBy(t => t.DueDate)
          .ToListAsync();
}

3 · Logik

Service: Hier stehen die Fachregeln – ohne SQL, ohne HTTP. Deshalb lässt er sich mit einem Fake-Repository in Millisekunden testen.

class TaskService:
    def __init__(self, repo: TaskRepository) -> None:
        self.repo = repo

    def create(self, user_id: int, title: str, due: date | None) -> Task:
        if due is not None and due < date.today():
            raise ValueError("Fälligkeitsdatum liegt in der Vergangenheit")
        task = Task(user_id=user_id, title=title.strip(), due_date=due)
        return self.repo.add(task)

4 · API

FastAPI-Endpunkt: Die DTOs (Pydantic) definieren den Vertrag nach außen. Daraus entsteht automatisch die OpenAPI-Spec unter /docs.

class TaskIn(BaseModel):
    title: str = Field(min_length=1, max_length=200)
    due_date: date | None = None


class TaskOut(TaskIn):
    model_config = ConfigDict(from_attributes=True)
    id: int
    done: bool


@router.post("/tasks", status_code=201, response_model=TaskOut)
def create_task(body: TaskIn, user: CurrentUser, service: TaskServiceDep) -> Task:
    # CurrentUser und TaskServiceDep sind FastAPI-Dependencies (Auth bzw. Service mit Session)
    return service.create(user.id, body.title, body.due_date)

5 · Präsentation

Angular: Das Interface spiegelt TaskOut, ein Service kapselt den HTTP-Aufruf, die Komponente kennt nur den Service.

export interface Task {
  id: number;
  title: string;
  due_date: string | null; // ISO-Datum, z. B. "2026-11-30"
  done: boolean;
}

@Injectable({ providedIn: 'root' })
export class TaskApi {
  private readonly http = inject(HttpClient);

  create(title: string, due_date: string | null) {
    return this.http.post<Task>('/api/tasks', { title, due_date });
  }
}

Vertiefung pro Thema

Merksatz

„Erst die Daten richtig modellieren – Code lässt sich leicht ändern, Daten nicht.“

Selbsttest

Erst selbst überlegen, dann aufklappen.

01Warum beginnt man mit der Persistenzschicht?

Weil alle anderen Schichten davon abhängen und Schemaänderungen an produktiven Daten (Migrationen) teuer und riskant sind.

02In welcher Schicht gehört die Regel „Fälligkeitsdatum nicht in der Vergangenheit“?

In die Geschäftslogik (Service). Die API prüft nur das Format, die Datenbank sichert harte Invarianten wie NOT NULL oder Fremdschlüssel ab.

03Warum heißt die Tabelle im Beispiel app_user statt user?

user ist in PostgreSQL ein reserviertes Wort. Man müsste es überall in Anführungszeichen setzen – ein eindeutiger Name erspart das.

Weiter in Grundlagen

Persistenzschicht und Datenbankwahl