Artikkeli

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.

public interface IProductDA
{
  IEnumerable<Product> GetProductsByName(string name);
  IEnumerable<Product> 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.
    public interface IProductDA
    {
      IEnumerable<Product> GetProductsByName(string name);
      
      // pari ehtoa + eagerloadia ja lopputulos on tämä
      IEnumerable<Product> GetProductsByNameMaxPriceCategoryAndRatingIncludeCategoryAndReviews(string name, decimal maxPrice, Guid categoryId, decimal minRating);
      
      // nimet eroavat, mutta lopputulos on sama
      IEnumerable<Product> GetProductsByNameIncludeCategoryAndReviews(string name);
      IEnumerable<Product> GetProductsByNameIncludeReviewsAndCategory(string name);
    }
    
  • Oikean metodin valinta saattaa vaatia läjän iffejä tms.
    IEnumerable<Product> 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.)

public interface IProductDA
{
  IEnumerable<Product> Get(IEnumerable<Filter<Product>> filters, 
                           IEnumerable<Include<Product>> includes, 
                           IEnumerable<Order<Product>> 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
    var filters = new List<Filter<Product>>();
    if(!string.IsNullOrEmpty(productName))
    {
      filters.Add(Filter.For<Product>(p => p.Name.Contains(productName)));
    }
    if(maxPrice.HasValue)
    {
      var priceFilter = Filter.For<Product>(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<Product>(p => p.Category) };
    var ordering = new[]{ Order.Desc<Product>(p => p.Price), Order.Asc<Product>(p => p.Name) };
    var products = productDA.Get(filters, includes, ordering);
    
  • Ryhmittely ja projektiot tosi hankalia
    // väännäpäs tästä jotkin näpsäkät abstraktit speksit
    session.Query<Post>()
      .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.

public interface IDataAccess<T> where T : Entity
{
  void Create(T item);
  void Get(Guid id);
  void Update(T item);
  void Delete(T item);
  IQueryable<T> All();
}
public interface IProductDA : IDataAccess<Product>
{
}

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.

Navigointi

Social Media