This allows us to map multiple objects from a single table, which will separate large load situations and allow for better performance.
We will be using NuGet Package Manager to install the Entity Framework 4.1 assemblies.
The package installer can be found at http://nuget.org/.
We will also be using a database to connect to the data and update it.
Open the Improving Single Table To Multiple Object Maps solution in the included source code examples.
Let us get connected to the database using the following steps:
MappingTest
to the Test
project. We make a test that connects to the database and retrieves an object. This will test the configuration and ensure that the model matches the database schema using the following code:using System; using System.Collections.Generic; using System.Linq; using System.Text; using BusinessLogic; using DataAccess; using DataAccess.Database; using Microsoft.VisualStudio.TestTools.UnitTesting; using Test.Properties; using System.Data.Entity; namespace Test { [TestClass] public class MappingTest { [TestMethod] public void ShouldReturnABlogWithLogo() { //Arrange var init = new Initializer(); var context = new BlogContext(Settings.Default.BlogConnection); init.InitializeDatabase(context); //Act var post = context.Blogs.FirstOrDefault(); //Assert Assert.IsNotNull(post); Assert.IsNotNull(post.BlogLogo); } } }
Initializer
to the DataAccess
project in the Database
folder, with the following code:using System; using System.Collections.Generic; using System.Data.Entity; using BusinessLogic; namespace DataAccess.Database { public class Initializer : DropCreateDatabaseAlways<BlogContext> { public Initializer() { } protected override void Seed(BlogContext context) { context.Set<Blog>().Add(new Blog() { Creationdate = DateTime.Now, ShortDescription = "Testing", Title = "Test Blog", BlogLogo = new BlogLogo(){Logo = new byte[0]} }); context.SaveChanges(); } } }
Blog
to the BusinessLogic
project, with the following code:using System; using System.Collections.Generic; using DataAccess; namespace BusinessLogic { public class Blog { public int Id { get; set; } public DateTime Creationdate { get; set; } public string ShortDescription { get; set; } public string Title { get; set; } public virtual BlogLogo BlogLogo { get; set; } } }
BlogLogo
to the BusinessLogic
project, with the following code:namespace BusinessLogic { public class BlogLogo { public int Id { get; set; } public byte[] Logo { get; set; } } }
Mapping
folder to the DataAccess
project and then add a BlogMapping
class to the folder, with the following code:using System.ComponentModel.DataAnnotations; using System.Data.Entity.ModelConfiguration; using BusinessLogic; namespace DataAccess.Mappings { public class BlogMapping : EntityTypeConfiguration<Blog> { public BlogMapping() { this.ToTable("Blogs"); this.HasKey(x => x.Id); this.Property(x => x.Id) .HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity) .HasColumnName("BlogId"); this.Property(x => x.Title).HasMaxLength(175); this.HasRequired(x => x.BlogLogo).WithRequiredPrincipal(); } } }
BlogLogoMapping
to the Mapping
folder, with the following code:using System.ComponentModel.DataAnnotations; using System.Data.Entity.ModelConfiguration; using BusinessLogic; namespace DataAccess.Mappings { public class BlogLogoMapping : EntityTypeConfiguration<BlogLogo> { public BlogLogoMapping() { this.ToTable("Blogs"); this.HasKey(x => x.Id); this.Property(x => x.Id) .HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity) .HasColumnName("BlogId"); } } }
BlogContext
to include Blog DbSet<>
, and the new configurations, with the following code:using System; using System.Data.Entity; using System.Linq; using BusinessLogic; using DataAccess.Mappings; namespace DataAccess { public class BlogContext : DbContext, IUnitOfWork { public BlogContext(string connectionString) : base(connectionString) { } protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Configurations.Add(new BlogMapping()); modelBuilder.Configurations.Add(new BlogLogoMapping()); base.OnModelCreating(modelBuilder); } public DbSet<Blog> Blogs { get; set; } public IQueryable<T> Find<T>() where T : class { return this.Set<T>(); } public void Refresh() { this.ChangeTracker.Entries().ToList().ForEach(x=>x.Reload()); } public void Commit() { this.SaveChanges(); } } }
We start this solution with a test to make sure that we are accomplishing the goals and nothing more. For this solution, the goal is to split a table into multiple objects, so that we can load each of them independently, as needed, without a large data pull. This is often used to separate large images stored in SQL from the data that they are normally stored with.
We added our blog and the logo of our blog to give a representation of this scenario. BlogLogo
is a varbinary(max)
in the database, and could take sizable time to load. The key point to note, in the domain objects, is that the virtual keyword is on BlogLogo
, which will allow for the lazy loading of the logo when needed, but not before.
The mappings that allow for the splitting of a table are fairly straightforward. It requires a one-to-one relationship, of which the blog is the principal. The logo has no navigation property to get to the blog that holds it, as it was not needed in our code. This leads us to the HasRequired()
and WithRequiredPrinciple()
methods for the navigation property.
Also, note how the key is configured the same on both the objects, that is, this
and ToTable()
; this splits the table into separate objects. The required one-to-one relationship is to enforce that no one tries to insert BlogLogo
without a blog attached to it. If you are just dealing with objects whose needs may be uncertain to you, you might not know until the context throws an exception to let you know.
With splitting tables, you need to make sure that the need is there only for the sake of performance, and not just idle architecting.
The additional overhead of creating this type of mapping, and the knowledge that we force the future developers to have, is a major concern. We have forced developers to know that BlogLogo
is related to the Blog
class, and must be created with one, but that restriction does not exist in our code. This native knowledge is one of the main reasons that the onboarding process for legacy systems is so long. While writing new applications, there are other ways to solve this, such as a shared primary key association
.
Sharing a table through one-to-one relationships that hit it is a fairly simple scenario and can serve to dissect tables. If we pulled BlogLogo
into a separate table, and marked it as a one-to-one relationship that is required from Blog
but is optional from BlogLogo
, we would create a scenario in which we could query directly for BlogLogo
. This would allow us to reuse that data without the need to query only parts of BlogTable
.