Как бы я смоделировал данные, которые являются иерархическими и реляционными в системе документов-ориентированной базы данных, такой как RavenDB?
Документно-ориентированные базы данных (особенно RavenDB) действительно меня заинтриговали, и я хочу немного поиграть с ними. Однако, как человек, который очень привык к реляционному отображению, я пытался придумать, как правильно моделировать данные в базе данных документов.
Скажем, у меня есть приложение CRM со следующими объектами в моем приложении C# (пропуская ненужные свойства):
public class Company
{
public int Id { get; set; }
public IList<Contact> Contacts { get; set; }
public IList<Task> Tasks { get; set; }
}
public class Contact
{
public int Id { get; set; }
public Company Company { get; set; }
public IList<Task> Tasks { get; set; }
}
public class Task
{
public int Id { get; set; }
public Company Company { get; set; }
public Contact Contact { get; set; }
}
Я думал положить все это в Company
документ, так как контакты и задачи не имеют цели вне компаний, и большую часть времени запрос для задачи или контактов также будет отображать информацию о связанной компании.
Проблема идет с Task
юридические лица. Скажем, бизнес требует, чтобы задача ВСЕГДА была связана с компанией, но, возможно, также связана с задачей.
В реляционной модели это легко, так как у вас просто есть Tasks
стол и есть Company.Tasks
относятся ко всем задачам для компании, а Contact.Tasks
показывать только задачи для конкретной задачи.
Для моделирования этого в базе данных документов я подумал о следующих трех идеях:
Задачи модели в виде отдельного документа. Это кажется своего рода антидокументарной базой данных, так как большую часть времени, когда вы смотрите на компанию или контакт, вы захотите увидеть список задач, поэтому вам придется много выполнять объединения документов.
Сохраняйте задачи, не связанные с контактом, в
Company.Tasks
список и поставить задачи, связанные с контактом в списке для каждого отдельного контакта. К сожалению, это означает, что если вы хотите просмотреть все задачи для компании (которых, вероятно, будет много), вам нужно объединить все задачи для компании со всеми задачами для каждого отдельного контакта. Я также вижу, что это сложно, когда вы хотите отделить задачу от контакта, так как вам нужно перенести ее из контакта в компанию.Храните все задачи в
Company.Tasks
список, и каждый контакт имеет список значений идентификатора для задач, с которыми он связан. Это кажется хорошим подходом, за исключением необходимости принимать значения идентификаторов вручную и составлять подсписокTask
лица для контакта.
Каков рекомендуемый способ моделирования этих данных в базе данных, ориентированной на документы?
2 ответа
Используйте денормализованные ссылки:
http://ravendb.net/faq/denormalized-references
по сути, у вас есть класс DenormalizedReference:
public class DenormalizedReference<T> where T : INamedDocument
{
public string Id { get; set; }
public string Name { get; set; }
public static implicit operator DenormalizedReference<T> (T doc)
{
return new DenormalizedReference<T>
{
Id = doc.Id,
Name = doc.Name
}
}
}
ваши документы выглядят так - я реализовал интерфейс INamedDocument - это может быть все, что вам нужно, хотя:
public class Company : INamedDocument
{
public string Name{get;set;}
public int Id { get; set; }
public IList<DenormalizedReference<Contact>> Contacts { get; set; }
public IList<DenormalizedReference<Task>> Tasks { get; set; }
}
public class Contact : INamedDocument
{
public string Name{get;set;}
public int Id { get; set; }
public DenormalizedReference<Company> Company { get; set; }
public IList<DenormalizedReference<Task>> Tasks { get; set; }
}
public class Task : INamedDocument
{
public string Name{get;set;}
public int Id { get; set; }
public DenormalizedReference<Company> Company { get; set; }
public DenormalizedReference<Contact> Contact { get; set; }
}
Теперь сохранение задачи работает точно так же, как и раньше:
var task = new Task{
Company = myCompany,
Contact = myContact
};
Однако, потянув все это назад, вы получите только денормализованную ссылку на дочерние объекты. Для их увлажнения я использую индекс:
public class Tasks_Hydrated : AbstractIndexCreationTask<Task>
{
public Tasks_Hydrated()
{
Map = docs => from doc in docs
select new
{
doc.Name
};
TransformResults = (db, docs) => from doc in docs
let Company = db.Load<Company>(doc.Company.Id)
let Contact = db.Load<Contact>(doc.Contact.Id)
select new
{
Contact,
Company,
doc.Id,
doc.Name
};
}
}
И использование вашего индекса для получения гидратированных задач:
var tasks = from c in _session.Query<Projections.Task, Tasks_Hydrated>()
where c.Name == "taskmaster"
select c;
Который я считаю довольно чистым:)
Как и при конструктивном обсуждении, общее правило заключается в том, что если вам когда-либо понадобится загружать дочерние документы отдельно, а не в родительский документ. Будь то для редактирования или просмотра - вы должны смоделировать его с его собственным идентификатором, как его собственный документ. Использование метода выше делает это довольно простым.
Я новичок в документировании БД, а также... так с долей соли...
В качестве контрастного примера... если вы находитесь в Твиттере и у вас есть список людей, на которых вы подписаны, который содержит список их твитов... вы не будете перемещать их твиты в свой аккаунт в Твиттере, чтобы читать их, и если вы снова отправите твит, у вас будет только копия, а не оригинал.
Таким же образом, мое мнение таково: если Задачи принадлежат компании, они остаются внутри Компании. Компания - Совокупный Корень Задач. Контакты могут содержать только ссылки (идентификаторы) или копии Задач и не могут изменять их напрямую. Если у вашего контакта есть "копия" задания, это нормально, но для того, чтобы изменить задание (например, пометить его как завершенное), вы должны изменить задание через его Агрегированный корень (компания). Поскольку копия может быстро устареть, создается впечатление, что вы хотите, чтобы копия существовала только в памяти и при сохранении контакта, вы сохраняете только ссылки на задачи.