简体   繁体   English

通过没有N + 1的NHibernate查询-包括示例

[英]Querying via NHibernate without an N+1 - sample included

I have an N+1 problem and I'm not sure how to solve it. 我有一个N + 1问题,我不确定如何解决。

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. 因此,如果愿意,请创建数据库,设置NUnit测试和所有附带的类,并尝试在本地消除N + 1。 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. 在映射中,我尝试了一堆作为.Not.LazyLoad()的引用字段, .Not.LazyLoad()没有真正的成功。

I have also tried modifying the query by adding many combinations of .Fetch() and .FetchMany() and .ThenFetchMany() and even tried running multiple .ToFuture() queries. 我还尝试通过添加.Fetch().FetchMany()以及.ThenFetchMany()许多组合来修改查询,甚至尝试运行多个.ToFuture()查询。 These do make real changes to the SQL query, but not the final result I'm looking for. 这些确实对SQL查询做出了真正的改变,但是并没有改变我想要的最终结果。

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? 所以最后要问一个问题:如何解决这个N + 1问题? I would be happy with any of the following 我将对以下任何一项感到满意

  • (Preferred) A fix in the class mappings to eager load everything (首选)在类映射中的修复程序,它渴望加载所有内容
  • (2nd choice) Sprinking the Query with fetch's or query hints using the LINQ provider (第二选择)使用LINQ提供程序在查询中添加获取或查询提示
  • (3rd choice) mix of the above (第三选择)上面的组合
  • (4th choice) being told it's impossible and a limitation of NHibernate (第四选择)被告知这是不可能的,并且是NHibernate的局限性
  • (5th choice) Solution in HQL (第五选择)HQL解决方案

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. 我不想要HQL解决方案,因为我不会从映射和/或查询的错误中了解到任何信息- 我感觉好像缺少了一些基本信息 ,甚至不知道在哪里查找。


Sample instructions: 样本说明:

  1. Copy and paste the SQL setup script into your local SQL Server instance, run it. 将SQL安装脚本复制并粘贴到本地SQL Server实例中,然后运行它。
  2. Create a test project (or if you're lazy, use your existing test project) and add the nuget packages for NHibernate and Fluent NHibernate to the project. 创建一个测试项目(或者,如果您很懒,请使用现有的测试项目),然后将NHibernate和Fluent NHibernate的nuget包添加到该项目中。
  3. Run the test. 运行测试。 You should see: 您应该看到:
    1. Generated SQL run by NHibernate NHibernate运行生成的SQL
    2. Output from the test. 测试的输出。
  4. Fix mappings/query until N+1 is gone - you will know when you see first a bunch of SQL scripts outputted, then: 修复映射/查询,直到N + 1消失-您将首先看到输出的一堆SQL脚本,然后知道:

``` ```

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












-- */

Here is one way you could avoid fetches from the database when accessing the collections: 这是一种在访问集合时可以避免从数据库获取数据的方法:

var questions = session.Query<Question>()
    .Fetch(x => x.Choices)
    .ToList();

var choices = session.Query<Choice>()
    .Fetch(x => x.Questions)
    .ToList();

var questionsForSurvey = session.Query<SiteSurveyQuestion>()
    .Where(x => x.Site.Id == 1 && x.Survey.Id == 1)
    .ToArray();

The above obviously isn't ideal because it loads all questions and choices but you need tp link a Question to SiteSurveyQuestion by setting up a collection of SiteSurveyQuestion in Question to filter it like you want. 上面的内容显然不是理想的,因为它加载了所有问题和选择,但是您需要通过在Question设置SiteSurveyQuestion的集合来将一个Question链接到SiteSurveyQuestion ,以SiteSurveyQuestion需要SiteSurveyQuestion进行过滤。 This way you could just load the questions and choices you needed to based on your survey and site id. 这样,您就可以根据调查和站点ID加载所需的问题和选择。

The solution to 1 + N would've been built on top of a special NHibernate optimization feature (let me cite a bit) 1 + N的解决方案将建立在特殊的NHibernate 优化功能的基础上(让我引述一下)

19.1.5. 19.1.5。 Using batch fetching 使用批量提取

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. NHibernate可以有效地使用批处理获取,也就是说,如果访问了一个代理(或集合),NHibernate可以加载多个未初始化的代理。批处理获取是对惰性选择获取策略的优化。有两种方法可以调整批处理获取:类和收集级别。

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. 假设您在运行时遇到以下情况:在ISession中加载了25个Cat实例,每个Cat都有对其所有者Person的引用。 The Person class is mapped with a proxy, lazy="true". Person类与代理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. 如果您现在遍历所有cat并在每个cat上调用cat.Owner,则NHibernate默认情况下将执行25条SELECT语句,以检索代理的所有者。 You can tune this behavior by specifying a batch-size in the mapping of Person: 您可以通过在Person映射中指定一个批处理大小来调整此行为:

<class name="Person" batch-size="10">...</class>

NHibernate will now execute only three queries, the pattern is 10, 10, 5. NHibernate现在将仅执行三个查询,模式为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. 例如,如果每个人都有一个懒惰的Cat集合,并且当前在ISesssion中加载了10个人,则对所有人进行迭代将生成10个SELECT,每个对person.Cat的调用都会选择一个。 If you enable batch fetching for the Cats collection in the mapping of Person, NHibernate can pre-fetch collections: 如果您在Person映射中为Cats集合启用了批量获取,则NHibernate可以预获取集合:

<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. 批处理大小为3时,NHibernate将在四个SELECT中加载3、3、3、1个集合。 Again, the value of the attribute depends on the expected number of uninitialized collections in a particular Session. 同样,属性的值取决于特定会话中未初始化集合的预期数量。

So, that is the DOC. 因此,这就是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) 实际上,这意味着几乎任何one-to-many和实体映射都应包含BatchSize(25) (或50或100 ...一起玩,以找出适合您的套件)

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. 如果我们要使用using(var session...){} ,则会遇到麻烦。 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" 最好的办法是添加一些方法来遍历对象并将其转换为某些“ 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 我的首选方法是实现IClonable并在.Clone()内部.Clone()所需内容

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 而且, 有关批处理的更多信息

Using Radim's answer about setting batch sizes above, plus adding explicit eager loading for the Site and Survey entities, got me to a reasonably good 6 queries in one batch up-front, plus 2 additional queries in a second batch when accessing the first sub-question. 使用Radim关于在上面设置批量大小的答案,再加上对Site和Survey实体的明确的急切加载,可以让我在前一批中先后进行6次合理的查询,在访问第一个子目录时在第二批中另外进行2次查询。题。

The N+1 has become N/300 + 1 which ends up being ... a grand total of 8 queries in 2 batches. N + 1已变为N / 300 +1,最终成为... 2个批次中总共8个查询。

Here is what I changed (look for lines with //new comments): 这是我更改的内容(查找带有//new注释的行):

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");

        References(x => x.Site, "SiteId"). Not.LazyLoad();  //new
        References(x => x.Survey, "SurveyId").Not.LazyLoad();  //new
        References(x => x.Question, "QuestionId").Not.LazyLoad();  //new

        Map(x => x.IsActive, "ActiveFlag").Not.Nullable();
    }
}

//and so on

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")
            .BatchSize(300)  //new
            .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")
            .BatchSize(300)  //new
            .AsBag()
            .Cascade
            .AllDeleteOrphan()
            .Inverse();
    }
}

With the above changes, the SQL created by NHibernate is acceptable. 通过上述更改,可以接受NHibernate创建的SQL。

If anyone can improve upon this answer, please do. 如果有人可以改善此答案,请这样做。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM