Как сопоставить все свойства объекта значения с 1 строковым столбцом?

avatar
andresantacruz
8 августа 2021 в 17:34
97
2
1

Предположим, у меня есть класс сущностей Person со свойством Address.

public class Person
{
    public Address Address { get; set; }
}

        public class Address
        {
            public Address(string address)
            {
                // Retrieve the fields from a string
            }

            public string Street { get; set; }
            public int Number { get; set; }

            public override string ToString()
            {
                return $"{Street}; no. {Number}";
            }
        }

До сих пор я пробовал это:

public override void Configure(EntityTypeBuilder<Person> builder)
{
    builder
        .OwnsOne(x => x.Address, a =>
        {
            a.WithOwner();

            a.Property(y => y.Street);
            a.Property(y => y.Number);
        });

    base.Configure(builder);
}

This will map both Address.Street and Address.Number to their own column inside Person's table (respectively Address_Street and Address_Number).

Я хочу отобразить все свойства Address в таблице Person как 1 столбец строки типа и сделать его значением, как описано в его методе ToString(), и построить обратно из сохраненной строки, как описано в конструктор.

Как этого добиться?

Источник
Michael
8 августа 2021 в 17:45
0

Что произойдет, если вы измените это объединенное поле в своей базе данных? Вы можете использовать вычисляемые столбцы или представления в своей базе данных, если вам нужен такой столбец.

andresantacruz
8 августа 2021 в 17:50
0

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

Gert Arnold
8 августа 2021 в 18:03
0

По моему опыту, всякий раз, когда есть возможность хранить примитивные данные, эту возможность следует использовать с благодарностью. Хранение обработанных данных является формой потери данных. Хорошо, это маловероятно, но улица или имя могут содержать строку "; no.". Более вероятно, что однажды соглашение о форматировании может измениться, требуя обновления всех полей. Поэтому мой совет: храните поля отдельно и добавляйте вычисляемое поле, которое возвращает отформатированную строку.

andresantacruz
8 августа 2021 в 18:10
0

@GertArnold, что вы подразумеваете под вычисляемым полем?

Gert Arnold
8 августа 2021 в 18:15
1

Вычисляемый столбец AKA: database.guide/what-is-a-computed-column-in-sql-server

Michael
8 августа 2021 в 18:18
1

Также это можно сделать с помощью подхода «сначала код»: docs.microsoft.com/en-us/ef/core/modeling/… (новое в EF Core 5).

andresantacruz
8 августа 2021 в 18:18
0

@GertArnold Хорошо, я последую вашему предложению, но просто для сведения: то, что я спросил, возможно даже при условии, что EF Core?

andresantacruz
8 августа 2021 в 18:21
0

Если да, то не могли бы вы отправить ответ вместе с предложением не хранить вычисляемые данные?

Ответы (2)

avatar
Gert Arnold
8 августа 2021 в 18:41
1

По моему опыту, всякий раз, когда есть возможность сохранить примитивные данные, эту возможность следует использовать с благодарностью. Хранение обработанных данных является формой потери данных. Хорошо, это маловероятно, но улица или название могут содержать строку «; no.». Более вероятно, что однажды соглашение о форматировании может измениться, требуя обновления всех полей. Поэтому мой совет: храните поля отдельно и добавляйте вычисляемый столбец, который возвращает отформатированную строку. Это также можно сделать в code-first.

Для академических целей: да, это можно сделать с помощью преобразования значений. Идея состоит в том, чтобы преобразовать Address в строку для сохранения и, наоборот, сохраненную строку в объект Address, я немного упростил ваш случай и беззастенчиво пропустил все виды проверки/обработки исключений:

Классы:

public class Person
{
    public int ID { get; set; }
    public Address Address { get; set; }
}

public class Address
{
    public Address(string address)
    {
        var parts = address.Split(";");
        Street = parts[0];
        Number = int.Parse(parts[1]);
    }

    public string Street { get; set; }
    public int Number { get; set; }

    public override string ToString()
    {
        return $"{Street};{Number}";
    }
}

Сопоставление:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Person>()
        .Property(p => p.Address)
        .HasConversion(address => address.ToString(), 
                       str => new Address(str));
}

Обратите внимание, что Address теперь является классом, который не является частью модели EF.

avatar
Michael
8 августа 2021 в 19:20
2

Для заинтересованных читателей и в образовательных целях привожу ответ с вычисляемыми столбцами. Как уже было сказано: хранение этого поля в виде объединенной строки в реляционной базе данных нарушает один из самых фундаментальных шаблонов проектирования: поля должны быть атомарными.

В SQLite (и других системах баз данных) мы можем определить вычисляемые столбцы:

CREATE TABLE Persons (
    Id INTEGER NOT NULL CONSTRAINT "PK_Persons" PRIMARY KEY AUTOINCREMENT,
    Address_Street TEXT NOT NULL,
    Address_Number INTEGER NOT NULL,
    Address_Text AS ([Address_Street] || '; no. ' || [Address_Number])
);

Начиная с EF Core 5, мы можем сначала определить этот столбец в коде:

using System;
using System.Linq;
using System.ComponentModel.DataAnnotations;
using Microsoft.EntityFrameworkCore;

namespace DemoApp
{
    public class Person
    {
        private Person() { }                              // Constructor for EF Core

        public Person(Address address)
        {
            Address = address;
        }

        public int Id { get; private set; }               // Database generated by convention
        public Address Address { get; set; } = default!;  // Value object; nullable forgivng if you enable nullable.
        public string AddressText { get; } = default!;    // Our computed property (read only) nullable forgivng if you enable nullable.
    }

    // C# 9 records provides equality based on all values and they are immutable.
    // If you want to update person, you have to reassign a new instance.
    // Positional syntax for property definition
    public record Address(
        [property: MaxLength(255)] string Street,
        int Number
    );

    public class MyContext : DbContext
    {
        public DbSet<Person> Persons => Set<Person>();

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.LogTo(Console.WriteLine, Microsoft.Extensions.Logging.LogLevel.Information);
            optionsBuilder.UseSqlite("DataSource = MyDatabase.db");
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Person>().OwnsOne(p => p.Address);
            // SQL expressions can be database specific (string concatenation with || or +). So we use this
            // expression only for SQLite.
            if (Database.IsSqlite())
            {
                // Address is a value object, so street and number will be mapped to Address_Street
                // and Address_Number in the person table. Therefore we have to define this
                // as a property of person.
                modelBuilder.Entity<Person>()
                    .Property(p => p.AddressText)
                    .HasComputedColumnSql("[Address_Street] || '; no. ' || [Address_Number]");
            }
        }
    }

    internal class Program
    {
        private static void Main(string[] args)
        {
            using (var context = new MyContext())
            {
                context.Database.EnsureDeleted();
                context.Database.EnsureCreated();
                // Creates
                // INSERT INTO "Persons" ("Address_Number", "Address_Street")
                //    VALUES(@p0, @p1);
                context.Persons.Add(new Person(new Address("My Street", 10)));
                context.SaveChanges();
            }
            using (var context = new MyContext())
            {
                // Creates
                // SELECT "p"."AddressText" FROM "Persons" AS "p" LIMIT 1
                //
                // Output: My Street; no. 10
                Console.WriteLine(context.Persons.Select(p => p.AddressText).First());
            }
        }
    }
}