Miksi piilottaa ORM?

Yhä useammin tietokantoja käsitellään ORMien kautta. Yhä vahvemmin olen myös alkanut ajatella, että ORMin piilottaminen jonkin lisäkerroksen taakse on huono idea. Kokosin neljä tapaa hakea dataa sekä niiden hyvät ja huonot puolet. Oletuksena on, että kaikissa vaihtoehdoissa käytetään kuitenkin ORMia joko suoraan tai piilossa.

Metodi per kysely

Hyvin yksinkertainen malli. Jokaiselle erilaiselle kyselylle on oma metodi. ``` csharp public interface IProductDA { IEnumerable GetProductsByName(string name); IEnumerable GetProductsByCategory(Guid categoryId); } ```

Plussat

  • Ei sidoksia tiettyyn ORMiin
  • Voi optimoida jokaisen metodin

Miinukset

  • Missä tahansa vähänkin monimutkaisessa järjestelmässä metodeita tulee helposti satoja. Metodien joukosta on vaikea löytää se tietty ja sama toiminto voi jo löytyä hieman eri nimisen metodin takaa. ``` csharp public interface IProductDA { IEnumerable GetProductsByName(string name); // pari ehtoa + eagerloadia ja lopputulos on tämä IEnumerable GetProductsByNameMaxPriceCategoryAndRatingIncludeCategoryAndReviews(string name, decimal maxPrice, Guid categoryId, decimal minRating); // nimet eroavat, mutta lopputulos on sama IEnumerable GetProductsByNameIncludeCategoryAndReviews(string name); IEnumerable GetProductsByNameIncludeReviewsAndCategory(string name); } ```
  • Oikean metodin valinta saattaa vaatia läjän iffejä tms. ``` csharp IEnumerable products; if(includeCategory) { if(includeReviews) { products = productDA.GetProductsByNameIncludeCategoryAndReviews(productName); } else { products = productDA.GetProductsByNameIncludeCategory(productName); } } else if(includeReviews) { products = productDA.GetProductsByNameIncludeReviews(productName); } else { products = productDA.GetProductsByName(productName); } ```
  • Uusi kysely pitää lisätä interfaceen ja toteuttavaan luokkaan, josta aiheutuu joko kaikkien kirjastoa käyttävien sovellusten päivittäminen tai useita eri versioita kirjastosta

Specification tai vastaava pattern

Kyselyt hoidetaan yhdellä tai muutamalla metodille, jotka ottavat vastaan "speksit" (ehdot, eager load, järjestäminen jne.) ``` csharp public interface IProductDA { IEnumerable Get(IEnumerable> filters, IEnumerable> includes, IEnumerable> orderings); } ```

Plussat

  • Ei sidoksia tiettyyn ORMiin
  • Uudet kyselyt eivät aiheuta samanlaista päivitystarvetta ydinkirjastoon kuin edellisessä vaihtoehdossa
  • Selkeä rajapinta (ainakin edelliseen vaihtoehtoon verrattuna)

Miinukset

  • Paljon koodia perusarkkitehtuurin kasaamiseen, tulkkaamiseen eri ORMeille sopivaksi ja speksien rakentamiseen ``` csharp var filters = new List>(); if(!string.IsNullOrEmpty(productName)) { filters.Add(Filter.For(p => p.Name.Contains(productName))); } if(maxPrice.HasValue) { var priceFilter = Filter.For(p => p.Price <= maxPrice); if(filters.Any()) { var filter = filters.Last(); filters.Remove(filter); var andFilter = Filter.And(filter, priceFilter); filters.Add(andFilter); } else { filters.Add(priceFilter); } } var includes = new[]{ Include.For(p => p.Category) }; var ordering = new[]{ Order.Desc(p => p.Price), Order.Asc(p => p.Name) }; var products = productDA.Get(filters, includes, ordering); ```
  • Ryhmittely ja projektiot tosi hankalia ``` csharp // väännäpäs tästä jotkin näpsäkät abstraktit speksit session.Query() .GroupBy(p => new { Year = p.Published.Year, Month = p.Published.Month }) .Select(g => new { Year = g.Key.Year, Month = k.Key.Month, PostCount = g.Count() }); ```

IQueryable

Simppeli wrapper, jolla dataa haetaan IQueryablen kautta. ``` csharp public interface IDataAccess where T : Entity { void Create(T item); void Get(Guid id); void Update(T item); void Delete(T item); IQueryable All(); } public interface IProductDA : IDataAccess { } ```

Plussat

  • Edelleen ei sidoksia ORMiin
  • Minimalistinen. Koodaamiseen menee pari minuuttia.
  • Käyttää LINQ:ta suoraan, joten kyselyjä voi rakentaa vapaasti ydinkirjastoa muuttamatta

Miinukset

  • Ei voi käyttää ORMien erityisominaisuuksia tai niille pitää väsätä abstraktiot
    • Eager load: NHibernatessa Fetch, Entity Frameworkissä Include
    • Future query
    • 2nd level cache
    • Muut APIt (Esim. NHibernatessa Criteria, QueryOver, HQL sekä SQL ja Entity Frameworkissä Entity SQL ja SQL)
  • Eri ORMeilla voi olla erilainen tuki LINQ-operaatioille / muutenkin käyttäytyä eri tavoilla

ORMin käyttäminen suoraan

Plussat

  • Saa kaiken hyödyn ORMien ominaisuuksista
  • Ei tarvetta abstraktiokerrokselle

Miinukset

  • ORMin vaihto työlästä

Yksikkötestaus

Eräs peruste ORMin piilottamiselle on ollut yksikkötestauksen mahdollistaminen. Kuitenkin ainakin NHibernaten ISessionFactory ja ISession ovat helppoja mockattavia, samoin Entity Frameworkin DbContext.

Yhteenveto

Vaikka jokaisella vaihtoehdolla on puolensa, normitapauksissa suosisin niitä alhaalta ylöspäin. ORMin piilottaminen vaatii paljon ylimääräistä koodia, joka ei kuitenkaan tuo lisäarvoa. ORMin vaihto ei kuitenkaan tule tapahtumaan, joten siihenkin varautuminen on turhaa työtä, etenkin kun samalla menetetään kunkin ORMin erityisominaisuudet.