Как вы нарушаете круговые ассоциации между сущностями? - PullRequest
13 голосов
/ 05 марта 2009

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

Я продолжаю сталкиваться с конкретной ситуацией в моем текущем проекте, и мне было интересно, как вы, ребята, справитесь с этим. Шаблон: родитель с коллекцией дочерних элементов, и родитель имеет одну или несколько ссылок на определенные элементы в дочерней коллекции, обычно дочерний элемент «по умолчанию».

Более конкретный пример:

public class SystemMenu 
{
    public IList<MenuItem> Items { get; private set; }
    public MenuItem DefaultItem { get; set; }
}

public class MenuItem
{
    public SystemMenu Parent { get; set; }
    public string Name { get; set; }
}

Мне кажется, это хороший чистый способ моделирования отношений, но сразу вызывает проблемы благодаря круговой ассоциации, я не могу навязать отношения в БД из-за круговых внешних ключей, и LINQ to SQL взрывается из-за циклической ассоциации. Даже если бы я мог обойти это, это явно не лучшая идея.

Моя единственная идея в настоящее время - установить флаг IsDefault на MenuItem:

public class SystemMenu 
{
    public IList<MenuItem> Items { get; private set; }
    public MenuItem DefaultItem 
    {
        get 
        {
            return Items.Single(x => x.IsDefault);
        }
        set
        {
            DefaultItem.IsDefault = false;
            value.DefaultItem = true;
        }
    }
}

public class MenuItem
{
    public SystemMenu Parent { get; set; }
    public string Name { get; set; }
    public bool IsDefault { get; set; }
}

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

Ура!

Редактировать: Спасибо за ответы, возможно, пример «Меню» не был блестящим, хотя я пытался придумать что-то представительное, поэтому мне не пришлось вдаваться в подробности нашего не очень -объяснительная модель предметной области! Возможно, лучшим примером будут отношения между компанией и сотрудником:

public class Company
{
    public string Name { get; set; }
    public IList<Employee> Employees { get; private set; }
    public Employee ContactPerson { get; set; }
}

public class Employee
{
    public Company EmployedBy { get; set; }
    public string FullName { get; set; }
}

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

Ответы [ 8 ]

25 голосов
/ 07 марта 2009

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

Короче говоря, вы создаете интерфейс для родителя, который имеет только те методы, которые нужны ребенку. Вы также создаете интерфейс для дочернего элемента, который имеет только те методы, которые необходимы родительскому элементу. Затем у вас есть родительский список со списком дочерних интерфейсов, и у вас есть дочерняя точка обратно на родительский интерфейс. Я называю это Образцом флип-флопа , потому что диаграмма UML имеет геометрию триггера Эклса-Джордана (Сью, я старый инженер по аппаратному обеспечению!)

  |ISystemMenu|<-+    +->|IMenuItem|
          A    1  \  / *     A
          |        \/        |
          |        /\        |
          |       /  \       |
          |      /    \      |
          |     /      \     |
    |SystemMenu|        |MenuItem|

Обратите внимание, что на этой диаграмме нет цикла. Вы не можете начинать с одного класса и следовать стрелкам обратно к исходной точке.

Иногда, чтобы получить правильное разделение, вы должны передвигать некоторые методы. Возможно, существует код, который, по вашему мнению, должен быть в SystemMenu, который вы перемещаете в MenuItem и т. Д. Но в целом этот метод работает хорошо.

6 голосов
/ 05 марта 2009

Ваше решение кажется вполне разумным.

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

3 голосов
/ 07 марта 2009

Я не вижу твоей проблемы. Очевидно, вы используете C #, который содержит объекты в качестве ссылок, а не экземпляров. Это означает, что совершенно нормально иметь перекрестные ссылки или даже собственные ссылки.

в C ++ и других языках, где объекты более сложны, чем у вас, могут возникнуть проблемы, которые обычно решаются с помощью ссылок или указателей, но C # должен подойти.

Скорее всего, ваша проблема в том, что вы пытаетесь каким-то образом следовать всем ссылкам, что приводит к циклической ссылке. LINQ использует отложенную загрузку для решения этой проблемы. Например, LINQ не будет загружать компанию или сотрудника, пока вы не ссылаетесь на них. Вам просто нужно избегать таких ссылок дальше, чем один уровень.

Однако вы не можете добавить две таблицы в качестве внешнего ключа друг друга, иначе вы никогда не сможете удалить какую-либо запись, поскольку удаление сотрудника потребует сначала удаления компании, но вы не можете удалить компанию без удаляя сотрудника. Как правило, в этом случае вы будете использовать только один в качестве реального внешнего ключа, другой будет просто псевдо-FK (то есть тот, который используется в качестве FK, но не имеет включенных ограничений). Вы должны решить, какие отношения важнее.

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

Кроме того, избегайте создания новых объектов в конструкторе в этих ситуациях. Например, если ваш объект Employee создает новый объект Company, который включает в себя новый объект employee, созданный для сотрудника, он в конечном итоге исчерпает память. Вместо этого передайте объекты, уже созданные в конструктор, или установите их после построения, возможно, с помощью метода инициализации.

Например:

Company c = GetCompany("ACME Widgets");
c.AddEmployee(new Employee("Bill"));

затем, в AddEmployee, вы устанавливаете компанию

public void AddEmployee(Employee e)
{
    Employees.Add(e);
    e.Company = this;
}
2 голосов
/ 05 марта 2009

Может быть, самореферентный шаблон GoF Composite - это порядок здесь. Меню имеет коллекцию конечных элементов MenuItem, и оба имеют общий интерфейс. Таким образом, вы можете составить меню из меню и / или элементов меню. В схеме есть таблица с внешним ключом, который указывает на собственный первичный ключ. Так же работает и с пешеходным меню.

1 голос
/ 08 марта 2009

С точки зрения доменного дизайна вы можете избежать двунаправленных связей между объектами, где это возможно. Выберите один «объединенный корень» для хранения отношений и используйте другую сущность только при переходе от объединенного корня. Я стараюсь избегать двунаправленных отношений там, где это возможно. Из-за ЯГНИ, и это заставит вас задать вопрос "что было первым: курица или яйцо?" Иногда вам все равно понадобятся двунаправленные ассоциации, затем выберите одно из решений, упомянутых ранее.

/// This is the aggregate root
public class Company
{
    public string Name { get; set; }
    public IList<Employee> Employees { get; private set; }
    public Employee ContactPerson { get; set; }
}

/// This isn't    
public class Employee
{
    public string FullName { get; set; }
}
1 голос
/ 07 марта 2009

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

0 голосов
/ 09 марта 2009

Спасибо всем, кто ответил, некоторые действительно интересные подходы! В конце концов мне нужно было что-то сделать в спешке, так что вот что я придумал:

Введено третье лицо под названием WellKnownContact и соответствующее WellKnownContactType enum:

public class Company
{
    public string Name { get; set; }
    public IList<Employee> Employees { get; private set; }
    private IList<WellKnownEmployee> WellKnownEmployees { get; private set; }
    public Employee ContactPerson
    {
        get
        {
            return WellKnownEmployees.SingleOrDefault(x => x.Type == WellKnownEmployeeType.ContactPerson);
        }
        set
        {                
            if (ContactPerson != null) 
            {
                // Remove existing WellKnownContact of type ContactPerson
            }

            // Add new WellKnownContact of type ContactPerson
        }
    }
}

public class Employee
{
    public Company EmployedBy { get; set; }
    public string FullName { get; set; }
}

public class WellKnownEmployee
{
    public Company Company { get; set; }
    public Employee Employee { get; set; }
    public WellKnownEmployeeType Type { get; set; }
}

public enum WellKnownEmployeeType
{
    Uninitialised,
    ContactPerson
}

Это кажется немного громоздким, но обходит проблему циклических ссылок и четко отображает на БД, что избавляет от попыток заставить LINQ to SQL сделать что-то слишком умное! Также допускается несколько типов «известных контактов», которые обязательно появятся в следующем спринте (так что на самом деле это не ЯГНИ!).

Интересно, что когда я придумал надуманный пример «Компания / Сотрудник», мне стало намного проще думать, в отличие от довольно абстрактных сущностей, с которыми мы действительно имеем дело.

0 голосов
/ 08 марта 2009

Вы можете использовать внешние ключи в базе данных, где две таблицы ссылаются друг на друга. На ум приходят два способа:

  1. Дочерний столбец по умолчанию в родительском элементе изначально равен нулю и обновляется только после вставки всех дочерних строк.
  2. Вы откладываете проверку ограничений до времени фиксации. Это означает, что вы можете сначала вставить родителя с изначально неработающей ссылкой на потомка, а затем вставить потомка. Одной из проблем с отложенной проверкой ограничений является то, что вы можете получить исключения базы данных во время фиксации, что часто неудобно во многих структурах БД. Кроме того, это означает, что вам необходимо знать первичный ключ ребенка, прежде чем вставлять его, что может быть неудобно в вашей настройке.

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

Многие СУБД поддерживают отложенную проверку ограничений. Возможно, ваша тоже, хотя вы не упоминаете, какую СУБД вы используете

...