fbpx
devstyle.pl - Blog dla każdego programisty
devstyle.pl - Blog dla każdego programisty
6 minut

AutoMapper, NHibernate, lazy loading oraz problem select n+1


06.11.2009

Ostatnio pisałem o AutoMapperze, a kiedyś wcześniej o NHibernate. Dzisiaj złączę te dwa narzędzia niczym Jasia i Małgosię, Tristana i Izoldę, Lecha i Jarosława, a z ich nawzajemnego obcowania narodzi się problem, który dość łatwo przegapić.

Wróćmy do przedstawionych ostatnio, banalnych klas:

  1:  public class User
  2:  {
  3:  	public virtual int Id { get; set; }
  4:  	public virtual string Name { get; set; }
  5:  	public virtual IList<Address> Addresses { get; set; }
  6:  
  7:  	public User()
  8:  	{
  9:  		Addresses = new List<Address>();
 10:  	}
 11:  }
 12:  
 13:  public class Address
 14:  {
 15:  	public virtual int Id { get; set; }
 16:  	public virtual string Street { get; set; }
 17:  	public virtual string City { get; set; }
 18:  	public virtual string Country { get; set; }
 19:  }

Ich mapowań z wykorzystaniem Fluent NHibernate:

  1:  public class UserMap : ClassMap<User>
  2:  {
  3:  	public UserMap()
  4:  	{
  5:  		Id(x => x.Id);
  6:  		Map(x => x.Name);
  7:  		HasMany(x => x.Addresses).Inverse().Cascade.All();
  8:  	}
  9:  }
 10:  
 11:  public class AddressMap : ClassMap<Address>
 12:  {
 13:  	public AddressMap()
 14:  	{
 15:  		Id(x => x.Id);
 16:  		Map(x => x.Street);
 17:  		Map(x => x.City);
 18:  		Map(x => x.Country);
 19:  	}
 20:  }

Reprezentacji DTO:

  1:  [DataContract]
  2:  public class UserDto
  3:  {
  4:  	[DataMember]
  5:  	public int Id { get; set; }
  6:  	[DataMember]
  7:  	public string Name { get; set; }
  8:  	[DataMember]
  9:  	public AddressDto[] Addresses { get; set; }
 10:  }
 11:  
 12:  [DataContract]
 13:  public class AddressDto
 14:  {
 15:  	[DataMember]
 16:  	public int Id { get; set; }
 17:  	[DataMember]
 18:  	public string Street { get; set; }
 19:  	[DataMember]
 20:  	public string City { get; set; }
 21:  	[DataMember]
 22:  	public string Country { get; set; }
 23:  }

Oraz konfiguracji AutoMappera:

  1:  Mapper.CreateMap<User, UserDto>();
  2:  Mapper.CreateMap<Address, AddressDto>();

Raczej nic nie powinno wymagać tu wyjaśnienia (o Fluent-NH zamierzam wkrótce napisać trochę więcej).

Teraz kontekst:

Jakiś klient żąda od nas listy wszystkich użytkowników do wyświetlenia… gdzieś tam. Potrzebuje jedynie nazwy użytkownika oraz jego ID, adresy go nie obchodzą. Zadowoleni pobieramy więc samych użytkowników bez adresów:

  1:  UserDto[] dtos = null;
  2:  using (var session = AppFacade.DataAccess.OpenSession())
  3:  {
  4:  	User[] users = session.Linq<User>().ToArray();
  5:  	dtos = Mapper.Map<User[], UserDto[]>(users);
  6:  }
  7:  //...

Ależ z nas optymalizatory! Nie potrzebujemy adresów, więc ich nie pobieramy! A przecież równie dobrze mogło to dotyczyć o wiele bardziej obciążających bazę i łącze danych. Za tak wyrafinowane rozwiązanie należy nam się browar. Czy aby na pewno?

Na wszelki wypadek podejrzymy wygenerowany przez NHibernate SQL:

U’la’la. Nie dość, że pobieramy z bazy adresy, to jeszcze wpadliśmy w pułapkę "select n+1". A przecież mapowanie NH nie mówiło nic o "eager loading". Oj, nie na browar zasłużyliśmy, a co najwyżej na klapsa. I to bez niespodzianki.

Wtrącenie: w teorii powinno się mieć osobny obiekt DTO dla każdej operacji… ale często można pójść na skróty i zdefiniować jeden wykorzystywany w kilku miejscach. Moim zdaniem nie ma w tym nic szczególnie zdrożnego, należy po prostu uważać i być świadomym tego co się robi. W tym przypadku poszedłem na skróty i zdefiniowałem UserDto zamiast np. UserDto i UserWithAddressesDto.

Powód takiego a nie innego zapytania jest prosty, wystarczy lekko zmodyfikować kod aby się domyślić:

  1:  User[] users;
  2:  using (var session = AppFacade.DataAccess.OpenSession())
  3:  {
  4:  	users = session.Linq<User>().ToArray();
  5:  }
  6:  UserDto[] dtos = Mapper.Map<User[], UserDto[]>(users);

Takie coś będzie skutkowało NHibernate.LazyInitializationException w linijce 6, czyli podczas mapowania. I… prawidłowo! AutoMapper chce dobrać się do kolekcji adresów i przepisać je do DTO. NHibernate próbuje w tym momencie załadować adresy z bazy, a że jesteśmy poza sesją – wyrzuca wyjątek. Poprzednio cały czas byliśmy w obszarze aktywności sesji i adresy były leniwie pobierane dla każdego użytkownika po kolei.

Ale samo przedstawienie problemu to tylko połowa fajności pisania postów, czas na rozwiązanie.

Napisałem klasę dziedziczącą z AutoMapper.ValueResolver, która będzie próbowała dostać się do wartości źródłowej tylko i wyłącznie wtedy, gdy ta zostanie uprzednio zainicjowana przez NHibernate. W przeciwnym wypadku w miejsce docelowe wpisany zostanie null. Oto i ona:

  1:  public class NHibernateResolver : ValueResolver<object, object>
  2:  {
  3:  	protected override object ResolveCore(object source)
  4:  	{
  5:  		if (NHibernateUtil.IsInitialized(source))
  6:  			return source;
  7:  		return null;
  8:  	}
  9:  }

Oczywiście trzeba odpowiednio poinstruować AutoMappera, aby wiedział że ma z tego mechanizmu skorzystać:

  1:  Mapper.CreateMap<User, UserDto>()
  2:  	.ForMember(dest => dest.Addresses,
  3:  	opts => opts.ResolveUsing<NHibernateResolver>().FromMember(src => src.Addresses));

I wszystko jest już jak być powinno, przyjemnie i "reużywalnie":

Wygraliśmy browar!

Tip: do testowania podobnych rzeczy wykorzystuje się klasę NHibernateUtil (zastosowaną zresztą w prezentowanym rozwiązaniu).

Comments are closed.

Kurs Gita

Zaawansowany frontend

Szkolenie z Testów

Szkolenie z baz danych

Książka

Zobacz również