StubDb — база данных на стабах для быстрого прототипирования и не только

в 6:26, , рубрики: .net, entity framework, orm, Программирование, метки: , , , ,

О чем это

Доброго времени суток!

Я бы хотел рассказать о StubDb — библиотеке для быстрого прототипирования приложений и легкого юнит тестирования. Она позволяет заменять работу с реальной базой данных, на работу с данными хранимыми в памяти/файле. Это дает возможность сконцентрироваться на классах доменной модели, а не на особенностях хранения данных. StubDb использует принцип работы Entity Framework Code First, что делает удобным их совместное использование, но может использоваться отдельно.

Чтобы написать Data Persistence Layer, нужно затратить немало усилий. Работа с базой данных это — хлопотно: начиная от подключения и редактирования конфигов, и заканчивая написанием запросов. Раньше, пока ORM еще не были так популярны, много времени занимало написание однообразных SQL запросов. С развитием ORM, меньше времени тратится непосредственно на SQL, но вместо этого необходимо изучать особенности самих ORM фреймворков.

В теории все просто: программист работает с доменными объектами, изменения в них транслируются в базу данных легко и безболезненно. Но на практике особенности фреймворков часто вызывают недоумение и прострацию. Например, на данный момент на StackOverflow по ASP.NET MVC 77,852 вопросов, а по Entity Framework 33,276, меньше, но не значительно. А ведь в идеале EF должен просто незаметно делать свою работу.

Конечно, без БД не обойтись. Но когда приложение или новая фича находятся в начальной стадии проектирования и разработки, в БД нет необходимости. На этом этапе вполне достаточно: иметь доменную модель классов, хранить данные в этой модели вместе с взаимосвязями между отдельными классами (один к одному, многие ко многим), получать данные доменной модели, учитывая эти связи. StubDb реализует этот минимальный набор требований. Используя StubDb на начальном этапе разработки, можно избежать сложностей работы с БД и ORM, но при этом иметь возможность хранить данные в доменной модели и легко ее менять, не нарушая работоспособности приложения.

Зачем это надо

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

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

Стаб режим для приложения — разверни приложение в 3 клика
Мне пришлось поработать довольно много на проектах энтерпрайс уровня и, к сожалению, в моей практике это значило — большая куча плохо и разнообразно написанного кода, которая работает в продакшене и на которой зарабатывают реальные деньги. При этом, чтобы пофиксить любой баг, даже в пользовательском интерфейсе, нужно потратить уйму времени на разворачивание проекта на машине разработчика. Хотелось бы, чтобы в идеальном мире для проекта можно было получить исходники из репозитория, поменять режим работы на режим разработчика в конфиге и проект сразу бы запустился.
Со StubDb это может быть реальностью, если реализовать Stub DAL наряду с реальным DAL, и, благодаря этому, иметь возможность работать с проектом без доступа к реальной базе данных, веб-сервисам и прочим источникам данных, используемых на проекте. Используя Dependency Injection, и имплементируя интерфейсы классов доступа к данным (репозиториев), для реальных данных и для стабов, можно легко переключаться с реальных источников данных на стабы. Часто доступ к реальным данным не обязателен, а иногда бывает затруднен (нет интернет соединения, проблемы на сервере), в это время можно переключиться на стабы. Я уже начал использовать StubDb в таком режиме на нескольких проектах. Время покажет насколько это применимо на практике.

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

Как этим пользоваться

Лучше всего показать на примере. Возьмем как пример простую доменную модель (ДМ):

public class Student 
{ 
       public int Id { get; set; } 
       public string FirstName { get; set; } 
       public string LastName { get; set; } 
       public virtual List<Enrollment> Enrollments { get; set; } 
} 
 
public class Course 
{ 
       public int Id { get; set; } 
       public string Title { get; set; } 
       public int Credits { get; set; } 
       public virtual Department Department { get; set; } 
       public virtual List<Enrollment> Enrollments { get; set; } 
} 
 
public class Department 
{ 
       public int Id { get; set; } 
       public string Name { get; set; } 
       public decimal Budget { get; set; } 
       public virtual List<Course> Courses { get; set; } 
} 
 
public class Enrollment  
{ 
       public int Id { get; set; } 
       public Grade Grade { get; set; } 
       public Student Student { get; set; } 
       public Course Course { get; set; } 
} 
 
public enum Grade 
{ 
       NoGrade, A, B, C, D, F 
} 

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

Для того чтобы работать с приведенным примером ДМ, создадим класс контекста. По аналогии с Entity Framework, нужно наследоваться от базового StubContext.

Для каждой сущности, к которой мы будет строить отдельные запросы, создаем свойство типа StubSet, опять же, по аналогии с EF.

Должно получиться так:

public class SampleStubContext: StubContext 
{ 
       public StubSet<Course> Courses { get; set; } 
       public StubSet<Department> Departments { get; set; } 
       public StubSet<Enrollment> Enrollments { get; set; } 
       public StubSet<Student> Students { get; set; } 
} 

Это все минимальные настройки, необходимые для работы.

Теперь добавим в контекст данные:

    var context = new SampleStubContext();

    var departmentIt = new Department() {Budget = 40000, Name = "IT"};
    var departmentMath = new Department() {Budget = 14000, Name = "Math"};

    context.Departments.Add(departmentIt);
    context.Departments.Add(departmentMath); 
    //now departmentMath.Id == 2 like with EF it is updated after adding to context

    var courseJavascript = new Course() {Department = departmentIt, Title = "JavaScript basics"};
    var courseAlgorithms = new Course() {Department = departmentIt, Title = "Algorithms"};
    var courseAlgebra = new Course() {Department = departmentMath, Title = "Algebra"};

    context.Courses.Add(courseAlgebra);
    context.Courses.Add(courseAlgorithms);
    context.Courses.Add(courseJavascript);

    var studentAlex = new Student() {FirstName = "Alex", LastName = "Black"};
    var studentPer = new Student() {FirstName = "Per", LastName = "Sundin"};

    context.Students.Add(studentAlex);
    context.Students.Add(studentPer);

    context.Enrollments.Add(new Enrollment(){Student = studentAlex, Course = courseAlgebra, Grade = Grade.B});
    context.Enrollments.Add(new Enrollment(){Student = studentAlex, Course = courseAlgorithms, Grade = Grade.A});

    context.Enrollments.Add(new Enrollment() { Student = studentPer, Course = courseAlgorithms, Grade = Grade.C });
    context.Enrollments.Add(new Enrollment() { Student = studentPer, Course = courseJavascript, Grade = Grade.A });

У нас есть 2 кафедры Math и IT, в Math курс Algebra, в IT JavaScript и Algorithms. Есть два студента Alex и Per, Alex записан на курсы Algebra и Algorithms, Per — Algorithms и JavaScript

Напишем пару запросов к данным в контексте:

    var departmentItFromContext = context.Departments.Query().Single(x => x.Name == "IT");
    var coursesForIt = departmentItFromContext.Courses; //coursesForIt == { Javascript, Algorithms };

    var courseAlgorithmsFromContext = context.Courses.Query(2).First(x => x.Title == "Algorithms");
    var studentsForAlgorithms = courseAlgorithmsFromContext.Enrollments.Select(x => x.Student).ToList(); 
    // studentsForAlgorithms == {Alex, Per}

Запрашивая данные из доменной модели через контекст, мы получаем их с учетом взаимосвязей между объектами. Курсы для IT кафедры — Javascript, Algorithms; студенты записанные на Algorithms — Alex и Per.

Изменим данные в контексте:

    var alexAlgorithmsEnrollment = context.Enrollments.Query()
        .FirstOrDefault(x => x.Student.FirstName == "Alex" && x.Course.Title == "Algorithms");
    context.Enrollments.Remove(alexAlgorithmsEnrollment);

    var courseAlgorithmsFromContextUpdated = context.Courses.Query(2).First(x => x.Title.Contains("Algorithms"));
    var studentsForAlgorithmsUpdated = courseAlgorithmsFromContextUpdated.Enrollments.Select(x => x.Student); 
    //studentsForAlgorithms == {Per}

    courseAlgorithms.Department = departmentMath;
    courseAlgorithms.Title = "Methods of optimisation";
    context.Courses.Update(courseAlgorithms);

    var departmentMathFromContext = context.Departments.Query().Single(x => x.Name == "Math");
    var coursesForMath = departmentMathFromContext.Courses; 
    //coursesForMath == { Algebra, Methods of optimisation };

Мы удалили Enrollment для Алекса на Algorithms, после этого в списке студентов подписанных на Algorithms Алекса уже нет. Мы поменяли кафедру для Algorithms, после этого этот курс появился в списке курсов его новой кафедры.

Примеры использования

Проект на GitHub: github.com/yegor-sytnyk/StubDb

Для удобства использования создан StubDb NuGet package.

Текст консольного приложения с доменной моделью из этой статьи тут: gist.github.com/yegor-sytnyk/c3f9ba12f5bb6b188286.

MyUniversity: github.com/yegor-sytnyk/MyUniversity. Полностью работающий проект на основе доменной модели из ContosoUniversity ASP.NET MVC tutorial. Реализован c DAL на StubDb, на примере этого проекта видно как можно использовать StubDb совместно с Entity Framework.

Буду рад замечаниям и предложениям. Надеюсь, это может быть полезно еще кому-нибудь.

Как это работает

Для тех, кому интересны детали реализации.

Данные из ДМ хранятся в сущностях (Entities) и связях (Connections).

Сущности — это классы из ДМ (Student, Course, т.д.). Для каждого типа сущности, хранится словарь со всеми текущими значениями этой сущности и ключом айди сущности. Как и в энтити фреймворке, ключ сущности может называться либо Id либо {ИмяКласса}Id, например StudentId, для сущности Student. Чтобы излишне не усложнять реализацию, Id должно иметь тип Int.

Не зависимо от типа связи (один к одному, многие ко многим или один ко многим), каждый элемент коллекции связей хранит данные о связях между двумя типами сущностей. Например, если у студента есть список курсов на которые он подписан, то в соответствующей связи для студента и курса будут храниться парные связи {студент(Id)-курс(Id)}. Таким образом, можно смоделировать все типы связей: один к одному, один ко многим, многие ко многим. Для случая один к одному будет хранится единственный элемент связи, для случая множественного типа связи будет хранится несколько связей.

Используя сущности и связи, можно записывать, читать данные из контекста ДМ, учитывая связи между объектами.
Во время добавления данных, используя Add/Update методы у StubSet, обновляются внутренние entities/connections в контексте. При вычитывании данных через метод Query, в сущностях инициализируются навигационные свойства, исходя из существующих связей. Если, например, у курса есть свойство типа отдел, то в методе Query будет поиск по связям — есть ли связь которая связывает курс с этим айди с каким-то из отделов. Если такая связь есть, то будет выставлен соответствующий отдел, иначе свойство отдел будет выставлено в null. По тому же принципу инициализируются навигационные свойства с коллекцией объектов, например, лист зачислений на курсы для студента.

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

Автор: Yeggor

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js