#897: AuftragsCache.kt

projectforge-business/src/main/kotlin/org/projectforge/business/fibu/AuftragsCache.kt

Type: Spring @Service — timed cache (AbstractCache)

Package: org.projectforge.business.fibu

Full path: projectforge-business/src/main/kotlin/org/projectforge/business/fibu/AuftragsCache.kt

252 lines · 174 code · 48 comments · 30 blank

Performance-critical timed cache that preloads and maintains all order (Auftrag) information for the order book. Extends AbstractCache with an 8-hour refresh cycle. Loads all non-deleted orders, their positions, and payment schedules into in-memory maps to avoid repeated database queries. Calculates invoiced sums, fully-invoiced status, "to be invoiced" flags, and tracks counts of orders needing invoicing. Listens for changes via BaseDOModifiedListener hooks on both AuftragDO and RechnungDO to invalidate cached data. Handles a cyclic dependency with AuftragDao via post-construction wiring.

Class Definition

@Service
class AuftragsCache : AbstractCache(8 * TICKS_PER_HOUR)

Dependencies

DependencyTypeNotes
auftragsCacheService@Autowired AuftragsCacheServiceProvides bulk SELECT queries for orders, positions, and payment schedules
rechnungDao@Autowired RechnungDaoListened for invoice changes that invalidate order data
auftragDao@Autowired AuftragDaoWired back to AuftragDao.auftragsCache via init(); listened for order changes

Internal Data Structures

MapKeyValueDescription
orderInfoMapLong (order ID)OrderInfoAll cached order info by order ID
orderPositionMapByPosIdLong (position ID)OrderPositionInfoAll cached position info by position ID
toBeInvoicedCounterInt?Cached count of orders with toBeInvoiced == true (lazily calculated)

Initialization (@PostConstruct init())

Sets the singleton instance, wires this into auftragDao.auftragsCache, and registers two change listeners:

Public Lookup Methods

MethodReturnsDescription
getOrderInfo(orderId)OrderInfo?Get cached info by order ID
getOrderInfo(order)OrderInfoGet cached info for an order object (falls back to empty OrderInfo if not found)
findOrderInfoByNumber(orderNumber)OrderInfo?Lookup by AuftragDO.nummer
getOrderInfoByPositionId(positionInfoId)OrderInfo?Get the parent order info for a given position ID
getOrderPositionInfo(positionId)OrderPositionInfo?Get cached position info by position ID
getOrderPositionInfosByAuftragId(auftragId)Collection<OrderPositionInfo>?Get all position infos for an order
setOrderInfo(order)UnitFills order.info with cached calculated data
getFakturiertSum(order)BigDecimalConvenience for getOrderInfo(order).invoicedSum
isVollstaendigFakturiert(order)BooleanConvenience for getOrderInfo(order).isVollstaendigFakturiert
isPositionAbgeschlossenUndNichtVollstaendigFakturiert(order)BooleanConvenience lookup
isPaymentSchedulesReached(order)BooleanConvenience lookup
getToBeInvoicedCounter()IntLazily counts orders with toBeInvoiced == true

Cache Invalidation

Refresh Logic (refresh())

Called when the cache TTL expires (every 8 hours) or when manually expired:

  1. Logs refresh start with LogDuration timer
  2. Loads all non-deleted order positions via auftragsCacheService.selectNonDeletedAuftragsPositions(), groups by auftrag.id
  3. Loads all orders via auftragsCacheService.selectAuftragsList()
  4. Loads all non-deleted payment schedules via auftragsCacheService.selectNonDeletedPaymentSchedules(), groups by auftrag.id
  5. Initializes OrderInfo objects for each order (calling updateFields(order) to copy basic data)
  6. Creates OrderPositionInfo objects for each position (note: constructor uses AuftragsRechnungCache, which in turn uses AuftragsCache — the cyclic dependency is resolved by running caches twice on initialization)
  7. Assigns positions and payment schedules to each order, calling order.info.calculateAll(order, positions, paymentSchedules)
  8. Replaces the immutable maps atomically
  9. Resets toBeInvoicedCounter to null
  10. Logs completion time

Thread Safety

Maps (orderInfoMap, orderPositionMapByPosId) are replaced atomically with new immutable maps during refresh. The comment "No sync, immutable map" on all read accesses indicates a read-only snapshot pattern: on each checkRefresh() call, the cache may rebuild a fresh map and swap the reference.

Design Notes

Git History

868d6abb7 2025 -> 2026
51d352133 AuftragsCache / AuftragsRechnungsCache handling refactored (cyclic usage resolved by running caches twice on initialization)
0043cbdb5 WIP Forecast
63081666f Source file headers: 2024-> 2025.
ae2c04ee0 Migration stuff in progress... (all tests of all packages: OK).
9aff90908 Migration stuff in progress... (all tests of all packages: OK).
89ea9a532 Migration stuff in progress... (all tests of all packages: OK).
d02c8a770 Migration stuff in progress...
3159cfa6c Migration stuff in progress... (all tests of all packages: OK).
ff2cc4cfa Migration stuff in progress... (all tests of all packages: OK).
714452019 Migration stuff in progress... (all tests of all packages: OK).
567ca70cd Migration in progress... spring.datasource.hikari.auto-commit=false
(40+ commits spanning migration, refactoring, java.time conversion, Jakarta migration, etc.)