I have an N+1 problem and I'm not sure how to solve it.
A fully-reproducible sample may be found at the bottom of this question. So if you are willing, please create the database, set up the NUnit test and all the accompanying classes, and try to eliminate the N+1 locally. This is the anonymized version of a real problem I encountered. For all you know, this code is crucial in helping launch the next space shuttle to the moon. I won't deny it if asked.
To summarize the problem: I am trying to query a table structure that is drawn below. The only weird thing to note about this table structure is that questions have choices, which then have sub-questions, which then have sub-choices. You can assume only 2 levels of question->choice->question->choice.
SiteSurveyQuestion
|
+---Site
|
+---Survey
|
+---Question
|
+---Choice
+---Choice
+---Choice
|
+---Question
+---Question
+---Question
|
+---Choice
+---Choice
+---Choice
I've tried everything I know to try.
In the mappings, I have tried a bunch of referencing fields as .Not.LazyLoad()
to no real success.
I have also tried modifying the query by adding many combinations of .Fetch()
and .FetchMany()
and .ThenFetchMany()
and even tried running multiple .ToFuture()
queries. These do make real changes to the SQL query, but not the final result I'm looking for.
The query as it is boiled down, is "get me a list of all questions for this survey on this site, including all sub-questions". Here is the query:
using (var session = sessionFactory.OpenSession())
{
var questionsForSurvey = session.Query<SiteSurveyQuestion>()
.Where(x => x.Site.Id == 1 && x.Survey.Id == 1)
.ToArray();
}
So to finally ask the question: how can I fix this N+1 problem? I would be happy with any of the following
I do not want an HQL solution because I won't learn anything about what I'm doing wrong with my mapping and/or querying - I feel like I'm missing something fundamental, and I don't even know where to look.
Sample instructions:
```
Site: Site1 Survey: SurveyAboutCats Q: Own A Cat?
o Yes
Q: How many cats did you feed yesterday?
o 1
o 2-5
o 6-10
o 11-20
o 20+
o 100+
Q: How much do you spend on cats annually?
o 0-100
o 100-500
o 500-2000
o 2000+
o No
Q: No cats? What is wrong with you?
o I am sorry
Site: Site1 Survey: SurveyAboutCats Q: Own A Dog?
o Yes
o No
Full sample:
/*
Nuget packages:
<package id="FluentNHibernate" version="1.3.0.733" targetFramework="net40" />
<package id="NHibernate" version="3.3.3.4001" targetFramework="net45" />
<package id="NUnit" version="2.6.2" targetFramework="net40" />
*/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using FluentNHibernate.Cfg;
using FluentNHibernate.Cfg.Db;
using FluentNHibernate.Conventions.Helpers;
using FluentNHibernate.Mapping;
using NHibernate;
using NHibernate.Cfg;
using NHibernate.Linq;
using NUnit.Framework;
namespace StackOverflow.CryForHelp
{
[TestFixture]
public class NHibernateMappingTests
{
[Test]
public void ShouldMapEntitiesWithoutNPlusOneIssue()
{
//Arrange
var connectionString = "Data Source=(local);Initial Catalog=NinetyNineProblemsAndAnNPlusOne;Integrated Security=SSPI;";
Configuration applicationConfiguration = new Configuration();
applicationConfiguration.SetProperty("connection.provider", "NHibernate.Connection.DriverConnectionProvider");
applicationConfiguration.SetProperty("dialect", "NHibernate.Dialect.MsSql2008Dialect");
applicationConfiguration.SetProperty("connection.driver_class", "NHibernate.Driver.SqlClientDriver");
applicationConfiguration.SetProperty("default_schema", "dbo");
applicationConfiguration.SetProperty("format_sql", "format_sql");
applicationConfiguration.SetProperty("show_sql", "true");
applicationConfiguration.SetProperty("generate_statistics", "true");
applicationConfiguration.Configure();
Configuration fluentConfiguration = null;
ISessionFactory sessionFactory = Fluently.Configure(applicationConfiguration)
.Mappings(m =>
{
m.FluentMappings.Conventions.Setup(x => x.Add(AutoImport.Never()));
m.FluentMappings.AddFromAssembly(Assembly.GetAssembly(GetType()));
})
.ExposeConfiguration(c => fluentConfiguration = c)
.Database(MsSqlConfiguration.MsSql2008.ConnectionString(connectionString))
.BuildSessionFactory();
var mappings = fluentConfiguration.ClassMappings;
//Act + Assert that we please don't create N+1 queries
using (var session = sessionFactory.OpenSession())
{
var questionsForSurvey = session.Query<SiteSurveyQuestion>()
.Where(x => x.Site.Id == 1 && x.Survey.Id == 1)
.ToArray();
foreach (var question in questionsForSurvey)
{
Console.WriteLine("Site: {0} Survey: {1} Q: {2}", question.Site.Name, question.Survey.Name, question.Question.InternalName);
foreach (var choice in question.Question.Choices)
{
Console.WriteLine("\t> " + choice.InternalName);
foreach (var subQuestion in choice.Questions)
{
Console.WriteLine("\t\tQ: " + subQuestion.InternalName);
foreach (var subChoice in subQuestion.Choices)
Console.WriteLine("\t\t\t> " + subChoice.InternalName);
}
}
}
}
}
}
public class Site
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
}
public class Survey
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
}
public class SiteSurvey
{
public virtual Site Site { get; set; }
public virtual Survey Survey { get; set; }
public virtual string Status { get; set; }
public virtual string Name { get; set; }
public virtual bool Equals(SiteSurvey other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return Site.Id == other.Site.Id && Survey.Id == other.Survey.Id;
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != this.GetType()) return false;
return Equals((SiteSurvey) obj);
}
public override int GetHashCode()
{
unchecked
{
return (Survey.Id * 397) ^ Site.Id;
}
}
}
public class SiteSurveyQuestion
{
public virtual Site Site { get; set; }
public virtual Survey Survey { get; set; }
public virtual Question Question { get; set; }
public virtual bool IsActive { get; set; }
public virtual bool Equals(SiteSurveyQuestion other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return Site.Id == other.Site.Id && Survey.Id == other.Survey.Id && Question.Id == other.Question.Id;
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != this.GetType()) return false;
return Equals((SiteSurveyQuestion) obj);
}
public override int GetHashCode()
{
unchecked
{
return Question.Id ^ (((Survey.Id * 397) ^ Site.Id) * 397);
}
}
}
public class Question
{
public virtual int Id { get; set; }
public virtual string InternalName { get; set; }
public virtual bool IsActive { get; set; }
public virtual IEnumerable<Choice> Choices { get; set; }
}
public class Choice
{
public virtual int Id { get; set; }
public virtual string InternalName { get; set; }
public virtual bool IsActive { get; set; }
public virtual IEnumerable<Question> Questions { get; set; }
}
public class SurveyMap : ClassMap<Survey>
{
public SurveyMap()
{
Table("Surveys");
Id(x => x.Id, "SurveyId").GeneratedBy.Identity().UnsavedValue(0);
Map(x => x.Name).Not.Nullable();
}
}
public class SiteMap : ClassMap<Site>
{
public SiteMap()
{
Table("Sites");
Id(x => x.Id, "SiteId").GeneratedBy.Identity().UnsavedValue(0);
Map(x => x.Name, "Name").Not.Nullable();
}
}
public class SiteSurveyMap : ClassMap<SiteSurvey>
{
public SiteSurveyMap()
{
Table("SiteSurveys");
CompositeId()
.KeyReference(x => x.Site, "SiteId")
.KeyReference(x => x.Survey, "SurveyId");
Map(x => x.Status).Not.Nullable();
Map(x => x.Name).Not.Nullable();
}
}
public class SiteSurveyQuestionMap : ClassMap<SiteSurveyQuestion>
{
public SiteSurveyQuestionMap()
{
Table("SiteSurveyQuestions");
CompositeId()
.KeyReference(x => x.Site, "SiteId")
.KeyReference(x => x.Survey, "SurveyId")
.KeyReference(x => x.Question, "QuestionId");
Map(x => x.IsActive, "ActiveFlag").Not.Nullable();
}
}
public class QuestionMap : ClassMap<Question>
{
public QuestionMap()
{
Table("Questions");
Id(x => x.Id, "QuestionId").GeneratedBy.Identity().UnsavedValue(0);
Map(x => x.InternalName);
Map(x => x.IsActive, "ActiveFlag");
HasMany(x => x.Choices).KeyColumn("QuestionId").AsBag().Cascade.AllDeleteOrphan().Inverse().Not.LazyLoad();
}
}
public class ChoiceMap : ClassMap<Choice>
{
public ChoiceMap()
{
Table("Choices");
Id(x => x.Id, "ChoiceId").GeneratedBy.Identity().UnsavedValue(0);
Map(x => x.InternalName);
Map(x => x.IsActive, "ActiveFlag");
HasMany(x => x.Questions)
.KeyColumn("ChoiceId")
.AsBag()
.Cascade
.AllDeleteOrphan()
.Inverse();
}
}
}
/*
use [master]
GO
CREATE DATABASE [NinetyNineProblemsAndAnNPlusOne]
GO
USE [NinetyNineProblemsAndAnNPlusOne]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
SET ANSI_PADDING ON
GO
CREATE TABLE [dbo].[Sites](
[SiteId] [int] IDENTITY(1,1) NOT NULL,
[Name] [varchar](100) NOT NULL,
CONSTRAINT [XPKSites] PRIMARY KEY CLUSTERED
(
[SiteId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY],
CONSTRAINT [XAK1Sites] UNIQUE NONCLUSTERED
(
[Name] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
CREATE TABLE [dbo].[Surveys](
[SurveyId] [int] IDENTITY(1,1) NOT NULL,
[Name] [varchar](500) NOT NULL,
[Status] [varchar](12) NOT NULL,
[SurveyTypeId] [int] NOT NULL,
CONSTRAINT [XPKSurveys] PRIMARY KEY CLUSTERED
(
[SurveyId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY],
CONSTRAINT [XAK1Surveys] UNIQUE NONCLUSTERED
(
[Name] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
CREATE TABLE [dbo].[SiteSurveys](
[SiteId] [int] NOT NULL,
[SurveyId] [int] NOT NULL,
[Name] [varchar](500) NOT NULL,
[Status] [varchar](12) NOT NULL,
CONSTRAINT [XPKSiteSurveys] PRIMARY KEY CLUSTERED
(
[SiteId] ASC,
[SurveyId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY],
CONSTRAINT [XAK1SiteSurveys] UNIQUE NONCLUSTERED
(
[SiteId] ASC,
[SurveyId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY],
CONSTRAINT [XAK2SiteSurveys] UNIQUE NONCLUSTERED
(
[SiteId] ASC,
[Name] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
CREATE TABLE [dbo].[SiteSurveyQuestions](
[SiteId] [int] NOT NULL,
[SurveyId] [int] NOT NULL,
[QuestionId] [int] NOT NULL,
[SurveyQuestionTypeId] [int] NULL,
[ActiveFlag] [bit] NOT NULL,
[IsRequired] [bit] NOT NULL,
CONSTRAINT [XPKSurveyQuestions] PRIMARY KEY CLUSTERED
(
[SurveyId] ASC,
[QuestionId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
CREATE TABLE [dbo].[Questions](
[QuestionId] [int] IDENTITY(1,1) NOT NULL,
[InternalName] [varchar](100) NOT NULL,
[ChoiceId] [int] NULL,
[ActiveFlag] [bit] NOT NULL,
CONSTRAINT [XPKQuestions] PRIMARY KEY CLUSTERED
(
[QuestionId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY],
CONSTRAINT [XAK1QuestionsInternalName] UNIQUE NONCLUSTERED
(
[InternalName] ASC,
[ChoiceId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
CREATE TABLE [dbo].[Choices](
[ChoiceId] [int] IDENTITY(1,1) NOT NULL,
[QuestionId] [int] NOT NULL,
[InternalName] [varchar](100) NOT NULL,
[ActiveFlag] [bit] NOT NULL,
CONSTRAINT [XPKChoices] PRIMARY KEY CLUSTERED
(
[ChoiceId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY],
CONSTRAINT [XAKChoiceIdQuestionId] UNIQUE NONCLUSTERED
(
[ChoiceId] ASC,
[QuestionId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY],
CONSTRAINT [XAKChoiceInternalName] UNIQUE NONCLUSTERED
(
[QuestionId] ASC,
[InternalName] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
ALTER TABLE [dbo].[Surveys] WITH CHECK ADD CONSTRAINT [VRSurveyStatuses_Surveys] CHECK (([Status]='Live' OR [Status]='NotLive' OR [Status]='Discontinued'))
GO
ALTER TABLE [dbo].[Surveys] CHECK CONSTRAINT [VRSurveyStatuses_Surveys]
GO
ALTER TABLE [dbo].[SiteSurveys] WITH CHECK ADD CONSTRAINT [R289] FOREIGN KEY([SurveyId])
REFERENCES [dbo].[Surveys] ([SurveyId])
GO
ALTER TABLE [dbo].[SiteSurveys] CHECK CONSTRAINT [R289]
GO
ALTER TABLE [dbo].[SiteSurveys] WITH CHECK ADD CONSTRAINT [R303] FOREIGN KEY([SiteId])
REFERENCES [dbo].[Sites] ([SiteId])
GO
ALTER TABLE [dbo].[SiteSurveys] CHECK CONSTRAINT [R303]
GO
ALTER TABLE [dbo].[SiteSurveys] WITH CHECK ADD CONSTRAINT [VRSurveyStatuses_SiteSurveys] CHECK (([Status]='Live' OR [Status]='NotLive' OR [Status]='Discontinued'))
GO
ALTER TABLE [dbo].[SiteSurveys] CHECK CONSTRAINT [VRSurveyStatuses_SiteSurveys]
GO
ALTER TABLE [dbo].[SiteSurveyQuestions] WITH CHECK ADD CONSTRAINT [QuestionsToSurveyQuestions] FOREIGN KEY([QuestionId])
REFERENCES [dbo].[Questions] ([QuestionId])
GO
ALTER TABLE [dbo].[SiteSurveyQuestions] CHECK CONSTRAINT [QuestionsToSurveyQuestions]
GO
ALTER TABLE [dbo].[SiteSurveyQuestions] WITH CHECK ADD CONSTRAINT [SurveysToSurveyQuestions] FOREIGN KEY([SurveyId])
REFERENCES [dbo].[Surveys] ([SurveyId])
GO
ALTER TABLE [dbo].[SiteSurveyQuestions] CHECK CONSTRAINT [SurveysToSurveyQuestions]
GO
ALTER TABLE [dbo].[Questions] WITH CHECK ADD CONSTRAINT [R409] FOREIGN KEY([ChoiceId])
REFERENCES [dbo].[Choices] ([ChoiceId])
GO
ALTER TABLE [dbo].[Choices] WITH CHECK ADD CONSTRAINT [R408] FOREIGN KEY([QuestionId])
REFERENCES [dbo].[Questions] ([QuestionId])
GO
ALTER TABLE [dbo].[Choices] CHECK CONSTRAINT [R408]
GO
SET ANSI_PADDING OFF
GO
GO
SET IDENTITY_INSERT [dbo].[Sites] ON
INSERT [dbo].[Sites] ([SiteId], [Name]) VALUES (1, N'Site1')
INSERT [dbo].[Sites] ([SiteId], [Name]) VALUES (2, N'Site2')
SET IDENTITY_INSERT [dbo].[Sites] OFF
SET IDENTITY_INSERT [dbo].[Surveys] ON
INSERT [dbo].[Surveys] ([SurveyId], [Name], [Status], [SurveyTypeId]) VALUES (1, N'SurveyAboutCats', N'Live', 0)
INSERT [dbo].[Surveys] ([SurveyId], [Name], [Status], [SurveyTypeId]) VALUES (2, N'Crime Survey', N'Live', 0)
SET IDENTITY_INSERT [dbo].[Surveys] OFF
SET IDENTITY_INSERT [dbo].[Questions] ON
INSERT [dbo].[Questions] ([QuestionId], [InternalName], [ChoiceId], [ActiveFlag]) VALUES (1, N'Own A Cat?', NULL, 1)
INSERT [dbo].[Questions] ([QuestionId], [InternalName], [ChoiceId], [ActiveFlag]) VALUES (2, N'Own A Dog?', NULL, 1)
INSERT [dbo].[Questions] ([QuestionId], [InternalName], [ChoiceId], [ActiveFlag]) VALUES (3, N'Witnessed any crimes recently?', NULL, 1)
INSERT [dbo].[Questions] ([QuestionId], [InternalName], [ChoiceId], [ActiveFlag]) VALUES (4, N'Committed any crimes yourself recently?', NULL, 1)
SET IDENTITY_INSERT [dbo].[Questions] OFF
SET IDENTITY_INSERT [dbo].[Choices] ON
INSERT [dbo].[Choices] ([ChoiceId], [QuestionId], [InternalName], [ActiveFlag]) VALUES (1, 1, N'Yes', 1)
INSERT [dbo].[Choices] ([ChoiceId], [QuestionId], [InternalName], [ActiveFlag]) VALUES (2, 1, N'No', 1)
INSERT [dbo].[Choices] ([ChoiceId], [QuestionId], [InternalName], [ActiveFlag]) VALUES (3, 2, N'Yes', 1)
INSERT [dbo].[Choices] ([ChoiceId], [QuestionId], [InternalName], [ActiveFlag]) VALUES (4, 2, N'No', 1)
INSERT [dbo].[Choices] ([ChoiceId], [QuestionId], [InternalName], [ActiveFlag]) VALUES (5, 3, N'Yes', 1)
INSERT [dbo].[Choices] ([ChoiceId], [QuestionId], [InternalName], [ActiveFlag]) VALUES (6, 3, N'Yes but I ain''t no snitch', 1)
INSERT [dbo].[Choices] ([ChoiceId], [QuestionId], [InternalName], [ActiveFlag]) VALUES (7, 4, N'No', 1)
INSERT [dbo].[Choices] ([ChoiceId], [QuestionId], [InternalName], [ActiveFlag]) VALUES (8, 4, N'I plead the fifth', 1)
SET IDENTITY_INSERT [dbo].[Choices] OFF
SET IDENTITY_INSERT [dbo].[Questions] ON
INSERT [dbo].[Questions] ([QuestionId], [InternalName], [ChoiceId], [ActiveFlag]) VALUES (6, N'No cats? What is wrong with you?', 2, 1)
INSERT [dbo].[Questions] ([QuestionId], [InternalName], [ChoiceId], [ActiveFlag]) VALUES (7, N'How many cats did you feed yesterday?', 1, 1)
INSERT [dbo].[Questions] ([QuestionId], [InternalName], [ChoiceId], [ActiveFlag]) VALUES (8, N'How much do you spend on cats annually?', 1, 1)
SET IDENTITY_INSERT [dbo].[Questions] OFF
SET IDENTITY_INSERT [dbo].[Choices] ON
INSERT [dbo].[Choices] ([ChoiceId], [QuestionId], [InternalName], [ActiveFlag]) VALUES (9, 6, N'I am sorry', 1)
INSERT [dbo].[Choices] ([ChoiceId], [QuestionId], [InternalName], [ActiveFlag]) VALUES (10, 7, N'1', 1)
INSERT [dbo].[Choices] ([ChoiceId], [QuestionId], [InternalName], [ActiveFlag]) VALUES (11, 7, N'2-5', 1)
INSERT [dbo].[Choices] ([ChoiceId], [QuestionId], [InternalName], [ActiveFlag]) VALUES (12, 7, N'6-10', 1)
INSERT [dbo].[Choices] ([ChoiceId], [QuestionId], [InternalName], [ActiveFlag]) VALUES (13, 7, N'11-20', 1)
INSERT [dbo].[Choices] ([ChoiceId], [QuestionId], [InternalName], [ActiveFlag]) VALUES (14, 7, N'20+', 1)
INSERT [dbo].[Choices] ([ChoiceId], [QuestionId], [InternalName], [ActiveFlag]) VALUES (15, 7, N'100+', 1)
INSERT [dbo].[Choices] ([ChoiceId], [QuestionId], [InternalName], [ActiveFlag]) VALUES (16, 8, N'0-100', 1)
INSERT [dbo].[Choices] ([ChoiceId], [QuestionId], [InternalName], [ActiveFlag]) VALUES (17, 8, N'100-500', 1)
INSERT [dbo].[Choices] ([ChoiceId], [QuestionId], [InternalName], [ActiveFlag]) VALUES (18, 8, N'500-2000', 1)
INSERT [dbo].[Choices] ([ChoiceId], [QuestionId], [InternalName], [ActiveFlag]) VALUES (19, 8, N'2000+', 1)
SET IDENTITY_INSERT [dbo].[Choices] OFF
INSERT [dbo].[SiteSurveys] ([SiteId], [SurveyId], [Name], [Status]) VALUES (1, 1, N'Site #1 Cat Survey', N'Live')
INSERT [dbo].[SiteSurveys] ([SiteId], [SurveyId], [Name], [Status]) VALUES (1, 2, N'Site #1 Crime Survey', N'Live')
INSERT [dbo].[SiteSurveys] ([SiteId], [SurveyId], [Name], [Status]) VALUES (2, 1, N'Site #2 Cat Survey', N'Live')
INSERT [dbo].[SiteSurveys] ([SiteId], [SurveyId], [Name], [Status]) VALUES (2, 2, N'Site #2 Crime Survey', N'Live')
INSERT [dbo].[SiteSurveyQuestions] ([SiteId], [SurveyId], [QuestionId], [SurveyQuestionTypeId], [ActiveFlag], [IsRequired]) VALUES (1, 1, 1, 0, 1, 0)
INSERT [dbo].[SiteSurveyQuestions] ([SiteId], [SurveyId], [QuestionId], [SurveyQuestionTypeId], [ActiveFlag], [IsRequired]) VALUES (1, 1, 2, 0, 1, 0)
GO
USE [master]
GO
-- */
The solution to 1 + N would've been built on top of a special NHibernate optimization feature (let me cite a bit)
NHibernate can make efficient use of batch fetching, that is, NHibernate can load several uninitialized proxies if one proxy is accessed (or collections. Batch fetching is an optimization of the lazy select fetching strategy. There are two ways you can tune batch fetching: on the class and the collection level.
Batch fetching for classes/entities is easier to understand. Imagine you have the following situation at runtime: You have 25 Cat instances loaded in an ISession, each Cat has a reference to its Owner, a Person. The Person class is mapped with a proxy, lazy="true". If you now iterate through all cats and call cat.Owner on each, NHibernate will by default execute 25 SELECT statements, to retrieve the proxied owners. You can tune this behavior by specifying a batch-size in the mapping of Person:
<class name="Person" batch-size="10">...</class>
NHibernate will now execute only three queries, the pattern is 10, 10, 5.
You may also enable batch fetching of collections. For example, if each Person has a lazy collection of Cats, and 10 persons are currently loaded in the ISesssion, iterating through all persons will generate 10 SELECTs, one for every call to person.Cats. If you enable batch fetching for the Cats collection in the mapping of Person, NHibernate can pre-fetch collections:
<class name="Person">
<set name="Cats" batch-size="3">
...
</set>
</class>
With a batch-size of 3, NHibernate will load 3, 3, 3, 1 collections in four SELECTs. Again, the value of the attribute depends on the expected number of uninitialized collections in a particular Session.
So, that is the DOC. The great on this solution is, that we will have simple queries, and optimization inside of the mapping.
In practice it means, that almost any one-to-many
and entity mapping should contain BatchSize(25)
(or 50 or 100... play with to find out what suites to you)
To illustrate that, I adjusted one of the mappings above
public QuestionMap()
{
Table("Questions");
// here, load this in batches by 25
BatchSize(25);
Id(x => x.Id, "QuestionId").GeneratedBy.Identity().UnsavedValue(0);
Map(x => x.InternalName);
Map(x => x.IsActive, "ActiveFlag");
HasMany(x => x.Choices)
.KeyColumn("QuestionId")
.AsBag()
.Cascade
.AllDeleteOrphan()
.Inverse()
// here again
.BatchSize(25)
.Not.LazyLoad();
}
Next step, would depend on the life time of the session. If we will use using(var session...){}
we are in troubles. The above stuff won't work - outside of the session. All that must be populated via the session. So how to solve it?
The best would be to append some method to iterate though objects and convert them into some "DTO"
using (var session = sessionFactory.OpenSession())
{
var questionsForSurvey = session.Query<SiteSurveyQuestion>()
.Where(x => x.Site.Id == 1 && x.Survey.Id == 1)
.ToArray();
var result = new List<SiteSurveyQuestionDTO>();
foreach(var s in questionsForSurvey)
{
// here we can touch all the inner properties and collections
// so NHibernate will load all needed data in batches
var dto = s.doSomething();
result.Add(dto);
}
}
My preferred way would be to implement IClonable
and inside of the .Clone()
touche what is needed
using (var session = sessionFactory.OpenSession())
{
var questionsForSurvey = session.Query<SiteSurveyQuestion>()
.Where(x => x.Site.Id == 1 && x.Survey.Id == 1)
.ToArray()
.Select(s => s.Clone() as SiteSurveyQuestion);
}
Check the Prototype pattern. Some more stuff about life cycle. And also, some more about batch fetching
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With