在实际开发应用中我们经常会遇到实体间存在一对多的关系。如:一个分类下可有多个产品,这就是一对多关系。下面我们就延用以前开发的产品模块示例举例来说明在Orchard中是如何处理一对多关系的。
绑定一对多关系
首先我们先定义一个产品分类的数据实体CategoryPartRecord.cs,输入以下代码:
CategoryPartRecord.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Orchard.ContentManagement.Records;
namespace MyCompany.Products.Models
{
/// <summary>
/// 这个地方只是作为演示提供一些可选数据,所以不用将其设计为一种part,也就不用继承于ContentPartRecord
/// </summary>
public class CategoryPartRecord
{
public virtual int Id { get; set; }
public virtual string CategoryName { get; set; }
}
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Orchard.ContentManagement.Records;
namespace MyCompany.Products.Models
{
/// <summary>
/// 这个地方只是作为演示提供一些可选数据,所以不用将其设计为一种part,也就不用继承于ContentPartRecord
/// </summary>
public class CategoryPartRecord
{
public virtual int Id { get; set; }
public virtual string CategoryName { get; set; }
}
由于产品分类本文并不想把它处理为一个Part,所以产品分类的数据实体没有继承于ContentPartRecord。当然我们也可以把它做成一个Part的形式来维护。那么一个产品类型可以看作一个产品部件外加一个产品分类部件的形式来构成,这种形式用的是Orchard对内容的组织与管理机制实现的和本文需要介绍的一对多关系就无关了。本文所讲的一对多关系更多是从数据访问的层面来介绍的。
接下来我们需要修改一下原有的产品部件的数据实体,新增一个分类的属性,输入代码如下:
ProductRecord.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Orchard.ContentManagement.Records;
using Orchard.Data.Conventions;
namespace MyCompany.Products.Models
{
public class ProductRecord : ContentPartRecord
{
public virtual double Price { get; set; }
public virtual string Brand { get; set; }
public virtual CategoryPartRecord Category { get; set; }
}
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Orchard.ContentManagement.Records;
using Orchard.Data.Conventions;
namespace MyCompany.Products.Models
{
public class ProductRecord : ContentPartRecord
{
public virtual double Price { get; set; }
public virtual string Brand { get; set; }
public virtual CategoryPartRecord Category { get; set; }
}
注:这个地方示例命名有些不规范,通过这段时间的学习,我总结的规律是:如果需要做成Part的实体,在数据实体的命名上应该为“实体名+PartRecord”,如果不做为Part的实体命名规则为“实体名+Record”。由于在制作本示例代码时,还未总结出规律,所以命名规则正好反了。特此注明以免误导大家。
在这里我们通过新增一个分类实体的属性,就让产品实体和分类实体间形成了一个一对多关系。
同样我们在产品的的业务实体ProductPart中也添加这么一个属性,用于存储分类数据。
在数据库中体现一对多关系
通过上节对产品和分类数据实体的定义形成了一个数据实体层面上的一对多关系,这个关系在数据库中要如何体现呢?
在数据库中,一个一对多关系就好比是一种主外键关系,分类是主键表,产品是外键表。这样我们就需要在原有模块已有表的基础上新增一个产品分类表,并在产品表中新增一个分类Id的字段。在Orchard中对于数据库的初始化操作是通过Migrations.cs文件来实现了。由于是升级操作,我们在这个文件中新增一个UpdateFrom2的方法即可,代码如下:
Migrations.cs
/// <summary>
/// 升级产品模块,新增产品分类功能,为了演示Orchard中1-N的数据访问
/// </summary>
/// <returns></returns>
public int UpdateFrom2()
{
//创建一个分类表
SchemaBuilder.CreateTable("CategoryPartRecord", table => table
.Column<int>("Id", column => column.PrimaryKey().Identity()) //分类Id(这个地方用自增Id即可)
.Column<string>("CategoryName", c => c.WithLength(50)) //分类名称
);
//在产品表中新增一个分类Id,用来实现一对多关系
//这个地方分类ID的名称必须为在ProductRecord所定义的相关类的名称+Id的形式
SchemaBuilder.AlterTable("ProductRecord", table => table.AddColumn<int>("Category_Id"));
//插入默认数据(在本例中不实现维护分类的功能,就默认插入一些数据好了)
if (_categoryRepository == null)
{
throw new InvalidOperationException("Couldn't find category repository.");
}
foreach (var category in _categories)
{
_categoryRepository.Create(category);
}
return 3;
/// 升级产品模块,新增产品分类功能,为了演示Orchard中1-N的数据访问
/// </summary>
/// <returns></returns>
public int UpdateFrom2()
{
//创建一个分类表
SchemaBuilder.CreateTable("CategoryPartRecord", table => table
.Column<int>("Id", column => column.PrimaryKey().Identity()) //分类Id(这个地方用自增Id即可)
.Column<string>("CategoryName", c => c.WithLength(50)) //分类名称
);
//在产品表中新增一个分类Id,用来实现一对多关系
//这个地方分类ID的名称必须为在ProductRecord所定义的相关类的名称+Id的形式
SchemaBuilder.AlterTable("ProductRecord", table => table.AddColumn<int>("Category_Id"));
//插入默认数据(在本例中不实现维护分类的功能,就默认插入一些数据好了)
if (_categoryRepository == null)
{
throw new InvalidOperationException("Couldn't find category repository.");
}
foreach (var category in _categories)
{
_categoryRepository.Create(category);
}
return 3;
关于Migrations.cs以前文章中有过介绍,可参见这里。特别要说明的是,在产品表中新增的那个字段命名必须为产品数据实体(ProductRecord)中的“分类属性名称_Id”的形式才行,这样Orchard才会自动更具相应关系抽取产品分类表中的数据。
在数据库中并没有创建真实的外键关系,但是逻辑上可以理解为外键关系。
在画面中体现一对多关系
在Orchard中一个Part要能工作,Driver, Handler,View, Model这四个东西不能少。以前也介绍过这个部分的功能,Driver,View,Model相当于一个部件的MVC,Handler是定义部件存储功能定义。这四个部分的功能以前的产品模块都有介绍,这里只特别说明一下在编辑画面的时候,页面Model不能直接使用产品的业务对象了,他需要单独定义一个ViewModel。因为这个画面中不但需要产品的信息,还需要存储可选分类的信息。
定义一个ProductEditViewModel
ProductEditViewModel.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.ComponentModel.DataAnnotations;
using MyCompany.Products.Models;
namespace MyCompany.Products.ViewModels
{
/// <summary>
/// 编辑画面数据模型
/// </summary>
public class ProductEditViewModel
{
[Required]
public double Price { get; set; }
[Required]
public string Brand { get; set; }
public int Category_Id { get; set; }
/// <summary>
/// 可供选择的分类
/// </summary>
public IEnumerable<CategoryPartRecord> Categories { get; set; }
}
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.ComponentModel.DataAnnotations;
using MyCompany.Products.Models;
namespace MyCompany.Products.ViewModels
{
/// <summary>
/// 编辑画面数据模型
/// </summary>
public class ProductEditViewModel
{
[Required]
public double Price { get; set; }
[Required]
public string Brand { get; set; }
public int Category_Id { get; set; }
/// <summary>
/// 可供选择的分类
/// </summary>
public IEnumerable<CategoryPartRecord> Categories { get; set; }
}
修改原有ProductPartDriver
ProductPartDriver.cs
using System;
using System.Linq;
using JetBrains.Annotations;
using Orchard.ContentManagement;
using Orchard.ContentManagement.Drivers;
using Orchard.ContentManagement.Handlers;
using Orchard.Security;
using MyCompany.Products.Models;
using MyCompany.Products.ViewModels;
using Orchard.Data;
namespace Orchard.Products.Drivers
{
/// <summary>
/// Driver相当于内容部件的Controller
/// </summary>
public class ProductPartDriver : ContentPartDriver<ProductPart>
{
private readonly IRepository<CategoryPartRecord> _categoryRepository;
public ProductPartDriver(IRepository<CategoryPartRecord> categoryRepository)
{
_categoryRepository = categoryRepository;
}
/// <summary>
/// 显示界面显示时执行(相当于Action)
/// </summary>
/// <param name="part">相当于此Part的Model</param>
/// <param name="displayType">显示类型(如:"details"(详情显示)或 summary(摘要显示)")</param>
/// <param name="shapeHelper">类似视图引擎之类的东西,可以根据相应显示的动态对象去找对应的显示模板(相当于View)</param>
/// <returns>这里相当于是返回了一个ActionResult,Orchard框架会针对这个返回值进行相应处理</returns>
protected override DriverResult Display(ProductPart part, string displayType, dynamic shapeHelper)
{
return ContentShape("Parts_Product", () => shapeHelper.Parts_Product(
Price: part.Price,
Brand: part.Brand,
Category: (part.Category == null) ? string.Empty : part.Category.CategoryName) //新增一个分类信息的显示
);
}
/// <summary>
/// 编辑界面显示时执行(Get)
/// </summary>
/// <param name="part"></param>
/// <param name="shapeHelper"></param>
/// <returns></returns>
protected override DriverResult Editor(ProductPart part, dynamic shapeHelper)
{
//构造一个ViewModel来存储编辑画面的数据模型。这个ViewModel不但需要存储原有产品的数据,还需要存储产品可选的分类信息
var vm = new ProductEditViewModel { Brand = part.Brand, Category_Id = (part.Category == null) ? 0 : part.Category.Id, Price = part.Price, Categories = _categoryRepository.Table.ToList() };
return ContentShape("Parts_Product_Edit",
() => shapeHelper.EditorTemplate(
TemplateName: "Parts/Product",
Model: vm,
Prefix: Prefix));
}
/// <summary>
/// 编辑界面提交时执行(Post)
/// </summary>
/// <param name="part"></param>
/// <param name="updater"></param>
/// <param name="shapeHelper"></param>
/// <returns></returns>
protected override DriverResult Editor(ProductPart part, IUpdateModel updater, dynamic shapeHelper)
{
var model = new ProductEditViewModel();
updater.TryUpdateModel(model, Prefix, null, null);
//更新数据
if (part.ContentItem.Id != 0)
{
var productPart = part.ContentItem.As<ProductPart>();
productPart.Brand = model.Brand;
productPart.Price = model.Price;
productPart.Category = _categoryRepository.Get(model.Category_Id);
}
//updater.TryUpdateModel(part, Prefix, null, null);
return Editor(part, shapeHelper);
}
}
using System.Linq;
using JetBrains.Annotations;
using Orchard.ContentManagement;
using Orchard.ContentManagement.Drivers;
using Orchard.ContentManagement.Handlers;
using Orchard.Security;
using MyCompany.Products.Models;
using MyCompany.Products.ViewModels;
using Orchard.Data;
namespace Orchard.Products.Drivers
{
/// <summary>
/// Driver相当于内容部件的Controller
/// </summary>
public class ProductPartDriver : ContentPartDriver<ProductPart>
{
private readonly IRepository<CategoryPartRecord> _categoryRepository;
public ProductPartDriver(IRepository<CategoryPartRecord> categoryRepository)
{
_categoryRepository = categoryRepository;
}
/// <summary>
/// 显示界面显示时执行(相当于Action)
/// </summary>
/// <param name="part">相当于此Part的Model</param>
/// <param name="displayType">显示类型(如:"details"(详情显示)或 summary(摘要显示)")</param>
/// <param name="shapeHelper">类似视图引擎之类的东西,可以根据相应显示的动态对象去找对应的显示模板(相当于View)</param>
/// <returns>这里相当于是返回了一个ActionResult,Orchard框架会针对这个返回值进行相应处理</returns>
protected override DriverResult Display(ProductPart part, string displayType, dynamic shapeHelper)
{
return ContentShape("Parts_Product", () => shapeHelper.Parts_Product(
Price: part.Price,
Brand: part.Brand,
Category: (part.Category == null) ? string.Empty : part.Category.CategoryName) //新增一个分类信息的显示
);
}
/// <summary>
/// 编辑界面显示时执行(Get)
/// </summary>
/// <param name="part"></param>
/// <param name="shapeHelper"></param>
/// <returns></returns>
protected override DriverResult Editor(ProductPart part, dynamic shapeHelper)
{
//构造一个ViewModel来存储编辑画面的数据模型。这个ViewModel不但需要存储原有产品的数据,还需要存储产品可选的分类信息
var vm = new ProductEditViewModel { Brand = part.Brand, Category_Id = (part.Category == null) ? 0 : part.Category.Id, Price = part.Price, Categories = _categoryRepository.Table.ToList() };
return ContentShape("Parts_Product_Edit",
() => shapeHelper.EditorTemplate(
TemplateName: "Parts/Product",
Model: vm,
Prefix: Prefix));
}
/// <summary>
/// 编辑界面提交时执行(Post)
/// </summary>
/// <param name="part"></param>
/// <param name="updater"></param>
/// <param name="shapeHelper"></param>
/// <returns></returns>
protected override DriverResult Editor(ProductPart part, IUpdateModel updater, dynamic shapeHelper)
{
var model = new ProductEditViewModel();
updater.TryUpdateModel(model, Prefix, null, null);
//更新数据
if (part.ContentItem.Id != 0)
{
var productPart = part.ContentItem.As<ProductPart>();
productPart.Brand = model.Brand;
productPart.Price = model.Price;
productPart.Category = _categoryRepository.Get(model.Category_Id);
}
//updater.TryUpdateModel(part, Prefix, null, null);
return Editor(part, shapeHelper);
}
}
修改原有的View
编辑画面
@model MyCompany.Products.ViewModels.ProductEditViewModel
<fieldset>
@Html.LabelFor(m => m.Price, T("Price"))
@Html.TextBoxFor(m => m.Price, new { @class = "textMedium" })
@Html.ValidationMessageFor(m => m.Price, "*")
</fieldset>
<fieldset>
@Html.LabelFor(m => m.Brand, T("Brand"))
@Html.TextBoxFor(m => m.Brand, new { @class = "textMedium" })
@Html.ValidationMessageFor(m => m.Brand, "*")
</fieldset>
<!--新增分类选择-->
<fieldset>
@Html.LabelFor(m => m.Brand, T("Category"))
@Html.DropDownListFor(m => m.Category_Id, this.Model.Categories.Select(s => new SelectListItem { Selected = s.Id == this.Model.Category_Id, Text = s.CategoryName, Value = s.Id.ToString() }))
@Html.ValidationMessageFor(m => m.Category_Id, "*")
</fieldset
<fieldset>
@Html.LabelFor(m => m.Price, T("Price"))
@Html.TextBoxFor(m => m.Price, new { @class = "textMedium" })
@Html.ValidationMessageFor(m => m.Price, "*")
</fieldset>
<fieldset>
@Html.LabelFor(m => m.Brand, T("Brand"))
@Html.TextBoxFor(m => m.Brand, new { @class = "textMedium" })
@Html.ValidationMessageFor(m => m.Brand, "*")
</fieldset>
<!--新增分类选择-->
<fieldset>
@Html.LabelFor(m => m.Brand, T("Category"))
@Html.DropDownListFor(m => m.Category_Id, this.Model.Categories.Select(s => new SelectListItem { Selected = s.Id == this.Model.Category_Id, Text = s.CategoryName, Value = s.Id.ToString() }))
@Html.ValidationMessageFor(m => m.Category_Id, "*")
</fieldset
显示画面
@using MyCompany.Products.Extensions;
@using MyCompany.Products.Models;
@using Orchard.ContentManagement;
@using Orchard.Utility.Extensions;
Price:
@Model.Price
<br />
Brand:
@Model.Brand
<br />
Category:
@using MyCompany.Products.Models;
@using Orchard.ContentManagement;
@using Orchard.Utility.Extensions;
Price:
@Model.Price
<br />
Brand:
@Model.Brand
<br />
Category:
查看成果
完成以上代码的修改后,产品模块就增加了分类功能。进入管理后台升级产品模块后。再次添加产品时就会出现分类的选项了。
浏览产品时也有了分类的字段
总结
Orchard中处理一对多关系的核心是利用了NHibernate抽取数据的特性。关键就是数据实体的定义和数据表的定义。处理多对多的关系也和处理一对多关系类似,可以在官网《Creating 1-N and N-N Relations》文档中获取相关介绍。
相关文档
示例下载
Orchard.Module.MyCompany.Products.3.0.rar(解压后安装.nupkg文件)
==========================================
作者:二十四画生
转载请注明来源于博客园——二十四画生的Blog,并保留有原文链接。