Entkopplung des Datenzugriffs mit dem Repository-Muster in NestJS und ASP.NET Core
Min-jun Kim
Dev Intern · Leapcell

Einleitung
In der komplexen Welt der Backend-Entwicklung wird die Verwaltung von Dateninteraktionen oft zu einer kritischen Herausforderung. Wenn Anwendungen an Komplexität gewinnen, kann die direkte Verflechtung von Geschäftslogik mit spezifischen Datenzugriffsmechanismen zu brüchigen, schwer zu wartenden und schwierig zu testenden Codebasen führen. Stellen Sie sich ein Szenario vor, in dem ein Wechsel Ihrer Datenbanktechnologie oder Ihres ORM eine Überarbeitung großer Teile Ihrer Anwendung erfordert – eine wahrhaft entmutigende Aussicht. Genau hier werden Muster, die auf die Trennung von Belangen ausgelegt sind, unschätzbar wertvoll. Unter diesen sticht das Repository-Muster als leistungsstarker Wegbereiter für Modularität und Testbarkeit hervor und fungiert effektiv als saubere Schnittstelle zwischen Ihrer Domänenlogik und dem zugrunde liegenden Datenspeicher. Dieser Artikel befasst sich mit der praktischen Implementierung des Repository-Musters in zwei prominenten Backend-Frameworks: NestJS und ASP.NET Core, und zeigt, wie es die Architektur Ihrer Anwendungen dramatisch verbessern kann.
Kernkonzepte des Repository-Musters
Bevor wir uns mit den Implementierungen befassen, wollen wir ein klares Verständnis der beteiligten Kernkonzepte schaffen.
Domänenmodell: Dies repräsentiert die Kernentitäten und die Geschäftslogik Ihrer Anwendung, unabhängig von Datenpersistenzbelangen. Zum Beispiel wären in einer E-Commerce-Anwendung Produkt, Bestellung und Kunde Teil des Domänenmodells.
Datenzugriffsschicht (DAL): Diese Schicht ist für die Interaktion mit dem Datenpersistenzmechanismus verantwortlich, wie z. B. einer Datenbank (SQL, NoSQL), einer externen API oder sogar einem Dateisystem. Aufgaben wie das Abfragen, Einfügen, Aktualisieren und Löschen von Daten gehören hierher.
Repository-Muster: Ein Entwurfsmuster, das die Art und Weise abstrahiert, wie Daten abgerufen und gespeichert werden, und es den Domänenobjekten der Anwendung ermöglicht, die zugrunde liegende Datenzugriffstechnologie nicht zu beachten. Es fungiert als In-Memory-Sammlung von Domänenobjekten und bietet Methoden zum Hinzufügen, Entfernen, Finden und Aktualisieren dieser Objekte.
Unit of Work (Optional, aber empfohlen): Wird oft in Verbindung mit dem Repository-Muster verwendet. Das Unit of Work-Muster verwaltet eine Gruppe von Operationen als eine einzige Transaktion. Es stellt sicher, dass alle Änderungen innerhalb einer Geschäftstransaktion entweder zusammen committet oder zusammen zurückgerollt werden, wodurch die Datenkonsistenz aufrechterhalten wird.
Das Hauptziel des Repository-Musters ist es, die Geschäftslogik vor Änderungen in der Datenzugriffstechnologie zu schützen. Durch die Einführung einer Abstraktionsebene interagieren Ihre Dienste oder Controller mit Repositories, nicht direkt mit ORMs oder rohen Datenbankabfragen.
Implementierung des Repository-Musters
Lassen Sie uns die praktische Implementierung des Repository-Musters in NestJS und ASP.NET Core untersuchen und Kernprinzipien hervorheben und illustrative Codebeispiele teilen.
Das Problem ohne Repository
Betrachten Sie einen typischen Dienst, der direkt mit einem ORM interagiert:
// NestJS ohne Repository // product.service.ts import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { Product } from './product.entity'; @Injectable() export class ProductService { constructor( @InjectRepository(Product) private productRepository: Repository<Product>, ) {} async createProduct(name: string, price: number): Promise<Product> { const product = this.productRepository.create({ name, price }); return this.productRepository.save(product); } async findAllProducts(): Promise<Product[]> { return this.productRepository.find(); } }
Dieser Code legt direkt die Repository-Methoden von TypeORM offen. Wenn Sie sich entscheiden, von TypeORM zu Mongoose oder sogar zu einem benutzerdefinierten Datenbankclient zu wechseln, müsste ProductService direkt geändert werden. Diese enge Kopplung erschwert auch das Testen, da selbst für einfache Unittests eine Datenbanks einrichtung erforderlich ist.
Implementierung des Repository-Musters in NestJS
In NestJS können wir TypeScript-Schnittstellen und Abhängigkeitsinjektion nutzen, um das Repository-Muster effektiv zu implementieren.
Zuerst definieren wir einen Vertrag für unser Repository:
// src/product/interfaces/product-repository.interface.ts import { Product } from '../product.entity'; export interface IProductRepository { create(product: Partial<Product>): Promise<Product>; findAll(): Promise<Product[]>; findById(id: number): Promise<Product | undefined>; update(id: number, product: Partial<Product>): Promise<Product | undefined>; delete(id: number): Promise<void>; } // Wir definieren ein Token für die Abhängigkeitsinjektion export const PRODUCT_REPOSITORY = 'PRODUCT_REPOSITORY';
Als Nächstes implementieren wir diese Schnittstelle mit TypeORM:
// src/product/infrastructure/typeorm-product.repository.ts import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { Product } from '../product.entity'; import { IProductRepository } from '../interfaces/product-repository.interface'; @Injectable() export class TypeOrmProductRepository implements IProductRepository { constructor( @InjectRepository(Product) private readonly ormRepository: Repository<Product>, ) {} async create(productData: Partial<Product>): Promise<Product> { const product = this.ormRepository.create(productData); return await this.ormRepository.save(product); } async findAll(): Promise<Product[]> { return await this.ormRepository.find(); } async findById(id: number): Promise<Product | undefined> { return await this.ormRepository.findOne({ where: { id } }); } async update(id: number, productData: Partial<Product>): Promise<Product | undefined> { await this.ormRepository.update(id, productData); return this.findById(id); // Die aktualisierte Entität neu abrufen } async delete(id: number): Promise<void> { await this.ormRepository.delete(id); } }
Nun hängt der ProductService nur noch von der IProductRepository-Schnittstelle ab:
// src/product/product.service.ts import { Injectable, Inject } from '@nestjs/common'; import { Product } from './product.entity'; import { IProductRepository, PRODUCT_REPOSITORY } from './interfaces/product-repository.interface'; @Injectable() export class ProductService { constructor( @Inject(PRODUCT_REPOSITORY) private readonly productRepository: IProductRepository, ) {} async createProduct(name: string, price: number): Promise<Product> { return this.productRepository.create({ name, price }); } async getAllProducts(): Promise<Product[]> { return this.productRepository.findAll(); } async getProductById(id: number): Promise<Product | undefined> { return await this.productRepository.findById(id); } }
Konfigurieren Sie schließlich die Abhängigkeitsinjektion im Modul:
// src/product/product.module.ts import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { Product } from './product.entity'; import { ProductService } from './product.service'; import { ProductController } from './product.controller'; import { IProductRepository, PRODUCT_REPOSITORY } from './interfaces/product-repository.interface'; import { TypeOrmProductRepository } from './infrastructure/typeorm-product.repository'; @Module({ imports: [TypeOrmModule.forFeature([Product])], controllers: [ProductController], providers: [ ProductService, { provide: PRODUCT_REPOSITORY, useClass: TypeOrmProductRepository, }, ], exports: [ProductService], }) export class ProductModule {}
Diese Einrichtung ermöglicht den einfachen Austausch von TypeOrmProductRepository durch eine andere Implementierung (z. B. MongooseProductRepository oder MockProductRepository zum Testen), ohne ProductService zu ändern.
Implementierung des Repository-Musters in ASP.NET Core
Der Ansatz in ASP.NET Core ist recht ähnlich und nutzt C#-Schnittstellen und die integrierte Abhängigkeitsinjektion.
Definieren Sie die Schnittstelle:
// Interfaces/IProductRepository.cs using System.Collections.Generic; using System.Threading.Tasks; using YourApp.Domain.Models; // Angenommen, Product ist ein Domänenmodell namespace YourApp.Application.Interfaces { public interface IProductRepository { Task<Product> AddAsync(Product product); Task<IEnumerable<Product>> GetAllAsync(); Task<Product> GetByIdAsync(int id); Task UpdateAsync(Product product); Task DeleteAsync(int id); } }
Implementieren Sie die Schnittstelle mit Entity Framework Core:
// Infrastructure/Data/Repositories/ProductRepository.cs using Microsoft.EntityFrameworkCore; using System.Collections.Generic; using System.Threading.Tasks; using YourApp.Application.Interfaces; using YourApp.Domain.Models; using YourApp.Infrastructure.Data; // Ihr DbContext namespace YourApp.Infrastructure.Data.Repositories { public class ProductRepository : IProductRepository { private readonly ApplicationDbContext _dbContext; public ProductRepository(ApplicationDbContext dbContext) { _dbContext = dbContext; } public async Task<Product> AddAsync(Product product) { await _dbContext.Products.AddAsync(product); await _dbContext.SaveChangesAsync(); return product; } public async Task<IEnumerable<Product>> GetAllAsync() { return await _dbContext.Products.ToListAsync(); } public async Task<Product> GetByIdAsync(int id) { return await _dbContext.Products.FindAsync(id); } public async Task UpdateAsync(Product product) { _dbContext.Entry(product).State = EntityState.Modified; await _dbContext.SaveChangesAsync(); } public async Task DeleteAsync(int id) { var product = await _dbContext.Products.FindAsync(id); if (product != null) { _dbContext.Products.Remove(product); await _dbContext.SaveChangesAsync(); } } } }
Die Servicesschicht hängt nur von der Schnittstelle ab:
// Application/Services/ProductService.cs using System.Collections.Generic; using System.Threading.Tasks; using YourApp.Application.Interfaces; using YourApp.Domain.Models; namespace YourApp.Application.Services { public class ProductService { private readonly IProductRepository _productRepository; public ProductService(IProductRepository productRepository) { _productRepository = productRepository; } public async Task<Product> CreateProduct(string name, decimal price) { var product = new Product { Name = name, Price = price }; return await _productRepository.AddAsync(product); } public async Task<IEnumerable<Product>> GetAllProducts() { return await _productRepository.GetAllAsync(); } public async Task<Product> GetProductById(int id) { return await _productRepository.GetByIdAsync(id); } // ... weitere Geschäftslogik-Methoden } }
Konfigurieren Sie die Abhängigkeitsinjektion in Startup.cs (oder Program.cs in .NET 6+ Minimal APIs):
// Startup.cs (ConfigureServices-Methode) public void ConfigureServices(IServiceCollection services) { // ... andere Dienste services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))); services.AddScoped<IProductRepository, ProductRepository>(); services.AddScoped<ProductService>(); // Den Dienst ebenfalls registrieren }
Anwendungsfälle und Vorteile
Das Repository-Muster glänzt in mehreren Szenarien:
- Datenbankmigrationen: Wenn Sie Ihr Datenbanksystem (z. B. von SQL Server zu PostgreSQL) oder Ihr ORM (z. B. von Entity Framework Core zu Dapper ohne Änderung der Datenbank) ändern müssen, müssen Sie nur eine neue Repository-Implementierung erstellen, die der vorhandenen Schnittstelle entspricht.
- Testen: Es vereinfacht das Unit-Testing erheblich. Sie können die
IProductRepository-Schnittstelle für IhrenProductServiceeinfach als Mock oder Stub verwenden, ohne eine echte Datenbankverbindung zu benötigen. Dies führt zu schnelleren, zuverlässigeren und isolierteren Tests. - Domain-Driven Design (DDD): Das Repository-Muster ist ein Eckpfeiler von DDD und bietet eine sammlungsähnliche Schnittstelle zu Aggregat-Roots.
- Konsistenz: Es zentralisiert die Datenzugriffslogik und fördert eine konsistente Art der Dateninteraktion in der Anwendung.
- Leistungsoptimierung: Spezifische Operationen (z. B. komplexe Joins, gespeicherte Prozeduren) können innerhalb des Repositorys verkapselt werden, ohne die zugrunde liegenden Implementierungsdetails an die Service-Schicht preiszugeben.
Vorteile Zusammenfassung:
- Entkopplung: Geschäftslogik ist von Datenzugriffsbelangen isoliert.
- Testbarkeit: Einfach zu mocken und zu stubben für Unit-Tests.
- Wartbarkeit: Änderungen in der Datenzugriffstechnologie haben nur begrenzte Auswirkungen.
- Modularität: Fördert eine saubere Trennung der Belange.
- Flexibilität: Ermöglicht mehrere Datenquellenimplementierungen.
Fazit
Das Repository-Muster bietet eine robuste und effektive Strategie zur Entkopplung der Datenzugriffslogik von der Domänen- und Geschäftslogik Ihrer Anwendung. Durch die Einführung einer Abstraktionsebene fördert es die Modularität, verbessert die Testbarkeit erheblich und erhöht die Wartbarkeit in sowohl NestJS- als auch ASP.NET Core-Anwendungen. Die Implementierung dieses Musters stellt sicher, dass Ihre Dateninteraktionen sauber, konsistent und widerstandsfähig gegenüber sich entwickelnden technischen Anforderungen sind, was Ihre Backend-Systeme anpassungsfähiger und langfristig einfacher zu verwalten macht. Nutzen Sie das Repository-Muster, um flexiblere und nachhaltigere Backend-Architekturen aufzubauen.