将具有不同关系值的表转换为excel列

Posted

技术标签:

【中文标题】将具有不同关系值的表转换为excel列【英文标题】:Convert a table with different relational values to excel columns 【发布时间】:2019-05-10 13:32:38 【问题描述】:

我有这些表:

类别

CategoryId
CategoryTitle
...
ICollection<Article> Articles  

每个分类可以有几篇文章:

文章

ArticleId
ArticleTitle  
NumberOfComment
NumberOfView
...
ICollection<ArticleReview> Reviews 

每篇文章都有一些用户的评论:

文章评论

ArticleReviewId 
ReviewPoint
ArticleId
ReviewerId

我正在尝试使用 EPPlus 包导出 excel 报告 这是我的ExcelExport 课程:

public class excelExport 

    public string ArticleTitle  get; set; 

    public int NumberOfComment  get; set; 
    public int NumberOfReviews  get; set; 
    public List<ResearchReviewReport> Reviews  get; set; 


public class ArticleReviewReport

    public string Reviewer  get; set; 
    public int ReviewPoint  get; set; 

注意:由于一篇文章的评论数量不同,我使用一对多的关系,但最终的结果应该是所有的都被展平单行。现在我创建了不属于数据库的新类,并将这个类传递给ExcelPackage 类以生成 xlsx 作为输出:

Excel导出

ArticleTitle 
Reviewer1Point
Reviewer2Point
............
ReviewerNPoint
ReviewersAvaragePoint
NumberOfComment
NumberOfView  

如何使用另外 3 个类填充 ExcelExport 类?


编辑 这是我预期的 excel 输出

我的一个问题是 Reviewer Point 列是动态更改的, 一篇文章可能有 3 列(如上图),但另一篇文章可能有 4 或 5 个评论者点。Edit2 我忘了说每篇文章都有一些问题和每个问题的审稿人回答,所以如果我们有 3 个问题并且有 2 个审稿人,文章有 6 个 ArticleReview,我应该得到每个审稿人的平均 ArticleReview 并将其放在单个单元格中

【问题讨论】:

【参考方案1】:

为简单起见,我假设您正在使用以下简化模型并将描述解决方案。您可以轻松地使其适应您的模型:

public class Article

    public string Title  get; set; 
    public DateTime Date  get; set; 
    public List<Review> Reviews  get; set; 

public class Review

    public int Points  get; set; 

现在我们将根据输入数据生成以下输出,具有动态数量的审阅者列:

解决方案

创建一个将List&lt;Article&gt; 转换为DataTable 的函数就足够了。要创建这样的DataTable,为Article 的每个属性添加一个新列。然后找到Reviews 列表的最大计数并添加该列数。然后在一个循环中,对于每个Article,包括它的Review 列表,创建一个对象数组并添加到DataTable。显然你也可以对字段进行计算。

函数如下:

public DataTable GetData(List<Article> list)

    var dt = new DataTable();
    dt.Columns.Add("Title", typeof(string));
    dt.Columns.Add("Date", typeof(DateTime));
    var max = list.Max(x => x.Reviews.Count());
    for (int i = 0; i < max; i++)
        dt.Columns.Add($"Reviewer i + 1 Points", typeof(int));
    foreach (var item in list)
        dt.Rows.Add(new object[]  item.Title, item.Date .Concat(
            item.Reviews.Select(x => x.Points).Cast<object>()).ToArray());
    return dt;

测试数据

这是我的测试数据:

var list = new List<Article>

    new Article()
        Title = "Article 1", Date = new DateTime(2018,1,1),
        Reviews = new List<Review> 
            new Review()Points=10,
        ,
    ,
    new Article()
        Title = "Article 2", Date = new DateTime(2018,1,2),
        Reviews = new List<Review> 
            new Review()Points=10, new Review()Points=9, new Review()Points=8,
        ,
    ,
    new Article()
        Title = "Article 3", Date = new DateTime(2018,1,3),
        Reviews = new List<Review> 
            new Review()Points=9,
        ,
    ,
;

【讨论】:

更新了使用Concat连接数组的方法。 为什么不使用 TypeDescriptor.GetProperties 添加列? 可以使用。这只是一个示例作为起点。只要保留我的,如果您使用TypeDescriptor.GetProperties,您需要排除列表属性。 我用这篇文章导出excel,c-sharpcorner.com/article/export-to-excel-in-asp-net-mvc这个单独创建列标题并传递给ExportExcel方法 我明白了。现在你有了DataTable(解决了最大的问题,处理列表中可变数量的项目并得到一个平坦的结果。)下一步(这是最简单的部分)是将DataTable导出到excel。【参考方案2】:

假设您有以下模型:

public class Category

    public long CategoryId  get; set; 
    public string CategoryTitle  get; set; 
    public virtual ICollection<Article> Articles  get; set; 


public class Article

    public long ArticleId  get; set; 
    public long CategoryId  get; set; 
    public string ArticleTitle  get; set; 
    public int NumberOfComment  get; set; 
    public int NumberOfView  get; set; 
    public virtual Category Category  get; set; 
    public virtual ICollection<ArticleReview> Reviews  get; set; 

public class ArticleReview

    public long ArticleReviewId  get; set; 
    public long ArticleId  get; set; 
    public string ReviewerId  get; set; 
    public int ReviewPoint  get; set; 
    public virtual Article Article  get; set; 

public class ExcelExport

    public string ArticleTitle  get; set; 
    public int NumberOfComment  get; set; 
    public int NumberOfReviews  get; set; 
    public List<ArticleReviewReport> Reviews  get; set; 


public class ArticleReviewReport

    public string Reviewer  get; set; 
    public int ReviewPoint  get; set; 

最终您将获得ExcelExport 的列表,查询应如下所示(_context 是您的实体 DbContext 的一个实例):

public List<ExcelExport> GetExcelExports()

    return _context.Articles.Select(a => new ExcelExport
    
        ArticleTitle = a.ArticleTitle,
        NumberOfComment = a.NumberOfComment,
        NumberOfReviews = a.NumberOfView,
        Reviews = a.Reviews.Select(r => new ArticleReviewReport
        
            Reviewer = r.ReviewerId,
            ReviewPoint = r.ReviewPoint
        ).ToList()
    ).ToList();

【讨论】:

ArticleReviewReport 如何在单行中为每篇文章扁平化?每个表格也是独立的 这完全破旧了,我想在单列上显示每条评论,但它会返回它的列表!【参考方案3】:

我希望这是您正在寻找的。据我所知,目标是“扁平化”给定“类”中的数据。我将放弃导出到 Excel,因为这似乎是一个不同的问题。 “类”存在,我猜是返回 DataTable 或您想要的任何“集合”类型的方法。我猜这会使导出到 Excel 时变得更容易。

Catergory 类中,它有一个Article’s 的“集合”。每个Article 代表集合(Excel 电子表格)中的一个“行”。每个Article 都有一个ArticleReviews 的“集合”,称为Reviews。正如你所说……

我的一个问题是 Reviewer Point 列是动态更改的, 一篇文章可能有 3 列(如上图),但在 另一个可能是 4 或 5 个 Reviewer Point。

听起来每个Article 可能有很多审稿人,此外,并非所有审稿人都会“审阅”所有文章。鉴于这一点以及“扁平化”这些数据的要求,这意味着为“每个”审阅者创建一个列。另外,我假设只列出了审过一篇文章的审稿人,否则,为每个审稿人创建一个列会很简单。我猜目标是只有评论者“评论”了至少一 (1) 篇文章的专栏。

话虽如此,我猜第一个问题是弄清楚我们需要为审稿人提供“多少”列,以及这些审稿人的姓名是“什么”。我们需要一些方法来识别“哪个”列属于哪个审阅者。我使用Reviewer 名称来识别正确的列。那么我们如何找到审稿人……

Category 类有一个Artlicles 列表很方便。如果创建了一个方法来遍历每篇文章,然后遍历文章的每条评论并收集所有评论者并忽略重复项……这应该为我们提供需要为其添加列的“评论者”列表。如果该方法重新调整了Reviewer 的列表,我们可以使用它来确定我们需要多少列,以及这些列的名称应该是什么。

其中一个可能的问题是列顺序可能无法预测。根据首先是哪篇文章,将确定列的顺序。因此,我建议对列进行一些“排序”以保持某种顺序。

我添加了一个类Reviewer,它应该有助于对列名进行排序和比较。这是一个简单的Reviewer 类,如下所示。请注意排序使用的compareTo 方法。它按审阅者 ID 排序。这将保持相同的列顺序。

public class Reviewer : IComparable<Reviewer> 

  public int ReviewerID  get; set; 
  public string ReviewerName  get; set; 

  public Reviewer() 
  

  public Reviewer(int reviewerID, string reviewerName) 
    ReviewerID = reviewerID;
    ReviewerName = reviewerName;
  

  public override string ToString() 
    return "ReviewerID: " + ReviewerID.ToString();
  

  public override bool Equals(object obj) 
    return this.ReviewerName.Equals(((Reviewer)obj).ReviewerName);
  

  public override int GetHashCode() 
    return ReviewerName.GetHashCode();
  

  public int CompareTo(Reviewer other) 
    return this.ReviewerID.CompareTo(other.ReviewerID);
  

这将影响ArticleReview 类,需要在那里进行一些更改。有些变量看起来是不必要的,只显示需要的变量。主要变化是上面的Reviewer 对象来定义审阅者。

public class ArticleReview 

  public long ArticleId  get; set; 
  public Reviewer TheReviewer  get; set; 
  public int ReviewPoint  get; set; 

  public ArticleReview() 
  

  public ArticleReview (long articleId, Reviewer reviewerId, int reviewPoint) 
    ArticleId = articleId;
    TheReviewer = reviewerId;
    ReviewPoint = reviewPoint;
  

接下来是Article 类。它包含该文章的所有评论。似乎有一个名为“平均点”的列。这看起来像是评论中的“计算”值。因此,我猜想Article 类可以方便地为我们“计算”这个值。它包含所有评论……所需要的只是将所有分数相加并除以评论数量。这个方法被添加到Article 类中。

public class Article 
  public long ArticleId  get; set; 
  public string ArticleTitle  get; set; 
  public int NumberOfComment  get; set; 
  public int NumberOfView  get; set; 
  public virtual ICollection<ArticleReview> Reviews  get; set; 

  public Article() 
  

  public Article(long articleId, string articleTitle, int numberOfComment, int numberOfView, ICollection<ArticleReview> reviews) 
    ArticleId = articleId;
    ArticleTitle = articleTitle;
    NumberOfComment = numberOfComment;
    NumberOfView = numberOfView;
    Reviews = reviews;
  

  public decimal GetAverage() 
    if (Reviews.Count <= 0)
      return 0;
    decimal divisor = Reviews.Count;
    int totPoints = 0;
    foreach (ArticleReview review in Reviews) 
      totPoints += review.ReviewPoint;
    
    return totPoints / divisor;
  

最后,Category 类包含所有 Articles。这个类是我们需要完成前面描述的所有列内容的地方。第一部分是得到一个没有重复的List&lt;Reviewer&gt;。这将需要遍历所有文章,然后遍历每篇文章中的所有评论。在此过程中,我们可以检查“审阅者”并创建所有用户的非重复列表。代码创建一个新的空List&lt;Reviewer&gt; 然后循环遍历每篇文章,循环遍历每条评论。检查“reviewer”是否已经在列表中,如果没有,则添加它们,否则忽略重复的“reviewer”。对列表进行排序以保持列顺序,然后将其返回。

我猜这个列表可以通过多种方式来解决“列”难题。在这个例子中,另一个方法被添加到Category 类。 GetDataTable 方法从文章中的数据返回一个DataTable。开始前四列添加到表中,“标题”、“#ofView”、“#ofComment”和“平均点”。接下来循环遍历所有审阅者以添加审阅者列。审阅者姓名用作列名。这就是我们在添加数据时识别哪个列属于哪个审阅者的方式。

最后,循环遍历每个Article 以添加数据。每篇文章都会创建一个新行。可以设置行中的前三列……标题、视图、评论和平均值。接下来,我们遍历所有评论。对于每条评论,targetName 设置为评论者的姓名,然后循环遍历每一列,直到找到与评论者姓名匹配的列名。找到后,我们知道这是数据所属的列。添加值并跳出列循环并进行下一次审查。

public class Category 
  public long CategoryId  get; set; 
  public string CategoryTitle  get; set; 
  //...
  public virtual ICollection<Article> Articles  get; set; 

  public Category() 
  

  public Category(long categoryId, string categoryTitle, ICollection<Article> articles) 
    CategoryId = categoryId;
    CategoryTitle = categoryTitle;
    Articles = articles;
  

  public DataTable GetDataTable() 
    List<Reviewer> allReviewers = GetNumberOfReviewers();
    DataTable dt = new DataTable();
    dt.Columns.Add("Title", typeof(string));
    dt.Columns.Add("#ofView", typeof(long));
    dt.Columns.Add("#ofComment", typeof(long));
    dt.Columns.Add("Average point", typeof(decimal));
    foreach (Reviewer reviewer in allReviewers) 
      dt.Columns.Add(reviewer.ReviewerName, typeof(long));
    
    foreach (Article article in Articles) 
      DataRow newRow = dt.NewRow();
      newRow["Title"] = article.ArticleTitle;
      newRow["#ofView"] = article.NumberOfView;
      newRow["#ofComment"] = article.NumberOfComment;
      newRow["Average point"] = article.GetAverage();
      foreach (ArticleReview review in article.Reviews) 
        string targetName = review.TheReviewer.ReviewerName;
        for (int i = 4; i < dt.Columns.Count; i++) 
          if (targetName == dt.Columns[i].ColumnName) 
            newRow[review.TheReviewer.ReviewerName] = review.ReviewPoint;
            break;
          
        
      
      dt.Rows.Add(newRow);
    
    return dt;
  

  private List<Reviewer> GetNumberOfReviewers() 
    // we need a list of all the different reviewers
    List<Reviewer> reviewers = new List<Reviewer>();
    foreach (Article article in Articles) 
      foreach (ArticleReview review in article.Reviews) 
        if (!reviewers.Contains(review.TheReviewer)) 
          reviewers.Add(review.TheReviewer);
        
      
    
    reviewers.Sort();
    return reviewers; 
  

将所有这些放在一起,下面的代码会创建一些数据来演示。然后,DataTable 用作DataSourceDataGridView。我希望这会有所帮助。

DataTable dt;

public Form1() 
  InitializeComponent();


private void Form1_Load(object sender, EventArgs e) 
  Category cat = new Category();
  cat.CategoryId = 1;
  cat.CategoryTitle = "Category 1";
  cat.Articles = GetArticles();
  dt = cat.GetDataTable();
  dataGridView1.DataSource = dt;


private List<Article> GetArticles() 
  List<Article> articles = new List<Article>();
  Article art = new Article(1, "Article 1 Title", 10, 1200, GetReviews(1));
  articles.Add(art);
  art = new Article(2, "Article 2 Title", 32, 578, GetReviews(2));
  articles.Add(art);
  art = new Article(3, "Article 3 Title", 15, 132, GetReviews(3));
  articles.Add(art);
  art = new Article(4, "Article 4 Title", 13, 133, GetReviews(4));
  articles.Add(art);
  art = new Article(5, "Article 5 Title", 55, 555, GetReviews(5));
  articles.Add(art);
  art = new Article(6, "Article 6 Title", 0, 0, GetReviews(6));
  articles.Add(art);
  return articles;


private ICollection<ArticleReview> GetReviews(int reviewId) 
  ICollection<ArticleReview> reviews = new List<ArticleReview>();
  ArticleReview ar;
  Reviewer Reviewer1 = new Reviewer(1, "Reviewer 1");
  Reviewer Reviewer2 = new Reviewer(2, "Reviewer 2");
  Reviewer Reviewer3 = new Reviewer(3, "Reviewer 3");
  Reviewer Reviewer4 = new Reviewer(4, "Reviewer 4");
  Reviewer Reviewer5 = new Reviewer(5, "Reviewer 5");
  Reviewer Reviewer6 = new Reviewer(6, "Reviewer 6");

  switch (reviewId) 
    case 1:
      ar = new ArticleReview(1, Reviewer1, 15);
      reviews.Add(ar);
      ar = new ArticleReview(1, Reviewer2, 35);
      reviews.Add(ar);
      ar = new ArticleReview(1, Reviewer3, 80);
      reviews.Add(ar);
      ar = new ArticleReview(1, Reviewer5, 55);
      reviews.Add(ar);
      ar = new ArticleReview(1, Reviewer6, 666);
      reviews.Add(ar);
      break;
    case 2:
      ar = new ArticleReview(2, Reviewer1, 50);
      reviews.Add(ar);
      ar = new ArticleReview(2, Reviewer2, 60);
      reviews.Add(ar);
      ar = new ArticleReview(2, Reviewer3, 40);
      reviews.Add(ar);
      break;
    case 3:
      ar = new ArticleReview(3, Reviewer1, 60);
      reviews.Add(ar);
      ar = new ArticleReview(3, Reviewer2, 60);
      reviews.Add(ar);
      ar = new ArticleReview(3, Reviewer3, 80);
      reviews.Add(ar);
      break;
    case 4:
      ar = new ArticleReview(4, Reviewer1, 30);
      reviews.Add(ar);
      ar = new ArticleReview(4, Reviewer2, 70);
      reviews.Add(ar);
      ar = new ArticleReview(4, Reviewer3, 70);
      reviews.Add(ar);
      break;
    case 5:
      ar = new ArticleReview(5, Reviewer3, 44);
      reviews.Add(ar);
      break;
    case 6:
      break;
    default:
      break;
  
  return reviews;

使用 EPPlus,下面是使用上面的 DataTable 并将 DataTable 导出到 Excel 工作表的一种方法。

private void btn_ExportToExcel_Click(object sender, EventArgs e) 
  using (var p = new ExcelPackage()) 
    var ws = p.Workbook.Worksheets.Add("MySheet");
    ws.Cells["A1"].LoadFromDataTable(dt, true);
    p.SaveAs(new FileInfo(@"D:\Test\ExcelFiles\EpplusExport.xlsx"));
  

【讨论】:

【参考方案4】:

如何使用另外 3 个类填充 excelExport 类?

根据所描述的关系,您可以枚举ExcelExport 类中的每个属性,如下所示:

NumberOfComment 等于 article.NumberOfComment 对于每个文章条目! 除非您使用另一个名为ArticleComment 的表并利用Article 类中的导航属性(使用public virtual ICollection&lt;ArticleComment&gt; Comments get; set;),然后用@987654327 计算cmets 的数量@。

NumberOfReviews 等于每个文章条目的article.Reviews.Count()

每篇文章的Reviews 可以是以下内容:

article.Reviews.Select(s => new ArticleReviewReport  
       Reviewer = r.ReviewerId, // user id
       ReviewPoint = r.ReviewPoint
);

看来您还必须在 ExcelExport 类中添加另一个属性以显示 ReviewersAvaragePoint 并像这样枚举:

var reviewPoints = article.Reviews.Select(s => s.ReviewPoint);
ReviewersAvaragePoint = reviewPoints.Sum()/reviewPoints.Count();

根据 OP 的编辑进行编辑

通过使用ArticleReviewReport 中的List(例如List&lt;ArticleReviewReport&gt; Reviews),您可以拥有一个灵活的数组(动态列)以相应的格式呈现。 缺少的部分是根据从 ArticleReview 表中提取的 Distinct ReviewerId 创建动态列。整篇文章如下所示:

var allReviewers = db.articleReviews/*condition*/.Select(s => s.ReviewerId).Distinct();

现在您可以将每个ArticleReviewReport 分配给相应的列。对于Reviews 成员,使用List&lt;Dictionary&lt;string, string&gt;&gt; 将是一个很好的数据类型

【讨论】:

【参考方案5】:
public IEnumarable<ExcelExport> GetExcelExports()

    return _context.Articles.Select(a => new ExcelExport
    
       ArticleTitle = a.ArticleTitle,
       NumberOfComment = a.NumberOfComment,
       NumberOfReviews = a.NumberOfView,
       Reviewer1Point = a.Reviews.Any(e => e.ReviewerId = 1) ? a.Reviews.Where(e => e.ReviewerId = 1).Sum(e => e.ReviewPoint) : 0,
       Reviewer2Point = a.Reviews.Any(e => e.ReviewerId = 2) ? a.Reviews.Where(e => e.ReviewerId = 2).Sum(e => e.ReviewPoint) : 0,
       ....
       ReviewerNPoint = a.Reviews.Any(e => e.ReviewerId = N) ? a.Reviews.Where(e => e.ReviewerId = N).Sum(e => e.ReviewPoint) : 0
     );

如果您使用延迟加载,您还必须 .Include(e => e.Reviews)。

【讨论】:

以上是关于将具有不同关系值的表转换为excel列的主要内容,如果未能解决你的问题,请参考以下文章

使用名为查询的数据 jpa 返回具有不同列值的行

将关系数据库 (OLTP) 转换为数据仓库模型

Laravel 将枢轴附加到具有多个值的表

Excel VLOOKUP #N/A 来自具有相似值的表

如何将具有排名值的列转换为oracle中的行

将具有日期和纪元格式值的字符串列转换为 postgresql/Tableau prep 中的日期列