company_banner

Учебный курс. Создание модели данных Entity Framework для приложения ASP.NET MVC

http://www.asp.net/entity-framework/tutorials/creating-an-entity-framework-data-model-for-an-asp-net-mvc-application
  • Перевод
На примере веб-приложения для Contoso University мы продемонстрируем создание приложений ASP.NET MVC с использованием Entity Framework, в функциональность которого будут входить такие возможности как принятие стуентов, создание курсов и назначение преподавателей.

Данные учебные материалы объяснят по шагам процесс создания веб-приложения для Contoso University. Вы можете скачать готовое приложение или создать его согласно приведенной последовательности шагов. Примеры приведены на C#, примеры кода доступны в C# и VB. Если у вас есть вопросы, косвенно касающиеся учебных материалов, вы можете задать их ASP.NET Entity Framework forum или Entity Framework and LINQ to Entities forum.

Обучение предполагает наличие знаний по работе с ASP.NET MVC в Visual Studio, в противном случае хорошее место для начала обучения ASP.NET MVC Tutorial. Если вы предпочитаете работать с ASP.NET Web Forms, обратите внимание на Getting Started with the Entity Framework и Continuing with the Entity Framework.

Перед началом удостоверьтесь в том, что у вас установлено следующее ПО:

The Contoso University


Приложение, которые вы разработаете, является простым вебсайтом университета.

clip_image001

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

clip_image002

clip_image003

clip_image004

UI близок по стилю к тому, что генерируется шаблонами по умолчанию, поэтому акцент будет на вопросах использования Entity Framework.

Подходы к разработке с Entity Framework


Исхоя из диаграммы, имеется три подхода к работе с данными в Entity Framework: Database First, Model First, и Code First.

clip_image005

Database First

В случае уже имеющейся базы данных Entity Framework может автоматически создать модель данных, состоящую из классов и свойств, соответствующих объектам базы даных (таким, как таблицы и столбцы). Информация о структуре базы (store schema), модель данных (conceptual model) и маппинг их друг на друга содержится в XML в файле .edmx. Visual Studio предоставляет графический дизайнер Entity Framework, с помощью которого можно просматривать и редактировать .edmx. Части Getting Started With the Entity Framework и Continuing With the Entity Framework в материалах о Web Forms используют подход Database First.

Model First

Если базы нет, вы можете начать с создания модели данных, используя дизайнер Entity Framework Visual Studio. После окончания работ над моделью дизайнер сгенерирует DDL (data definition language)-код для создания базы. В этом подходе для хранения информации о модели и маппингах также используется .edmx. What's New in the Entity Framework 4 включает небольшой пример разработки с использованием данного подхода.

Code First

Вне зависимости от наличия базы вы можете вручную написать код классов и свойств, соответствующих сущностям в базе и использовать этот код с Entity Framework без использования файла .edmx. Именно поэтому можно порой увидеть, как этот подход называют code only, хотя официальное наименование Code First. Маппинг между store schema и conceptual model в represented by your code is handled by convention and by a special mapping API. Если базы ещё нет, Entity Framework может создать, удалить или пересоздать её в случае изменений в модели.

API доступа к данным, разработанное для Code First, основано на классе DbContext. API может быть использован также и в процессе разработки с подходами Database First и Model First. Для дополнительной информации смотрите When is Code First not code first? В блоге команды разработки Entity Framework.

POCO (Plain Old CLR Objects)


По умолчанию для подходов Database First и Model First классы модели данных наследуются от EntityObject, который и предоставляет функциональность Entity Framework. Это значит, что эти классы не являются persistence ignorant и, таким образом, не полностью соответствуют одном из условий domain-driven design. Все подходы к разработке с Entity Framework также могут работать с POCO (plain old CLR objects), что, в целом, значит, что они являются persistence-ignorant из-за отсутствия наследования EntityObject.

Создание веб-приложения MVC


Откройте Visual Studio и создайте новый проект "ContosoUniversity", используя ASP.NET MVC 3 Web Application:

clip_image006

В New ASP.NET MVC 3 Project выберите шаблон Internet Application и движок представления Razor, снимите галочку с Create a unit test project и нажмите OK.

clip_image007

Настройка стилей

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

Для настройки меню Contoso University, в Views\Shared\_Layout.cshtml замените текст в h1 и ссылки в меню, как в примере:

<!DOCTYPE html> 
<html> 
<head> 
    <title>@ViewBag.Title</title> 
    <link href="@Url.Content("~/Content/Site.css")" rel="stylesheet" type="text/css" /> 
    <script src="@Url.Content("~/Scripts/jquery-1.5.1.min.js")" type="text/javascript"></script> 
</head> 
<body> 
    <div class="page"> 
       <div id="header"> 
            <div id="title"> 
                <h1>Contoso University</h1> 
            </div> 
 
            <div id="logindisplay"> 
                @Html.Partial("_LogOnPartial") 
            </div> 
            <div id="menucontainer"> 
                <ul id="menu"> 
                    <li>@Html.ActionLink("Home", "Index", "Home")</li> 
                    <li>@Html.ActionLink("About", "About", "Home")</li> 
                    <li>@Html.ActionLink("Students", "Index", "Student")</li> 
                    <li>@Html.ActionLink("Courses", "Index", "Course")</li> 
                    <li>@Html.ActionLink("Instructors", "Index", "Instructor")</li> 
                    <li>@Html.ActionLink("Departments", "Index", "Department")</li> 
                </ul> 
            </div> 
        </div> 
        <div id="main"> 
            @RenderBody() 
        </div> 
        <div id="footer"> 
        </div> 
    </div> 
</body> 
</html>

В Views\Home\Index.cshtml удалите всё в теге h2.

В Controllers\HomeController.cs замените "Welcome to ASP.NET MVC!" на "Welcome to Contoso University!"

В Content\Site.css для смещения меню влево совершите следующие изменения:

  • В блок #main добавьте clear: both:
#main  
{ 
    clear: both; 
    padding: 30px 30px 15px 30px; 
    background-color: #fff; 
    border-radius: 4px 0 0 0; 
    -webkit-border-radius: 4px 0 0 0; 
    -moz-border-radius: 4px 0 0 0; 
}

  • В блоках nav и #menucontainer добавьте clear: both; float: left:
nav,  
#menucontainer { 
    margin-top: 40px; 
    clear: both; 
    float: left; 
}

Запустите проект.

clip_image001[1]

Создание модели данных


Далее создадим первые классы-сущности для Contoso University. Мы начнём со следующих трёх сущностей:

clip_image008

Установлена связь один-ко-многим между сущностями Student и Enrollment, и связь один-ко-многим между Course и Enrollment. Другими словами, студент может посещать любое количество курсов, и курс может иметь любое количество студентов, посещающих его.

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

Note: компиляция проекта без созданных классов для этих сущностей вызовет ошибки компиляторов.

Сущность Student

clip_image009

В папке Models создайте Student.cs и замените сгенерированный код на:

using System; 
using System.Collections.Generic; 
 
namespace ContosoUniversity.Models 
{ 
    public class Student 
    { 
        public int StudentID { get; set; } 
        public string LastName { get; set; } 
        public string FirstMidName { get; set; } 
        public DateTime EnrollmentDate { get; set; } 
        public virtual ICollection<Enrollment> Enrollments { get; set; } 
    } 
}

Свойство StudentID будет первичным ключом соответствующей таблицы. По умолчанию, Entity Framework воспринимает свойство с ID или classnameID как первичный ключ.

Свойство Enrollmentsnavigation property. Navigation properties содержат другие сущности, относящиеся к текущей. В данном случае свойство Enrollments содержит в себе все сущности Enrollment, ассоциированные с текущей сущностью Student. Другими словами, если некая запись Student в базе данных имеет связь с двумя записями Enrollment (записей, содержащих значения первичных ключей для студента в поле внешнего ключа StudentID), свойство этой записи Enrollments будет содержать две сущности Enrollment.

Navigation properties обычно помечаются модификатором virtual дабы использовать возможность Entity Framework, называемую lazy loading. (суть Lazy loading будет объяснена позже, в Reading Related Data) Если navigation property может содержать несколько сущностей (в связях многие-ко-многим и один-ко-многим), его тип должен быть ICollection.

Сущность Enrollment

clip_image010

В папке Models создайте Enrollment.cs со следующим содержанием:

using System; 
using System.Collections.Generic; 
 
namespace ContosoUniversity.Models 
{ 
    public class Enrollment 
    { 
        public int EnrollmentID { get; set; } 
        public int CourseID { get; set; } 
        public int StudentID { get; set; } 
        public decimal? Grade { get; set; } 
        public virtual Course Course { get; set; } 
        public virtual Student Student { get; set; } 
    } 
}

Знак вопроса после указания типа decimal указывает на то, что свойство Grade nullable. Оценка, поставленная в null отличная от нулевой оценки— null обозначает то, что оценка еще не выставлена, тогда как 0 – уже значение.

Свойство StudentID является внешним ключом, и соответствующее navigation property Student. Сущность Enrollment ассоциирована с одной сущностью Student, поэтому свойство может содержать только одну сущность указанного типа (в отличие Student.Enrollments).

Свойство CourseID является внешним ключом, и соответствующее navigation property Course. Сущность Enrollment ассоциирована с одной сущностью Course.

Сущность Course

clip_image011

В папке Models создайтеCourse.cs со следующим содержанием:

using System; 
using System.Collections.Generic; 
 
namespace ContosoUniversity.Models 
{ 
    public class Course 
    { 
        public int CourseID { get; set; } 
        public string Title { get; set; } 
        public int Credits { get; set; } 
        public virtual ICollection<Enrollment> Enrollments { get; set; } 
    } 
}

Свойство Enrollments — navigation property. Сущность Course может ассоциироваться с бесконечным множеством сущностей Enrollment.

Создание Database Context


Главный класс, координирующий функциональность Entity Framework для текущей модели данных называется database context. Данный класс наследуется от System.Data.Entity.DbContext. В коде вы определяете, какие сущности включить в модель данных, и также можете определять поведение самого Entity Framework. В нашем коде этот класс имеет название SchoolContext.

Создайте папку DAL и в ней новый класс SchoolContext.cs:

using System; 
using System.Collections.Generic; 
using System.Data.Entity; 
using ContosoUniversity.Models; 
using System.Data.Entity.ModelConfiguration.Conventions; 
 
namespace ContosoUniversity.Models 
{ 
    public class SchoolContext : DbContext 
    { 
        public DbSet<Student> Students { get; set; } 
        public DbSet<Enrollment> Enrollments { get; set; } 
        public DbSet<Course> Courses { get; set; } 
 
        protected override void OnModelCreating(DbModelBuilder modelBuilder) 
        { 
            modelBuilder.Conventions.Remove<PluralizingTableNameConvention>(); 
        } 
    } 
}

Код создаёт свойство DbSet для каждого множества сущностей. В терминологии Entity Framework множество сущностей (entity set) относится к таблице базы данных, и сущность относится к записи в таблице.

Содержимое метода OnModelCreating защищает имена таблиц от плюрализации, и, если вы этого не делаете, то получаете такие имена таблиц, как Students, Courses, Enrollments. В ином случае имена таблиц будут Student, Course, Enrollment. Разработчики спорят на тему того, нужно ли плюрализовывать имена таблиц или нет. Мы используем одиночную форму, но важен тот момент, что вы можете выбрать, включать эту строчку в код или нет.

(Этот класс находится в namespace Models потому, что в некоторых ситуациях подход Code First подразумевает нахождение классов сущностей и контекста в одном и том же namespace.)

Определение Connection String


Вам не нужно определять connection string. Если вы не определили эту строку, Entity Framework автоматически создаст базу данных SQL Server Express. Мы, однако, будем работать с SQL Server Compact, и вам необходимо будет создать строку подключения с указанием на это.

Откройте Web.config и добавьте новую строку подключения в коллекцию connectionStrings. (Убедитесь, что вы обновляете Web.config в корне проекта, так как есть еще один Web.config в папке Views, который трогать не надо.)

<add name="SchoolContext" connectionString="Data Source=|DataDirectory|School.sdf"
providerName="System.Data.SqlServerCe.4.0"/>

По умолчанию Entity Framework ищет строку подключения, названную также как object context class. Строка подключения, которую вы добавили, определяет базу данных School.sdf, находящуюся в папке App_data и SQL Server Compact.

Инициализация базы данных с тестовыми данными


Entity Framework может автоматически создать базу данных при запуске приложения. Вы можете указать, что это должно выплоняться при каждом запуске приложения или только тогда, когда модель рассинхронизирована с существующей базой. Вы можете также написать класс с методом, который Entity Framework будет автоматически вызывать перед созданием базы для использования её с тестовыми данными. Мы укажем, что база должна удаляться и пересоздаваться при изменении модели.

В папке DAL создайте новый класс SchoolInitializer.cs с кодом, с помощью которого база будет создана при необходимости и заполнена тестовыми данными.

using System; 
using System.Collections.Generic; 
using System.Linq; 
using System.Web; 
using System.Data.Entity; 
using ContosoUniversity.Models; 
 
namespace ContosoUniversity.DAL 
{ 
    public class SchoolInitializer : DropCreateDatabaseIfModelChanges<SchoolContext> 
    { 
        protected override void Seed(SchoolContext context) 
        { 
            var students = new List<Student> 
            { 
                new Student { FirstMidName = "Carson",   LastName = "Alexander", EnrollmentDate = DateTime.Parse("2005-09-01") }, 
                new Student { FirstMidName = "Meredith", LastName = "Alonso",    EnrollmentDate = DateTime.Parse("2002-09-01") }, 
                new Student { FirstMidName = "Arturo",   LastName = "Anand",     EnrollmentDate = DateTime.Parse("2003-09-01") }, 
                new Student { FirstMidName = "Gytis",    LastName = "Barzdukas", EnrollmentDate = DateTime.Parse("2002-09-01") }, 
                new Student { FirstMidName = "Yan",      LastName = "Li",        EnrollmentDate = DateTime.Parse("2002-09-01") }, 
                new Student { FirstMidName = "Peggy",    LastName = "Justice",   EnrollmentDate = DateTime.Parse("2001-09-01") }, 
                new Student { FirstMidName = "Laura",    LastName = "Norman",    EnrollmentDate = DateTime.Parse("2003-09-01") }, 
                new Student { FirstMidName = "Nino",     LastName = "Olivetto",  EnrollmentDate = DateTime.Parse("2005-09-01") } 
            }; 
            students.ForEach(s => context.Students.Add(s)); 
            context.SaveChanges(); 
 
            var courses = new List<Course> 
            { 
                new Course { Title = "Chemistry",      Credits = 3, }, 
                new Course { Title = "Microeconomics", Credits = 3, }, 
                new Course { Title = "Macroeconomics", Credits = 3, }, 
                new Course { Title = "Calculus",       Credits = 4, }, 
                new Course { Title = "Trigonometry",   Credits = 4, }, 
                new Course { Title = "Composition",    Credits = 3, }, 
                new Course { Title = "Literature",     Credits = 4, } 
            }; 
            courses.ForEach(s => context.Courses.Add(s)); 
            context.SaveChanges(); 
 
            var enrollments = new List<Enrollment> 
            { 
                new Enrollment { StudentID = 1, CourseID = 1, Grade = 1 }, 
                new Enrollment { StudentID = 1, CourseID = 2, Grade = 3 }, 
                new Enrollment { StudentID = 1, CourseID = 3, Grade = 1 }, 
                new Enrollment { StudentID = 2, CourseID = 4, Grade = 2 }, 
                new Enrollment { StudentID = 2, CourseID = 5, Grade = 4 }, 
                new Enrollment { StudentID = 2, CourseID = 6, Grade = 4 }, 
                new Enrollment { StudentID = 3, CourseID = 1            }, 
                new Enrollment { StudentID = 4, CourseID = 1,           }, 
                new Enrollment { StudentID = 4, CourseID = 2, Grade = 4 }, 
                new Enrollment { StudentID = 5, CourseID = 3, Grade = 3 }, 
                new Enrollment { StudentID = 6, CourseID = 4            }, 
                new Enrollment { StudentID = 7, CourseID = 5, Grade = 2 }, 
            }; 
            enrollments.ForEach(s => context.Enrollments.Add(s)); 
            context.SaveChanges(); 
        } 
    } 
}

Метод Seed принимает объект контекста базы как входящий параметр и использует его для добавления новых сущностей в базу. Для каждого типа сущности код создает коллекцию новых сущностей, добавляя их в соответствующее свойство DbSet, и потом сохраняет изменения в базу. Нет необходимости в вызове SaveChanges после каждой группы сущностей, как сделано у нас, но это помогает определить проблему в случае возникновения исключений.

Измените Global.asax.cs для того, чтобы наш код вызывался при каждом запуске приложения:

  • Добавьте using:
using System.Data.Entity; 
using ContosoUniversity.Models; 
using ContosoUniversity.DAL;

  • В методе Application_Start вызовите метод Entity Framework, который запускает код инициализации базы:
Database.SetInitializer<SchoolContext>(new SchoolInitializer());

Приложение настроено таким образом, что при каждом первом обращении к базе данных после запуска приложения Entity Framework сравнивает базу с моделью ( класс SchoolContext), и в случае рассинхронизации приложение удаляет и пересоздает базу.

Note при развертывании приложения на продакшн-сервер вы должны удалить весь код, который инициализирует базу тестовыми данными.

Далее вы создадите веб-страницу для отображения данных, и процесс запроса данных автоматически инициирует создание базы. Вы начнете с нового контроллера, но перед этим, соберите проект для того, чтобы модель и контекстные классы стали доступны для MVC controller scaffolding.

Создание контроллера Student


Для создание контроллера Student, щелкните на папке Controllers в Solution Explorer, нажмите Add, Controller. В Add Controller совершите следующие действия и изменения и нажмите Add:

  • Controller name: StudentController.
  • Template: Controller with read/write actions and views, using Entity Framework. (по умолчанию.)
  • Model class: Student (ContosoUniversity.Models). (если этого нет, пересоберите проект)
  • Data context class: SchoolContext (ContosoUniversity.Models).
  • Views: Razor (CSHTML). (по умолчанию)
clip_image012

Откройте Controllers\StudentController.cs, вы увидите созданную переменную, инициализирующую объект контекста базы данных:

private SchoolContext db = new SchoolContext();

Действие Index собирает список студентов из свойства Students из экземпляра контекста базы данных:

public ViewResult Index() 
{ 
    return View(db.Students.ToList()); 
}

Автоматическое scaffolding было создано для множества Student. Для настройки заголовков и последовательности колонок откройте Views\Student\Index.cshtml и замените код на:

@model IEnumerable<ContosoUniversity.Models.Student> 
 
@{ 
    ViewBag.Title = "Students"; 
} 
 
<h2>Students</h2> 
 
<p> 
    @Html.ActionLink("Create New", "Create") 
</p> 
<table> 
    <tr> 
        <th></th> 
        <th>Last Name</th> 
        <th>First Name</th> 
        <th>Enrollment Date</th> 
    </tr> 
 
@foreach (var item in Model) { 
    <tr> 
        <td> 
            @Html.ActionLink("Edit", "Edit", new { id=item.StudentID }) | 
            @Html.ActionLink("Details", "Details", new { id=item.StudentID }) | 
            @Html.ActionLink("Delete", "Delete", new { id=item.StudentID }) 
        </td> 
        <td> 
            @Html.DisplayFor(modelItem => item.LastName) 
        </td> 
        <td> 
            @Html.DisplayFor(modelItem => item.FirstMidName) 
        </td> 
        <td> 
            @Html.DisplayFor(modelItem => item.EnrollmentDate) 
        </td> 
    </tr> 
} 
 
</table>

Запустите сайт, нажмите на вкладку Students.

clip_image002[1]

Закройте браузер. В Solution Explorer выберите проект ContosoUniversity. Нажмите Show all Files, Refresh и затем разверните папку App_Data.

clip_image013

Два раза щелкните на School.sdf для открытия Server Explorer, и Tables.

Note если у вас возникает ошибка после того, как вы два раза щелкаете на School.sdf, убедитесь, что вы установили Visual Studio 2010 SP1 Tools for SQL Server Compact 4.0. Если все установлено, перезапустите Visual Studio.

clip_image014

Для каждой таблицы есть свое множество сущностей + одна дополнительная таблица. EdmMetadata используется для определения Entity Framework, когда случилась разсинхронизация модели и базы.

Щелкните на одной из таблиц и Show Table Data чтобы увидеть загруженные классом SchoolInitializer данные.

clip_image015

Закройте подключение, иначе может возникнуть проблема с запуском приложения.

clip_image016

Соглашения


Количество кода, нужное для создания Entity Framework базы, минимально из-за использования (conventions) Entity Framework. Некоторые из них уже были упомянуты:

  • Форма множественного числа имен классов сущностей используется в качестве имен таблиц.
  • Имена свойств сущностей используется в качестве имен столбцов.
  • Свойства сущностей с именами ID или classnameID распознаются как первичные ключи.
  • Entity Framework подключается к базе, отыскав строку подключения с таким же именем, как и класс контекста (в данном случае SchoolContext).
Вы видели, что данные соглашения могут быть перекрыты (допустим, плюрализацию можно отключить) и вы можете узнать больше о том, как это делать, из Creating a More Complex Data Model.

Вы создали простое приложение с использованием Entity Framework и SQL Server Compact для хранения и отображения данных. Далее мы научимся совершать простые CRUD (create, read, update, delete) операции.

Благодарим за помощь в переводе Александра Белоцерковского (ahriman).

Загрузить все приложение  |  Загрузить руководство в фомате PDF
  • +22
  • 130k
  • 5

Microsoft

298,12

Microsoft — мировой лидер в области ПО и ИТ-услуг

Поделиться публикацией
Комментарии 5
    0
    Спасибо за перевод!
      0
      Полезная плюшка, даёт возможность посмотреть формируемый EF sql:
      public static string ToTraceString<T>(this IQueryable<T> t)
              {
                  string sql = "";
                  ObjectQuery<T> oqt = t as ObjectQuery<T>;
                  if (oqt != null)
                  {
                      
                      StringBuilder sb = new StringBuilder(oqt.ToTraceString());
      
                      foreach (ObjectParameter p in oqt.Parameters)
                      {
                          sb.Replace("@"+p.Name,"'" +p.Value+"'");
                      }
      
                      sql= sb.ToString();
                  }
                  
                  return sql;
              }
        0
        В какой-то мере оно полезно, и за перевод нужно однозначно сказать спасибо XaosCPS, но вот по содержанию…

        Сколько еще микрософт будет держать всех за чайников? Ни одного примера редактирования сложного объекта здесь нет — только одна сущность за раз. Теже enrollment заполняются только при инициализации базы тчк

        Учат молодое поколение писать неэффективный код, который генерирует N запросов к базе данных:
        foreach (Enrollment enrollment in selectedCourse.Enrollments)
        {
        db.Entry(enrollment).Reference(x => x.Student).Load();
        }

        Зато да, красиво. А не дай бог студент в свою очередь вычитает какой-то еще список (в реальном проекте так и будет) и все, проект безбожно тормозит…
          0
          О, я нашел пример редактирования сложного объекта — Instructor/Edit, только почему-то там красота автоматического биндинга параметров в объект куда-то исчезает — просто разгребается FormCollection и все делается вручную :(
          0
          Перевод отличный. Сам читал эту статью еще в английском варианте, но так и не дошли руки попробовать (я с .Net давно уже не работаю, если память не изменяет, еще со второй версии).

          Но вот на днях решил попробовать MVC 4.
          Все было хорошо. Стартовый проект создался, завелся.
          Добавил в модель новое поле, оно появилось в БД — красота.
          Создал еще несколько моделей (в другом DbContext). Тоже все хорошо, все работает.

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

          Решение показалось вполне логичным: перенес все из второго DbContext в первый. Второй грохнул.
          В итоге все завелось но опять не пахало с теми же ошибками.

          Тогда я грохнул нафиг все таблицы в БД.
          Запустил, НО тут увидел что то непонятное: в БД развернулась только первая модель, и то без дополнительных полей, такая какая была создана при первом старте, еще без правок в моделях. Остальные модели в БД даже не появились.

          Тогда я грохнул уже файл БД и создал новую. Результат тот же.
          Прогулялся по всем файлам в папке с проектом, следов «кеша» EF ни где не нашел, не понимаю, с какого перепугу и откуда он берет старую модель…

          Подскажите, люди, что не так и что делать? :)

          Спасибочки.

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

          Самое читаемое