Entity Framework vs. NHibernate - eli miksi NHibernate rokkaa edelleen
Törmäsin Entity Frameworkiin vuosien 2008/2009 vaihteessa ja siitä tuli pian de facto -datakerros omiin projekteihini. 2010 olin tekemässä projektia MySQL:n päälle, mutta Entity Designer ei oikein toiminut sen kanssa yhteen Visual Studio Expressissä, joten päädyin etsimään vaihtoehtoa. Ja sen tarjosi NHibernate.
Vaikeahan se oli... visuaaliseen työkaluun verrattuna. Xml-mäppäykset, LINQ-tuki erillisen kirjaston kautta ja sekin varsin puutteellinen, kummallinen konfiguraatio. Sitten löysin Fluent NHibernaten ja xml-mäppäykset olivat poissa. Myös LINQ-tuki integroitiin myöhemmin samaan pakettiin NHibernaten kanssa ja se on parantunut koko ajan. Opin rakastamaan POCO-luokkia. Entity Designerin tuottama ruma koodi alkoi puistattaa. Hylkäsin EF:n ja NHibernatesta tuli uusi de facto.
Code firstin myötä kokeilin EF4:ää, mutta sillä ei pystynyt mäppäämään vaikeita skenaarioita. Oikeastaan jopa FK:n nimeäminen taisi olla sille ylivoimainen haaste. En koskenut siihen tuon jälkeen. Nyt EF5 tarjosi tilaisuuden korjata tilanne. Samalla iskulla kokeilin myös NHibernaten mapping-by-code -systeemiä.
Testikantana toimi Adventure Works LT 2012
Mäppäykset
Tein mäppäykset ensiksi NHibernatelle. Siinä meni n. 5 minuuttia per taulu. Muutaman seikan jouduin tarkistamaan googlella. Käännös meni läpi, mutta testiajo paljasti, että tein jotain väärin. Google pelasti jälleen ja testiajo onnistui. Seuraavaksi tein mäppäykset EF:lle. Jälleen jouduin turvautumaan netin ohjeisiin, mutta sain homman pakettiin. Jopa nopeammin kuin NH:n tapauksessa. Sitten totuus iski vasten kasvoja. EF:n mäppäysmahdollisuuksien kattavuus on edelleen huono. Ongelmaksi muodostui komposiitti-id, jossa käytetään FK:ta. Esimerkiksi taulu CustomerAddress, jossa on kentät CustomerId ja AddressId. Sitä vastaamaan tein luokan
``` csharp
public class CustomerAddress
{
public virtual Customer Customer { get; set; }
public virtual Address Address { get; set; }
public virtual string AddressType { get; set; }
public virtual DateTime ModifiedDate { get; set; }
}
```
NH:n mäppäys toimi kivuttomasti pätkällä
``` csharp
ComposedId(id =>
{
id.ManyToOne(a => a.Customer, m =>
{
m.Column("CustomerID");
m.NotNullable(true);
});
id.ManyToOne(a => a.Address, m =>
{
m.Column("AddressID");
m.NotNullable(true);
});
});
```
mutta EF ei tue kyseistä skenaariota (ainakin muutaman selaamani nettisivun perusteella). Eli seuraavanlaista pätkää tarjosin, mutta komposiitti-id:ssä sallitaan kuitenkin vain yksinkertaiset tyypit (kuten int ja string).
``` csharp
HasKey(a => new { a.Customer, a.Address });
```
Muuten mäppäykset hoituivat molemmilla kirjastoilla melko kivottomasti. Molempien APIt olivat yhtä johdonmukaisia. EF-mäppäykset hoituivat hieman vähemmällä koodilla, koska metodeita pystyi ketjuttamaan. NH:n tapauksessa saman "fluent"-apin saa käyttämällä Fluent NHibernatea. Tässä vielä pari kokonaista POCO-luokkaa ja niiden mäppäykset.
``` csharp
public interface IEntity
{
int Id { get; set; }
DateTime ModifiedDate { get; set; }
}
public abstract class Entity : IEntity
{
public virtual int Id { get; set; }
public virtual DateTime ModifiedDate { get; set; }
}
public class Category : Entity
{
public virtual Category Parent { get; set; }
public virtual ICollection Children { get; set; }
public virtual string Name { get; set; }
public virtual ICollection Products { get; set; }
}
public class Product : Entity
{
public virtual string Name { get; set; }
public virtual string ProductNumber { get; set; }
public virtual string Color { get; set; }
public virtual decimal StandardCost { get; set; }
public virtual decimal ListPrice { get; set; }
public virtual string Size { get; set; }
public virtual decimal? Weight { get; set; }
public virtual Category Category { get; set; }
public virtual DateTime SellStartDate { get; set; }
public virtual DateTime? SellEndDate { get; set; }
public virtual DateTime? DiscontinuedDate { get; set; }
public virtual string ThumbNailPhotoFileName { get; set; }
}
// NHibernate
public class CategoryMapping : ClassMapping
{
public CategoryMapping()
{
Schema("SalesLT");
Table("ProductCategory");
Id(c => c.Id, m => m.Column("ProductCategoryID"));
ManyToOne(c => c.Parent, m =>
{
m.Column("ParentProductCategoryID");
m.NotNullable(false);
});
Bag(c => c.Children, m =>
{
m.Key(k => k.Column("ParentProductCategoryID"));
},
r => r.OneToMany());
Property(c => c.Name, m =>
{
m.Length(50);
m.NotNullable(true);
});
Property(c => c.ModifiedDate);
Bag(c => c.Products, m =>
{
m.Key(k => k.Column("ProductCategoryID"));
}, r => r.OneToMany());
}
}
public class ProductMapping : ClassMapping
{
public ProductMapping()
{
Schema("SalesLT");
Table("Product");
Id(c => c.Id, m => m.Column("ProductID"));
Property(p => p.Name, m =>
{
m.Length(50);
m.NotNullable(true);
});
Property(p => p.ProductNumber, m =>
{
m.Length(25);
m.NotNullable(true);
});
Property(p => p.Color, m =>
{
m.Length(15);
m.NotNullable(false);
});
Property(p => p.StandardCost, m =>
{
m.Column(c => c.SqlType("money"));
m.NotNullable(true);
});
Property(p => p.ListPrice, m =>
{
m.Column(c => c.SqlType("money"));
m.NotNullable(true);
});
Property(p => p.Size, m =>
{
m.Length(5);
m.NotNullable(false);
});
Property(p => p.Weight);
ManyToOne(p => p.Category, m =>
{
m.Column("ProductCategoryID");
m.NotNullable(false);
});
Property(p => p.SellStartDate);
Property(p => p.SellEndDate);
Property(p => p.DiscontinuedDate);
Property(p => p.ThumbNailPhotoFileName, m =>
{
m.Length(50);
m.NotNullable(false);
});
Property(p => p.ModifiedDate);
}
}
// Entity Framework
public class CategoryConfiguration : EntityTypeConfiguration
{
public CategoryConfiguration()
{
ToTable("ProductCategory", "SalesLT");
HasKey(c => c.Id);
Property(c => c.Id).HasColumnName("ProductCategoryID");
HasOptional(c => c.Parent).WithMany(c => c.Children).Map(m => m.MapKey("ParentProductCategoryID"));
HasMany(c => c.Children).WithOptional(c => c.Parent).Map(m => m.MapKey("ParentProductCategoryID"));
Property(c => c.Name).HasMaxLength(50).IsRequired();
Property(c => c.ModifiedDate);
HasMany(c => c.Products).WithOptional(p => p.Category).Map(m => m.MapKey("ProductCategoryID"));
}
}
public class ProductConfiguration : EntityTypeConfiguration
{
public ProductConfiguration()
{
ToTable("Product", "SalesLT");
HasKey(p => p.Id);
Property(p => p.Id).HasColumnName("ProductID");
Property(p => p.Name).HasMaxLength(50).IsRequired();
Property(p => p.ProductNumber).HasMaxLength(25).IsRequired();
Property(p => p.Color).HasMaxLength(15).IsOptional();
Property(p => p.StandardCost);
Property(p => p.ListPrice);
Property(p => p.Size).HasMaxLength(5).IsRequired();
Property(p => p.Weight);
HasOptional(p => p.Category).WithMany(c => c.Products).Map(m => m.MapKey("ProductCategoryID"));
Property(p => p.SellStartDate);
Property(p => p.SellEndDate);
Property(p => p.DiscontinuedDate);
Property(p => p.ThumbNailPhotoFileName).HasMaxLength(50).IsOptional();
Property(p => p.ModifiedDate);
}
}
```
Konffaus
NHibernate
NHibernaten konffausta pidin joskus vaikeana, mutta aika simppeli tuo perussetti on. Eipä sitä ulkoa muista, mutta copy-paste toimii.
(web.config/app.config).
``` xml
NHibernate.Connection.DriverConnectionProviderNHibernate.Driver.SqlClientDriverOrmComparisonNHibernate.Dialect.MsSql2008Dialect
```
Koodin puolellakin niin simppeliä
``` csharp
var mapper = new ModelMapper();
mapper.AddMapping();
mapper.AddMapping();
var cfg = new Configuration();
cfg.AddMapping(mapper.CompileMappingForAllExplicitlyAddedEntities());
var sessionFactory = cfg.BuildSessionFactory();
```
Sitten vain hakemaan dataa
``` csharp
using(var session = sessionFactory.OpenSession())
{
var products = session.Query().Where(p => p.Price < 100m);
// no limits baby
var entities = session.Query().Where(e => e.Id < 10);
}
```
Entity Framework
Entity frameworkin kaikki säädöt menee DbContext-luokkaan
``` csharp
public class AdventureWorksContext : DbContext
{
public DbSet Products { get; set; }
public DbSet Categories { get; set; }
public AdventureWorksContext() : base("Name=OrmComparison")
{
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Configurations.Add(new CategoryConfiguration());
modelBuilder.Configurations.Add(new ProductConfiguration());
}
}
```
Ja dataa haetaan
``` csharp
var context = new AdventureWorksContext();
var products = context.Products.Where(p => p.Price < 100m);
```
Suorituskyky
Hain vähän dataa molemmilla kirjastoilla käyttäen LINQ:ta ja seuraavassa yhteenvetoja. NH pärjäsi paremmin, kun haettiin objekteja pelkästään id:n avulla. Muissa hauissa EF pärjäsi paremmin. Include/Fetch hidasti NH:tä vähemmän kuin EF:ää. Erot olivat kuitenkin mitättömiä eikä noilla ole käytännön merkitystä. Insert/update/delete -operaatioissa NH voittaa kirkkaasti, koska se käyttää batchejä ja EF ei. NH on kymmeniä, jopa satoja, kertoja nopeampi riippuen batchin koosta.
Ominaisuudet, joissa NHibernate rokkaa edelleen
Event listener
NHibernatessa on useita tapahtumia, joihin voi liittää omia käsittelijöitä. Voit esim. muuttaa objekteja juuri ennen niiden tallentamista tietokantaan tai juuri ennen kuin objektit palautetaan käyttäjälle. EF:ssä on yksi ainoa metodi (SaveChanges), joka edes etäisesti muistuttaa samaa.
User type
Voit laittaa propertyn
``` csharp
int[]
```
tietokantaan muodossa
```
1,2,3,4,5
```
ja antaa NHibernaten tehdä muunnoksen automaattisesti aina objektia ladatessa. Voit myös mm. serialisoida objekteja json:iksi, xml:ksi jne. EF:ssä ei ole mitään vastaavaa.
2nd level cache
Objektit laitetaan molemmissa välimuistiin, mutta NH:ssa voi välimuistiin laittaa myös kyselyiden tulokset.
Lazy load
NH:ssa minkä tahansa propertyn voi laittaa lazyload-tilaan (esim. pitkä teksti tai binääridata). EF:ssä ainoastaan kokoelmat ja referenssit (navigation property) voivat olla lazyload-tilassa.
Kokoelmat
NH:ssa on useampia tapoja mäpätä kokoelmia. Esim. listan alkioilla on oikeasti järjestys. EF:hän ei tuota takaa.
Ei hardkoodattuja kokoelmia
NH:lla kyselyitä voi tehdä mitä tahansa luokkaa/interfacea vasten. EF:ssä joudut tekemään propertyn (DbSet) jokaista kokoelmaa varten erikseen. Esim. seuraavat toimivat vallan mainiosti ja esimerkkitapauksessamme palauttaisi sekä Product- että Category-tyyppiset objektit samalla kertaa.
``` csharp
session.Query().Where(e => e.Id < 10);
session.Query().Where(e => e.Id < 10);
```
Omat (LINQ)-operaatiot
NH:ssä voit tehdä generaattoreita, jotka kertovat kuinka jokin metodi muutetaan SQL:ksi. Esim. laajennusmetodi Like
``` csharp
session.Query().Where(p => p.Title.Like("%NHibernate%"))
```
joka tuottaisi sql:n
```
Title LIKE '%NHibernate%'
```
Asiat, joissa Entity Framework on parempi
Entity Designer
Jos haluaa nopeasti viritellä ORM:n olemassa olevaan tietokantaan, on Visual Studion graafinen työkalu nopein tapa. Syntyvä koodi on rumaa, mutta demo yms. projekteihin, joita käytetään vain hetki ja sitten heitetään menemään, tuo on ihan hyvä ratkaisu.
(perinteisen) ASP.NET:n komponentit
Esim. DataGridiin voi näpsäkästi laittaa EntityDataSourcen. Tosin samat periaatteet kuin edellisessä kohdassa; ei kai kukaan näitä oikeisiin projekteihin laita. Ja suunta on (onneksi) kovasti ASP.NET MVC:hen.
Yhteenveto
Demo- ja vastaaviin projekteihin, jotka pitää tehdä nopeasti ja sitten heitetään menemään, käytä toki Entity Frameworkiä. Entity Designerilla olemassa oleva tietokanta saadaan nopeasti koodiin käytettäväksi.
Oikeisiin töihin, joissa vaaditaan monimutkaisia ratkaisuja ja paljon säätömahdollisuuksia, käytä NHibernatea. NH:n säätömahdollisuudet ovat vuosia EF:ää edellä. Peruskonffaus ei vaadi älyttömästi paneutumista ja LINQ toimii, kuten EF:ssäkin.
Nyt kun EF on open sourcea, voi tilanne muuttua nopeastikin, mutta sitä odotellessa...