Как выполнять вычисляемые значения столбцов в SQL Server с помощью Entity Framework

Я привык использовать столбцы идентификаторов и создавать для меня pk. Мне интересно, есть ли способ ограничить столбец идентификаторов или иным образом вычислить его, чтобы он был уникальным во внешнем ключе?

Например, при использовании столбца идентификаторов таблица может выглядеть так:

| UserId | WidgetId | <-- Table Widget, WidgetId is identity column & pk
---------------------
|      1 |        1 |
|      1 |        2 |
|      1 |        3 |
|      2 |        4 |
|      2 |        5 |
|      2 |        6 |

Что, если я хочу добиться чего-то более похожего на следующее:

| UserId | WidgetId | <-- Table Widget, both UserId and WidgetId compose the pk
---------------------
|      1 |        1 |
|      1 |        2 |
|      1 |        3 |
|      2 |        1 |
|      2 |        2 |
|      2 |        3 |

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

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

Я предполагаю, что это также возможно сделать с помощью триггера, но добавление триггера в базу данных из DbContext приведет к зависимости от СУБД, верно?

Есть ли лучший способ, что-то, что требует меньшей зависимости от того, какой серверный EF использует?

Обновить

Чтобы быть более ясным, в приведенной выше модели намеренно существует идентифицирующая связь между виджетом и пользователем. UserId является первичным ключом таблицы User, и наличие FK для пользователя как части PK виджета означает, что виджеты никогда не передаются пользователям. Когда пользователь удаляется, все виджеты удаляются вместе с ним. Запрашивать виджеты только по WidgetId не имеет смысла; Для запроса конкретного виджета необходимы оба параметра UserId и WidgetId.

Я изучаю это, потому что помню, как читал в Evans DDD, что идентификаторы некорневых объектов в агрегате должны быть уникальными только в пределах агрегата. Выше приведен пример этого случая:

СУЩНОСТИ, отличные от корневой, имеют локальную идентичность, но эта идентичность должна быть различима только внутри АГРЕГАТА, потому что ни один внешний объект никогда не сможет увидеть ее вне контекста корневой СУЩНОСТИ. (Domain-Driven Design, Эванс)

Обновление 2

После ответа Горгана я реализовал в своем классе фабрики виджетов что-то вроде следующего. Является ли это правильным подходом для достижения желаемого результата?

public class WidgetFactory : BaseFactory
{
    private object _sync = new object();

    internal WidgetFactory(IWriteEntities entityWriter) : base(entityWriter)
    { 
        // my DbContext class implements the IWriteEntities interface
    }

    public Widget CreateOrUpdate(User user, int? widgetId, string prop1, 
        bool prop2, string prop3)
    {
        Widget widget = null;
        if (!widgetId.HasValue)
        {
            // when widgetId is null do a create (construct & hydrate), then
            EntityWriter.Insert(widget); // does DbEntityEntry.State = Added
        }
        else
        {
            // otherwise query the widget from EntityWriter to update it, then
            EntityWriter.Update(widget); // does DbEntityEntry.State = Modified
        }

        // determine the appropriate WidgetId & save
        lock (_sync)
        {
            widget.WidgetId = widgetId.HasValue ? widget.WidgetId
                : EntityWriter.Widgets.Where(w => w.UserId == user.Id)
                    .Max(w => w.WidgetId) + 1;
            EntityWriter.SaveChanges(); // does DbContext.SaveChanges()
        }

        return widget;
    }
}

Виджет

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

Пользователи автоматически создаются для каждого посетителя с использованием анонимной идентификации / Request.AnonymousID (позже будет функция регистрации, но нет необходимости опробовать демоверсию). Веб-приложение будет иметь спокойные URL-адреса, такие как /work-places/1, /work-places/2 и т. д. Поскольку каждый запрос гарантированно имеет пользователя, идентификатор пользователя не нужно указывать в URL-адресе. Я могу идентифицировать пользователей из Request.AnonymousID, когда в User.Identity.Name ничего нет.

Если бы WorkPlaceId был столбцом идентификации, мои действия контроллера сначала должны были бы проверить, чтобы убедиться, что пользователь владеет рабочим местом, прежде чем отображать его. В противном случае я мог бы взломать URL-адрес, чтобы увидеть каждое рабочее место, которое каждый пользователь настроил в системе. Сделав WorkPlaceId уникальным только для пользователя, мне не нужно об этом беспокоиться. URL-адрес /work-places/1 будет отображать совершенно разные данные для двух разных пользователей.


person danludwig    schedule 15.03.2012    source источник
comment
Если вы используете EF 5, вы должны пометить его этим тегом.   -  person Yuck    schedule 15.03.2012
comment
Круто, последний раз, когда я проверял, он еще не был создан. Спасибо за внимание.   -  person danludwig    schedule 15.03.2012
comment
Да, это об этом, но я хотел бы узнать больше о том, что представляет собой этот виджет, так как это кажется неестественным и должен быть лучший способ его смоделировать.   -  person Goran Obradovic    schedule 17.03.2012
comment
Я не должен был маскировать Виджет... Я имел в виду любую произвольную сущность, уникальную для пользователя. В приложении фактическим объектом является WorkPlace. Я обновил свой вопрос. Спасибо за вашу помощь.   -  person danludwig    schedule 17.03.2012
comment
Да, это кандидат на третий абзац, я его уточнил :)   -  person Goran Obradovic    schedule 17.03.2012
comment
Я прочитал ваше обновление, и я действительно думаю, что это усложнение. Предоставление WorkPlace собственного времени жизни не обязательно. Он содержит всего 3 свойства: Name, IsTaxable и Color. Пользователь должен иметь возможность редактировать любое из этих полей, не затрагивая других пользователей, текущих или будущих. По сути, вы можете получить возврат части налога за дни, когда вы не работаете в городе. Люди будут знать, по каким адресам они работают и облагаются ли они налогом. Это может помочь, если вы прочитаете настоящую форму возврата налога: cincinnati-oh.gov/ ситифинанс/загрузки/   -  person danludwig    schedule 17.03.2012
comment
Место имеет свое время жизни, работали вы там или нет, и вы можете смоделировать его таким образом или вычислить какой-то воображаемый идентификатор в коде для каждого рабочего места в этом месте. Вы пытаетесь смоделировать его по-другому, полностью упуская эту часть, и поэтому вы усложняете свою таблицу WorkPlace. PK слабого объекта должен состоять из идентификаторов сильных объектов, от которых он зависит. Даже если люди могут изменить Name, IsTaxable и Color, это не является причиной того, что Place не существует, если у меня есть имя для MY WorkPlace => оно попадает в таблицу WorkPlace, нет проблем. Но идентификатор места или почтовый индекс одинаковы для всех пользователей.   -  person Goran Obradovic    schedule 17.03.2012
comment
Если вы думаете, что Place — это усложнение, то почему вы усложняете workid? Нет необходимости в идентификаторе рабочего места, если его идентификация основана на идентификаторе пользователя и имени рабочего места.   -  person Goran Obradovic    schedule 17.03.2012
comment
давайте продолжим обсуждение в чате   -  person danludwig    schedule 17.03.2012


Ответы (2)


Назначение столбца идентификаторов состоит в том, чтобы иметь некоторый идентификатор в таблице, когда вам нужен столбец, который будет уникальным независимо от того, какие значения других столбцов, а сервер sql поддерживает автоматическую генерацию только для одного столбца идентификаторов в таблице (нет нескольких или сложных первичных автоматически сгенерированных ключи).

Если каждый ваш виджет отличается (даже если один и тот же виджет используется большим количеством пользователей), то этот первичный ключ имеет смысл, но его нельзя вычислить на сервере sql (за исключением случаев, когда вы используете хранимую процедуру или другое программирование БД для вставки). И вы можете читать только автоматически сгенерированный столбец с помощью ef (см. first/6995622#6995622">Как получить доступ к вычисляемому столбцу в Entity Framework Code First?).

Однако, если у вас больше типов виджетов (в этом примере типы виджетов 1, 2 и 3 EDIT: WidgetId может быть FK для таблицы WidgetType, где идентификатор генерируется автоматически, таким образом, у вас будет PK, состоящий из 2 автоматически сгенерированных идентификаторов), вы можете создать таблицу WidgetType (или WidgetInstance, WidgetSomething, любую сущность, описывающую часть виджета, не затронутую пользователем) и иметь в ней автоматически сгенерированный идентификатор, и используйте его в этой таблице как FK и часть первичного ключа. Это позволит вам автоматически сгенерировать UserId и WidgetId и существовать в момент вставки в эту таблицу.

person Goran Obradovic    schedule 15.03.2012
comment
Я думаю, что это может быть ответ, но вы как бы потеряли меня на 3-м абзаце. Я обновил вопрос, чтобы уточнить. Если такое значение WidgetId должно быть вычислено, я слышал, вы говорите, что это должно происходить на уровне EF или выше, если только я не хочу создавать sproc/триггер (я этого не делаю). Итак, что такое хороший подход? Я мог бы запросить максимальный идентификатор виджета для пользователя, увеличить на единицу, установить свойство объекта и сохранить изменения... но даже если две строки в исходном коде соседствуют, нет ли шанса конфликта параллелизма, если 2 каждый из потоков пытается одновременно вставить виджет для одного и того же пользователя? - person danludwig; 16.03.2012
comment
Да, вам нужно иметь синхронизированный раздел между потоками, который будет выполнять сохранение. Я думаю, что 3-й абзац - лучшее решение, но если у одного пользователя может быть 5 экземпляров одного и того же виджета, это неприменимо. Есть ли у вас что-то общее между некоторыми виджетами (например, имя «Часы» или «Погода»), которое не изменяется пользователями? Если нет, и ваша единственная забота - убедиться, что виджет не существует без пользователя, вы можете только сделать UserId FK NOT NULL, а когда пользователь удален, виджет должен быть удален. - person Goran Obradovic; 16.03.2012
comment
Основная причина, по которой я выбрал «Идентификационная связь», связана с тем, как EF ведет себя, когда вы пытаетесь удалить объект из коллекции (т.е. User.Widgets.Remove(User.Widgets.First()). По умолчанию он пытается установить для FK значение null. Если FK не допускает значение NULL, то EF выдает исключение, но если FK является частью PK, то вместо этого EF удаляет сущность. - person danludwig; 16.03.2012
comment
Вы хотите, чтобы ваш виджет был слабым объектом? Вы пытались использовать как WidgetId, так и UserId как часть PK, но установили WidgetId на автоматическое создание (удостоверение)? Я знаю, что в этом случае WidgetId может быть PK сам по себе, но это не должно мешать вам пометить оба поля как PK :) - person Goran Obradovic; 17.03.2012
comment
Я дополнил свой вопрос, чтобы он был полностью явным. Извините за путаницу. Я действительно не хочу использовать столбец идентификаторов для какой-либо части зависимого объекта pk. - person danludwig; 17.03.2012
comment
Если бы WorkPlace PK был UserId + Name, тогда имя стало бы неизменным. Пользователи должны иметь возможность редактировать/обновлять названия рабочих мест. В PDF читайте все это. Если вы работаете более чем в одном месте, вы должны заполнить форму L-2-C и представить хронологический маршрут мест, где вы работали, и в какие даты. - person danludwig; 17.03.2012

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

Виджеты и пользователи считаются основными данными. Таблица виджетов должна содержать только атрибуты виджета, а пользовательская таблица должна содержать только атрибуты пользователя. Если виджет связан с пользователем, вероятно, было бы нормально включить идентификатор пользователя в таблицу виджетов, но идентификатор виджета должен быть идентификатором, а идентификатор пользователя должен быть FK.

Связь виджета и пользователя должна выполняться за пределами этих мастер-таблиц. Если они связаны со счетом/счетом, обычно используется комбинация таблицы заголовка счета (invoiceid, userid, дата и время, общая стоимость, общая цена...) и таблицы строки счета (invoicelineid, invoiceid, widgetid, unit стоимость, цена за единицу, количество...)

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

person brian    schedule 16.03.2012
comment
Опять же, я думаю, что, возможно, я должен был быть более ясным в своем вопросе. Для этих двух сущностей нет большого масштаба, это простое личное приложение, над которым я работаю, чтобы познакомиться с VS11, EF5, MVC4 и т. д. Для этих двух сущностей существует определенная связь 1 User .. n Widgets. Я думаю, вы предлагаете таблицу герундия для сопоставления m Users .. n Widgets. В моей модели виджет не может существовать без пользователя... UserId является частью его личности. Я считаю, что нормализовал его правильно, и добавление отдельной таблицы для сопоставления отношений звучит как усложнение, а не упрощение. - person danludwig; 16.03.2012