Alternative al modello di repository per incapsulare la logica ORM?


24

Ho appena dovuto sostituire un ORM ed è stato un compito relativamente scoraggiante, perché la logica della query perdeva dappertutto. Se avessi mai dovuto sviluppare una nuova applicazione, la mia preferenza personale sarebbe stata quella di incapsulare tutta la logica della query (usando un ORM) per renderlo a prova di futuro per il cambiamento. Il modello del repository è abbastanza problematico da codificare e mantenere, quindi mi chiedevo se ci sono altri modelli per risolvere il problema?

Posso prevedere post su come non aggiungere ulteriore complessità prima che sia effettivamente necessario, essere agili ecc., Ma sono interessato solo ai modelli esistenti che risolvono un problema simile in un modo più semplice.

Il mio primo pensiero è stato quello di avere un repository di tipo generico, al quale aggiungo i metodi necessari a specifiche classi di repository di tipo tramite metodi di estensione, ma test di unità su metodi statici è terribilmente doloroso. IE:

public static class PersonExtensions
{
    public static IEnumerable<Person> GetRetiredPeople(this IRepository<Person> personRep)
    {
        // logic
    }
}

5
Cos'è esattamente nel modello di repository che trovi problematico da codificare e mantenere?
Matthew Flynn,

1
Ci sono alternative là fuori. Uno di questi consiste nell'utilizzare il modello Query Object che, sebbene non lo abbia usato, sembra un buon approccio. Repository IMHO è un buon modello se usato nel modo giusto ma non l'essere tutto e finire tutto
dreza,

Risposte:


14

Prima di tutto: un repository generico dovrebbe essere considerato una classe base e non implementazioni complete. Dovrebbe aiutarti a mettere in atto i metodi CRUD comuni. Ma devi ancora implementare i metodi di query:

public class UserRepository : EntityFrameworkRepository<User>, IUserRepository
{
    public UserRepository(DbContext context){}

    public ICollection<Person> FindRetired()
    {
        return context.Persons.Where(p => p.Status == "Retired").ToList();
    }
}

Secondo:

Non tornare IQueryable. È un'astrazione che perde. Prova a implementare quell'interfaccia tu stesso o usala senza conoscenza del provider di database sottostante. Ad esempio, ogni provider di database ha la propria API per caricare con entusiasmo le entità. Ecco perché è un'astrazione che perde.

Alternativa

In alternativa è possibile utilizzare le query (ad esempio, come definito dal modello di separazione Comando / Query). CQS è descritto in Wikipedia.

Fondamentalmente crei classi di query che invochi:

public class GetMyMessages
{
    public GetMyMessages(IDbConnection connection)
    {
    }


    public Message[] Execute(DateTime minDate)
    {
        //query
    }
}

il bello delle query è che è possibile utilizzare metodi di accesso ai dati diversi per query diverse senza ingombrare il codice. ad esempio è possibile utilizzare un servizio Web in uno, vietare in un altro e ADO.NET in un terzo.

Se sei interessato a un'implementazione .NET leggi il mio articolo: http://blog.gauffin.org/2012/10/griffin-decoupled-the-queries/


Il modello alternativo che hai suggerito non creerebbe un disordine ancora maggiore su progetti più grandi visto che quale sarebbe un metodo nel modello di repository ora è un'intera classe?
Dante,

3
Se la tua definizione di avere classi piccole è ingombra, beh sì. In tal caso, non dovresti provare a seguire i principi SOLIDI, poiché applicarli produrrà sempre più classi (più piccole).
jgauffin,

2
Le classi più piccole sono migliori, ma la navigazione delle classi di query esistenti non sarebbe più problematica, se non quella di esplorare l'intellisenso di un repository (di dominio) per vedere se la funzionalità necessaria si trova lì dentro e, in caso contrario, implementarla.
Dante,

4
La struttura del progetto diventa più importante. Se metti tutte le query in uno spazio dei nomi come YourProject.YourDomainModelName.Queriesquesto sarà facile da navigare.
jgauffin,

Una cosa che trovo difficile da fare in modo positivo con CQS sono le query più comuni. Ad esempio, dato un utente, una query ottiene tutti gli studenti associati (voti diversi, figli propri ecc.). Ora un'altra query deve ottenere figli per un utente e quindi eseguire alcune operazioni su di essi, quindi la query 2 può sfruttare la logica comune nella query 1 ma non esiste un modo semplice per farlo. Non sono stato in grado di trovare alcuna implementazione CQS che lo faciliti facilmente. I repository aiutano molto a questo proposito. Non sono un fan di nessuno dei due modelli, ma condivido semplicemente il mio approccio.
Mrchief,

8

Innanzitutto penso che dico che ORM è già abbastanza grande astrazione sul tuo database. Alcuni ORM forniscono un legame a tutti i database relazionali comuni (NHibernate ha collegamenti a MSSQL, Oracle, MySQL, Postgress, ecc.) Quindi costruire una nuova astrazione su di esso non mi sembra redditizio. Inoltre, sostenendo che è necessario "sottrarre" questo ORM non ha senso.

Se vuoi ancora costruire questa astrazione, andrei contro il modello del repository. Era eccezionale nell'era del puro SQL, ma è piuttosto problematico di fronte al moderno ORM. Principalmente perché finisci per reimplementare la maggior parte delle funzionalità ORM, come le operazioni CRUD e le query.

Se dovessi costruire tali astrazioni, userei queste regole / schemi

  • CQRS
  • Utilizzare il più possibile delle funzionalità ORM esistenti
  • Incapsula solo la logica complessa e le query
  • Cerca di nascondere "idraulica" dell'ORM all'interno di un'architettura

In una concreta implementazione, utilizzerei direttamente le operazioni CRUD di ORM, senza alcun wrapping. Vorrei anche fare semplici query direttamente nel codice. Ma query complesse verrebbero incapsulate nei propri oggetti. Per "nascondere" l'ORM, proverei a iniettare il contesto dei dati in modo trasparente in un servizio / oggetti dell'interfaccia utente e fare lo stesso per interrogare gli oggetti.

L'ultima cosa che vorrei dire è che molte persone usano ORM senza sapere come usarlo e come trarne il miglior "profitto". Le persone che raccomandano i repository sono di solito di questo tipo.

Come lettura consigliata, direi il blog di Ayende , in particolare questo articolo .


+1 "L'ultima cosa che vorrei dire è che molte persone usano ORM senza sapere come usarlo e come trarne il miglior" profitto ". Le persone che raccomandano i repository di solito sono di questo tipo"
Misters,

1

Il problema con l'implementazione che perde è che sono necessarie molte diverse condizioni di filtro.

È possibile restringere questo api se si implementa un metodo di repository FindByExample e lo si utilizza in questo modo

// find all retired persons
Person filter = new Person {Status=PersonStatus.Retired};
IEnumerable<Person> found = personRepository.FindByExample(filter);


// find all persons who have a dog named "sam"
Person filter = new Person();
filter.AddPet(new Dog{Name="sam"});
IEnumerable<Person> found = personRepository.FindByExample(filter);

0

Prendi in considerazione l'idea di far funzionare le tue estensioni su IQueryable anziché su IRepository.

public static class PersonExtensions
{
    public static IQueryable<Person> AreRetired(this IQueryable<Person> people)
    {
        return people.Where(p => p.Status == "Retired");
    }
}

Per unit test:

List<Person> people = new List<Person>();
people.Add(new Person() { Name = "Bob", Status = "Retired" });
people.Add(new Person() { Name = "Sam", Status = "Working" });

var retiredPeople = people.AsQueryable().AreRetired();
// assert that Bob is in the list
// assert that Sam is not in the list

Non è richiesto il derisione del dilemma per i test. È inoltre possibile combinare questi metodi di estensione per creare query più complesse secondo necessità.


5
-1 a) IQueryableè una madre cattiva, o piuttosto un'interfaccia DIO. L'implementazione è molto efficace e ci sono pochissimi (se ve ne sono) i LINQ completi per i provider di SQL. b) I metodi di estensione rendono impossibile l'estensione (uno dei principi OOP fondamentali).
jgauffin,

0

Ho scritto un modello di oggetti di query piuttosto accurato per NHibernate qui: https://github.com/shaynevanasperen/NHibernate.Sessions.Operations

Funziona utilizzando un'interfaccia come questa:

public interface IDatabases
{
    ISessionManager SessionManager { get; }

    T Query<T>(IDatabaseQuery<T> query);
    T Query<T>(ICachedDatabaseQuery<T> query);

    void Command(IDatabaseCommand command);
    T Command<T>(IDatabaseCommand<T> command);
}

Data una classe di entità POCO come questa:

class Database1Poco
{
    public int Property1 { get; set; }
    public string Property2 { get; set; }
}

È possibile creare oggetti query in questo modo:

class Database1PocoByProperty1 : DatabaseQuery<Database1Poco>
{
    public override Database1Poco Execute(ISessionManager sessionManager)
    {
        return sessionManager.Session.Query<Database1Poco>().SingleOrDefault(x => x.Property1 == Property1);
    }

    public int Property1 { get; set; }
}

E poi usali in questo modo:

var database1Poco = _databases.Query(new Database1PocoByProperty1 { Property1 = 1 });

1
Sebbene questo collegamento possa rispondere alla domanda, è meglio includere qui le parti essenziali della risposta e fornire il collegamento come riferimento. Le risposte di solo collegamento possono diventare non valide se la pagina collegata cambia.
Dan Pichelman,

Mi scuso, avevo fretta. Risposta aggiornata per mostrare qualche codice di esempio.
Shayne,

1
+1 per migliorare il post con più contenuti.
Songo,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.