Entity Framework Core Computed Property Несколько таблиц - PullRequest
2 голосов
/ 24 апреля 2020

Я работаю над ASP. Net Core веб-приложением с Entity Framework Core 3.1. Допустим, у меня есть эти четыре объекта:

public class Project
{
    public string Name { get; set; }

    public DateTimeOffset CreationDate { get; set; }

    public int ProjectTypeId { get; set; }
    public ProjectType ProjectType { get; set; }

    public ICollection<ProjectScope> ProjectScopes { get; private set; } = new HashSet<ProjectScope>();
}

public class ProjectScope
{
    public int ProjectId { get; set; }
    public Project Project { get; set; }

    public int ScopeId { get; set; }
    public Scope Scope { get; set; }

}

public class Scope
{
    public int Id { get; set; }

    public string Name { get; set; }

    public ICollection<ProjectScope> ProjectScopes { get; private set; } = new HashSet<ProjectScope>();
}

public class ProjectType
{
    public int Id { get; set; }

    public string Name { get; set; }
}

У меня есть требование, которое мне нужно отображать в нескольких местах, и я могу сортировать по нему, на уровне базы данных, имя проекта: Все Области, упорядоченные по имени + CreationDate + Имя типа проекта + Имя проекта

Вот как я могу добиться этого с помощью вычисляемого свойства:

    public string FullName => 
        "[" + ProjectScopes.Select(ps => ps.Scope.Name).OrderBy(s => s).Aggregate((a, b) => a + " - " + b) + "]" +
        " " + CreationDate.ToString("d") +
        " " + ProjectType.Name +
        " " + Name;

Конечно, его нельзя перевести эф ядро. Я отображаю это свойство в нескольких массивах своего приложения, и мне нужно иметь возможность упорядочивать результаты на основе этого поля.

Каков наилучший способ для меня получить это вычисляемое поле?

I нужно иметь возможность выбирать и сортировать в этом поле, на любом клиенте, который подключается к этой базе данных. (Это может быть, например, PowerBI)

  • Вычисляемый столбец в базе данных?
  • Триггер?
  • Пользовательская функция?

Спасибо за ваше время.

Ответы [ 4 ]

2 голосов
/ 29 апреля 2020

Это зависит от количества данных, частоты чтения / обновления, как часто происходит сортировка. Вы рассматривали возможность использования SQL View? Имеет способность индексироваться, материализоваться и т. Д. c.

using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace sqlview
{
    class Program
    {
        static void Main(string[] args)
        {
            using var dbContext = new EmpDbContext();

            // create db table
            dbContext.Database.EnsureCreated();

            // create view with "FullName" column as an aggregate like "First Name - Middle Name - Last Name"
            dbContext.Database.ExecuteSqlRaw("DROP VIEW IF EXISTS View_EmployeeView;");
            dbContext.Database.ExecuteSqlRaw(
                @"CREATE VIEW View_EmployeeView AS 
                        SELECT
                            e.Id,
                            e.FirstName + ' - ' + e.MiddleName + ' - '  + e.LastName as FullName
                        FROM Employees e;
            ");

            // insert record
            dbContext.Add(new Employee() { FirstName = "John", MiddleName = "Jack", LastName = "Sugarcoater" });
            dbContext.SaveChanges();

            // fetch using view
            var employeesFromView = dbContext.EmployeesFromView.ToListAsync().Result;
            foreach (var emp in employeesFromView)
            {
                Console.WriteLine($"Full name: {emp.FullName}");
            }
        }

        class EmpDbContext : DbContext
        {
            public DbSet<Employee> Employees { get; set; }
            public DbSet<EmployeeView> EmployeesFromView { get; set; }

            protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
            {
                optionsBuilder.UseSqlServer(
                    @"YOUR CONNECTION STRING");
            }

            protected override void OnModelCreating(ModelBuilder modelBuilder)
            {
                // configure view
                modelBuilder
                    .Entity<EmployeeView>(eb =>
                    {
                        eb.HasNoKey();
                        eb.ToView("View_EmployeeView");
                    });
            }
        }

        public class Employee
        {
            public int Id { get; set; }
            public string FirstName { get; set; }
            public string MiddleName { get; set; }
            public string LastName { get; set; }
        }

        public class EmployeeView
        {
            public int Id { get; set; }
            public string FullName { get; set; } // will be computed on the SQL side
        }
    }
}

1 голос
/ 02 мая 2020
IF OBJECTPROPERTY(OBJECT_ID('[dbo].[project_scope]'), 'IsTable') = 1
   DROP TABLE [dbo].[project_scope]
GO
IF OBJECTPROPERTY(OBJECT_ID('[dbo].[project]'), 'IsTable') = 1
   DROP TABLE [dbo].[project]
GO
IF OBJECTPROPERTY(OBJECT_ID('[dbo].[scope]'), 'IsTable') = 1
   DROP TABLE [dbo].[scope]
GO
IF OBJECTPROPERTY(OBJECT_ID('[dbo].[project_type]'), 'IsTable') = 1
   DROP TABLE [dbo].[project_type]
GO
CREATE TABLE [dbo].[project_type]
(
   id INT NOT NULL PRIMARY KEY,
   name NVARCHAR(100) NOT NULL
)
GO
CREATE TABLE [dbo].[scope]
(
   id INT NOT NULL PRIMARY KEY,
   name NVARCHAR(100) NOT NULL
)
GO
CREATE TABLE [dbo].[project]
(
   id INT NOT NULL PRIMARY KEY,
   name NVARCHAR(100) NOT NULL,
   creation_date DATETIMEOFFSET,
   project_type_id INT
)
GO
CREATE TABLE [dbo].[project_scope]
(
   project_id INT NOT NULL,
   scope_id INT NOT NULL
)
GO
INSERT INTO [dbo].[project_type]
   (id, name)
VALUES
   (1, 'Type 1')
GO
INSERT INTO [dbo].[scope]
   (id, name)
VALUES
   (1, 'Scope A')
GO
INSERT INTO [dbo].[scope]
   (id, name)
VALUES
   (2, 'Scope B')
GO
INSERT INTO [dbo].[scope]
   (id, name)
VALUES
   (3, 'Scope C')
GO
INSERT INTO [dbo].[project]
   (id, name, creation_date, project_type_id)
VALUES
   (1, 'Project Avengers', SYSDATETIMEOFFSET(), 1)
GO
INSERT INTO [dbo].[project_scope]
   (project_id, scope_id)
VALUES
   (1, 1),
   (1, 2),
   (1, 3)
GO
IF OBJECT_ID('[dbo].[udfGetProjectFullName]', 'FN') IS NOT NULL
   DROP FUNCTION udfGetProjectFullName
GO
CREATE FUNCTION [dbo].[udfGetProjectFullName](@project_id INT)
RETURNS NVARCHAR(255)
AS
BEGIN
   DECLARE @fullname NVARCHAR(255)

   SELECT @fullname = CONCAT('[', STRING_AGG(s.name, ' - ') WITHIN GROUP (ORDER BY s.name), '] ', p.creation_date, ' ', pt.name, ' ', p.name)
   FROM project AS p
      INNER JOIN project_type AS pt on pt.id = p.project_type_id
      LEFT JOIN project_scope AS ps on ps.project_id = p.id
      LEFT JOIN scope AS s on s.id = ps.scope_id
   WHERE p.id = 1
   GROUP BY p.id, p.name, p.creation_date, pt.name

   RETURN @fullname
END
GO
SELECT id, dbo.udfGetProjectFullName(id) as [full_name]
FROM project
GO
1 голос
/ 27 апреля 2020

Вы можете иметь его как свойство в самом классе.

Просто сообщите EF об игнорировании этого свойства, переопределив DbContext.OnModelCreating и используя метод EntityTypeBuilder.Ignore.

0 голосов
/ 28 апреля 2020

Вы можете использовать ViewModel, который может инкапсулировать это, и эта модель может иметь свойство, которое может оценить выше.

В моем случае я использовал AutoMapper и создал конфигурацию отображения, где я также выполняю преобразование. Например, я определяю свойство Active пользователя, которое мне нужно в пользовательском интерфейсе, на основе пропуска EndDate из Db.

Используя autopper, я достигаю этого:

public static IEnumerable<Models.ViewModels.User> AsViewModel(this IEnumerable<Models.DataModels.User> userData)
{
        var config = new MapperConfiguration(cfg => {                
            cfg.CreateMap<Models.DataModels.User, Models.ViewModels.User>()
            .ForMember(vm => vm.Active, d => d.MapFrom(m => !m.EndDate.HasValue))
            .ReverseMap();
        });
        var mapper = config.CreateMapper();
        return mapper.Map<IEnumerable<Models.DataModels.User>, IEnumerable<Models.ViewModels.User>>(userData);
}

Моя модель данных, которая взаимодействует с БД через EF:

namespace PM.Models.DataModels
{
    [Table("Users")]
    public class User
    {
        [Key]
        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
        public Guid Id { get; set; }
        [Required]
        public string FirstName { get; set; }
        [Required]
        public string LastName { get; set; }
        [Required]
        public string UserId { get; set; }
        [DefaultValue("getdate()")]
        public DateTime Created
        {
            get { return _createdValue == DateTime.MinValue ? DateTime.Now : _createdValue; }
            set { _createdValue = value; }
        }
        private DateTime _createdValue;

        public DateTime? EndDate { get; set; }
    }
}

и моя Viewmodel, которую я использую Чтобы взаимодействовать с моим уровнем презентации:

namespace PM.Models.ViewModels
{
    public class User
    {
        public Guid Id { get; set; }
        [Required]
        public string FirstName { get; set; }
        //[Required]
        public string LastName { get; set; }
        public string FullName { get { return string.Format($"{LastName}, {FirstName}"); } }
        [Required]
        public string UserId { get; set; }
        public bool Active { get; private set; }
    }
}

Как и в другом ответе, вы можете просто использовать Модель для инкапсуляции вашего FullName или в расширенном бизнес-сценарии, который вы можете преобразовать в сложные оценки.

...