Entity Framework: отслеживание изменений в ассоциациях FK

Я переопределяю SaveChanges в своем DbContext, чтобы реализовать журнал аудита. Работать с отношениями «многие ко многим» или независимыми ассоциациями относительно просто, поскольку EF создает ObjectStateEntries для любых изменений в этих типах отношений.

Я использую ассоциации внешнего ключа, и когда отношения между сущностями изменяются, все, что вы получаете, - это ObjectStateEnty, в котором говорится, например, что сущность «Title» имеет измененное свойство «PublisherID». Для человека это, очевидно, внешний ключ в объекте Title, но как мне определить это во время выполнения? Есть ли способ перевести это изменение в свойство «PublisherID», чтобы позволить EntityKey для сущности, которую представляет внешний ключ?

Я предполагаю, что имею дело с объектами, которые выглядят так:

public sealed class Publisher
{
    public Guid ID { get; set; }
    public string Name { get; set; }
    public ICollection<Title> Titles { get; set; }
}

public class Title
{
    public Guid ID { get; set; }
    public string Name { get; set; }
    public Guid? PublisherID { get; set; }
    public Publisher Publisher { get; set; }
}

Существует также код EF EntityConfiguration, определяющий отношение и внешний ключ:

public TitleConfiguration()
{
    HasOptional<Publisher>(t => t.Publisher).WithMany(
            p => p.Titles).HasForeignKey(t => t.PublisherID);
}

То, что я делаю сейчас, кажется слишком сложным. Я надеюсь, что есть более элегантный способ достичь моей цели. Для каждого измененного свойства из ObjectStateEntry я просматриваю все ReferentialConstraints для текущего объекта и смотрю, использует ли какой-либо из них его в качестве внешнего ключа. Код ниже вызывается из SaveChanges():

private void HandleProperties(ObjectStateEntry entry, 
        ObjectContext ctx)
{
    string[] changedProperties = entry.GetModifiedProperties().ToArray();
    foreach (string propertyName in changedProperties)
    {
        HandleForeignKey(entry, ctx, propertyName);
    }
}

private void HandleForeignKey(ObjectStateEntry entry, 
        ObjectContext ctx, string propertyName)
{
    IEnumerable<IRelatedEnd> relatedEnds = 
            entry.RelationshipManager.GetAllRelatedEnds();

    foreach (IRelatedEnd end in relatedEnds)
    {
        // find foreign key relationships
        AssociationType elementType = end.RelationshipSet.ElementType as 
                AssociationType;
        if (elementType == null || !elementType.IsForeignKey) continue;

        foreach (ReferentialConstraint constraint in 
                elementType.ReferentialConstraints)
        {
            // Multiplicity many means we are looking at a foreign key in a 
            // dependent entity
            // I assume that ToRole will point to a dependent entity, don't 
            // know if it can be FromRole
            Debug.Assert(constraint.ToRole.RelationshipMultiplicity == 
                    RelationshipMultiplicity.Many);
            // If not 1 then it is a composite key I guess. 
            // Becomes a lot more difficult to handle.
            Debug.Assert(constraint.ToProperties.Count == 1);
            EdmProperty prop = constraint.ToProperties[0];

            // entity types of current entity and foreign key entity 
            // must be the same
            if (prop.DeclaringType == entry.EntitySet.ElementType 
                    && propertyName == prop.Name)
            {
                EntityReference principalEntity = end as EntityReference;
                if (principalEntity == null) continue;

                EntityKey newEntity = principalEntity.EntityKey;
                // if there is more than one, the foreign key is composite
                Debug.Assert(newEntity.EntityKeyValues.Length == 1);

                // create an EntityKey for the old foreign key value
                EntityKey oldEntity = null;

                if (entry.OriginalValues[prop.Name] is DBNull)
                {
                    oldEntity = new EntityKey();
                    oldEntity.EntityKeyValues = new[] { 
                        new EntityKeyMember("ID", "NULL") 
                    };
                    oldEntity.EntitySetName = newEntity.EntitySetName;
                }
                else
                {
                    Guid oldGuid = Guid.Parse(
                            entry.OriginalValues[prop.Name].ToString());
                    oldEntity = ctx.CreateEntityKey(newEntity.EntitySetName, 
                            new Publisher()
                            {
                                ID = oldGuid
                            });
                }

                Debug.WriteLine(
                        "Foreign key {0} changed from [{1}: {2}] to [{3}: {4}]", 
                        prop.Name,
                        oldEntity.EntitySetName, oldEntity.EntityKeyValues[0],
                        newEntity.EntitySetName, newEntity.EntityKeyValues[0]);
            }
        }
    }
}

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

Спасибо!


person LeffeBrune    schedule 18.05.2011    source источник
comment
Что не так с аудитом только изменения идентификатора? Если вы проводите аудит непосредственно в базе данных, вы также только что изменили идентификатор. Кстати. если вам это не нравится, почему вы используете ассоциации FK вместо независимых ассоциаций?   -  person Ladislav Mrnka    schedule 19.05.2011
comment
Я пытаюсь предоставить конечному пользователю что-то более описательное, чем PublisherID, измененный с b087929b-3221-4c43-916b-ad49066969c8 на 6372b8b6-1848-4f7a-ada0-d778a5682d67. Возможно, это гиперссылка, указывающая на объект. Поэтому либо в момент сбора данных для журнала аудита, либо при отображении журнала аудита мне нужно будет выяснить, является ли свойство внешним ключом и на какой тип объекта оно указывает.   -  person LeffeBrune    schedule 19.05.2011
comment
Я прочитал вашу статью о независимых ассоциациях, и у меня осталось ощущение, что позже я могу столкнуться с некоторыми проблемами, если выберу этот путь. Кроме того, если я когда-нибудь решу сериализовать свои объекты POCO, мне может пригодиться правильное значение внешнего ключа. С независимыми ассоциациями мне нужно будет сериализовать какой-то граф сущностей, чтобы эти данные были полезными.   -  person LeffeBrune    schedule 19.05.2011
comment
Да, Guid не очень удобен для пользователя, но если основная проблема заключается в представлении данных, не должна ли она решаться каким-то отдельным запросом на объединение аудита с данными при создании представления? Должен сказать, что я проводил аудит в большинстве своих приложений, и мне это никогда не понадобилось - я просто проверял всю строку, чтобы знать, что это FK для другой таблицы. Вот почему я задаюсь вопросом, зачем вам это нужно. Проверяете ли вы каждое измененное значение как отдельную запись?   -  person Ladislav Mrnka    schedule 19.05.2011
comment
Меня попросили вести журнал истории, чтобы не поддерживать точно все предыдущие версии строки. Я предполагаю, что у вас есть отдельная таблица аудита для каждого объекта в вашей базе данных, но я рассматриваю возможность использования одной таблицы аудита для всех объектов и записи изменений в формате XML. Я также буду хранить имя таблицы и PK объекта в своей таблице аудита. Я пытаюсь найти универсальное решение для отслеживания изменений в отношениях, основанное на доступных метаданных. В качестве ввода я получаю ObjectContext и ObjectStateEntries в качестве вывода для каждого измененного отношения, которое я хочу создать EntityKey для новых и старых значений.   -  person LeffeBrune    schedule 19.05.2011
comment
В таком случае ваше текущее решение выглядит как путь.   -  person Ladislav Mrnka    schedule 19.05.2011
comment
Эй, посмотри, это то же самое, что меня просили сделать, и почти точное решение, которое я придумал :-) Что ж, я думаю, это подтверждает, что это единственный способ выполнить работу :-/ Кажется, это должно быть проще поймать эти данные...   -  person Shane Neuville    schedule 13.10.2012
comment
Назовите меня ленивым, но я бы предположил, что идентификатор в конце имени свойства означает, что это внешний ключ или используется атрибут ForeignKeyAttribute.   -  person Casey    schedule 12.10.2015


Ответы (1)


Похоже, мой код является правильным решением этой проблемы:/

В итоге я использовал независимые ассоциации, чтобы вообще избежать этой проблемы.

person LeffeBrune    schedule 15.07.2011