Prima di tutto: hai ragione, questo è un vero problema. Poiché spesso i problemi di progettazione emergono quando provi a testare cose. Sebbene il test sia per la maggior parte del tempo abbastanza facile se si hanno alcune capacità di progettazione e un codice basato su OOP, le cose diventano complicate quando il database viene coinvolto.
Un sintomo è che se vuoi avere un'entità A (cioè un oggetto persistente in un database) hai bisogno dell'intero albero delle dipendenze e non hai un modo semplice per deriderle.
La ragione di questo tipo di problema è che non esiste alcuna separazione tra runtime e tempo di compilazione nei database, quindi una dipendenza che si desidera avere / utilizzare in runtime è sempre presente.
Una soluzione teorica è creare un sistema di database che abbia in realtà qualcosa di simile alle interfacce. Ma probabilmente è fuori portata per la maggior parte di noi.
La soluzione più realistica è quella di separare il database in modo molto rigoroso dal resto del codice, isolando così il grande blob contorto che viene comunemente definito schema del database dall'applicazione ben strutturata.
Il primo passo è quello di avere i repository che restituiscono le tue entità e non perdono nessuna informazione su dove e come queste entità vengono mantenute. Un buon modo per garantire questo è avere due implementazioni di quelli: uno in realtà utilizza il tuo database e uno che usa ad esempio semplici HashMaps per l'archiviazione. I successivi sono anche molto utili per i test. Diffidare di eventuali strutture che sanguinano nel codice del dominio. L'ibernazione, ad esempio, ha l'abitudine di farlo tramite il caricamento lento e meccanismi simili.
Il secondo passo è identificare gli aggregati nel senso di Domain Driven Design. Cioè sacche di entità che vengono controllate da un'entità e dal suo repository, la root dell'entità. Ad esempio, LineItems of Orders dovrebbe probabilmente essere caricato e aggiornato solo tramite un Ordine, mai da solo. Ma le entità che appartengono a un aggregato diverso (ad esempio i clienti) dovrebbero essere caricate tramite il proprio repository e non tramite una sorta di caricamento lazy magico sulla navigazione. In questo modo puoi memorizzare diverse entità in diversi datastore, alla tua applicazione non interessa. Se si trovano nello stesso database (come probabilmente fanno) possono avere chiavi esterne, nessun problema.
Potrebbe esserci una sfida tecnica con questo approccio causato dai vincoli: i vincoli normali vengono applicati su ogni singola istruzione DML. Se ciò causa problemi, potresti considerare vincoli posticipati che vengono applicati solo al commit. Ciò potrebbe effettivamente migliorare le prestazioni, ma potrebbe anche rendere più difficile il debug, perché ora potresti ottenere violazioni delle chiavi esterne sul commit, molto tempo dopo che sono state eseguite le tue dichiarazioni DML.