Repository Pattern
Introduktion
Repository Pattern är ett arkitekturmönster som skapar ett abstraktionslager mellan din affärslogik och dataåtkomstlagret. Istället för att sprida SQL-frågor eller ORM-anrop överallt i koden, samlas all dataåtkomst för en entitet på ett ställe — i ett repository.
Problemet utan Repository
// Utan repository — databaslogik läcker in i affärslogiken
public class OrderService {
private final EntityManager em;
public void confirmOrder(String orderId) {
// SQL direkt i affärslogiken — svårt att testa och byta ut
Order order = em.createQuery(
"SELECT o FROM Order o WHERE o.id = :id", Order.class)
.setParameter("id", orderId)
.getSingleResult();
order.confirm();
em.merge(order);
// Tänk om vi byter till MongoDB? Hela denna klass måste skrivas om.
}
}
Problemen:
- Affärslogik och databaslogik är sammanvävda
- Svårt att enhetstesta utan en riktig databas
- Byta databas kräver ändringar i hela kodbasen
Lösningen: Repository Interface
// Med repository — affärslogiken bryr sig inte om databasen
// 1. Definiera kontraktet — ett interface i domänlagret
public interface OrderRepository {
void save(Order order);
Optional<Order> findById(String id);
List<Order> findByCustomerId(String customerId);
List<Order> findPendingOrders();
void delete(String id);
}
// 2. Affärslogiken beror bara på interfacet
public class OrderService {
private final OrderRepository orderRepository; // Bara ett interface!
public void confirmOrder(String orderId) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException(orderId));
order.confirm(); // Affärsregel på entiteten
orderRepository.save(order); // Sparas via interfacet
}
}
Exempel — Fullständig Implementation (Java / Spring)
Domänentitet
public class Order {
private String id;
private String customerId;
private List<OrderItem> items;
private OrderStatus status;
private Instant createdAt;
public Order(String customerId, List<OrderItem> items) {
this.id = UUID.randomUUID().toString();
this.customerId = customerId;
this.items = new ArrayList<>(items);
this.status = OrderStatus.PENDING;
this.createdAt = Instant.now();
}
public void confirm() {
if (status != OrderStatus.PENDING)
throw new IllegalStateException("Kan bara bekräfta en väntande order.");
this.status = OrderStatus.CONFIRMED;
}
public Money calculateTotal() {
return items.stream()
.map(OrderItem::getSubtotal)
.reduce(Money.ZERO, Money::add);
}
// Getters...
}
Repository Interface
// I domänlagret — inga import av JPA, SQL eller MongoDB
public interface OrderRepository {
void save(Order order);
Optional<Order> findById(String id);
List<Order> findByCustomerId(String customerId);
List<Order> findByStatus(OrderStatus status);
long countByCustomerId(String customerId);
void deleteById(String id);
}
JPA-implementation (SQL)
@Repository
public class JpaOrderRepository implements OrderRepository {
private final JpaOrderEntityRepository jpaRepo; // Spring Data JPA
public JpaOrderRepository(JpaOrderEntityRepository jpaRepo) {
this.jpaRepo = jpaRepo;
}
@Override
public void save(Order order) {
jpaRepo.save(OrderEntity.fromDomain(order));
}
@Override
public Optional<Order> findById(String id) {
return jpaRepo.findById(id)
.map(OrderEntity::toDomain);
}
@Override
public List<Order> findByCustomerId(String customerId) {
return jpaRepo.findByCustomerId(customerId)
.stream()
.map(OrderEntity::toDomain)
.collect(Collectors.toList());
}
@Override
public List<Order> findByStatus(OrderStatus status) {
return jpaRepo.findByStatus(status.name())
.stream()
.map(OrderEntity::toDomain)
.collect(Collectors.toList());
}
@Override
public long countByCustomerId(String customerId) {
return jpaRepo.countByCustomerId(customerId);
}
@Override
public void deleteById(String id) {
jpaRepo.deleteById(id);
}
}
In-Memory-implementation (för tester)
// Ingen databas behövs för att köra enhetstester!
public class InMemoryOrderRepository implements OrderRepository {
private final Map<String, Order> store = new HashMap<>();
@Override
public void save(Order order) {
store.put(order.getId(), order);
}
@Override
public Optional<Order> findById(String id) {
return Optional.ofNullable(store.get(id));
}
@Override
public List<Order> findByCustomerId(String customerId) {
return store.values().stream()
.filter(o -> o.getCustomerId().equals(customerId))
.collect(Collectors.toList());
}
@Override
public List<Order> findByStatus(OrderStatus status) {
return store.values().stream()
.filter(o -> o.getStatus() == status)
.collect(Collectors.toList());
}
@Override
public long countByCustomerId(String customerId) {
return store.values().stream()
.filter(o -> o.getCustomerId().equals(customerId))
.count();
}
@Override
public void deleteById(String id) {
store.remove(id);
}
}
Enhetstest utan databas
class OrderServiceTest {
private OrderService orderService;
private OrderRepository orderRepository;
@BeforeEach
void setUp() {
// Ingen databas — vi använder in-memory-implementationen
orderRepository = new InMemoryOrderRepository();
orderService = new OrderService(orderRepository);
}
@Test
void confirmOrder_shouldUpdateStatus() {
// Arrange
Order order = new Order("cust-1", List.of(new OrderItem("prod-1", 2, Money.of(100))));
orderRepository.save(order);
// Act
orderService.confirmOrder(order.getId());
// Assert
Order found = orderRepository.findById(order.getId()).orElseThrow();
assertEquals(OrderStatus.CONFIRMED, found.getStatus());
}
@Test
void confirmOrder_shouldThrow_whenOrderNotFound() {
assertThrows(OrderNotFoundException.class,
() -> orderService.confirmOrder("icke-existerande-id"));
}
}
Repository vs. DAO
| Repository | DAO (Data Access Object) | |
|---|---|---|
| Abstraktionsnivå | Domänobjekt (affärslogik) | Databastabeller |
| Returnerar | Domänentiteter | Datastrukturer / DTOs |
| Passar med | DDD, Clean Architecture | Enklare applikationer |
| Frågetyp | Affärsregelbaserat | Tabellbaserat |
Fördelar
- Testbarhet — Byt ut mot In-Memory-implementation i tester.
- Utbytbarhet — Byt SQL mot MongoDB utan att ändra affärslogiken.
- Single Responsibility — All dataåtkomst för en entitet på ett ställe.
- Läsbarhet —
orderRepository.findPendingOrders()är tydligare än en SQL-fråga.
Nackdelar
- Mer kod — Interface + implementation + mapper = fler filer.
- N+1-problem — Utan omsorg kan repositories generera ineffektiva databasanrop.
- Kan vara överkurs — För enkla CRUD-appar utan komplex domänlogik.
Av Victor Hernandez från Bytebase.se