Sign in to follow this  
Malboro

[TUTORIAL] Entity Framework Core (Postgresql)

Recommended Posts

Всем привет!!

Увидел тутер на английском по Entity Framework на MySql и решил сообразить свой вариант на Великом и Могучем, только для базы данных Postgresql. Надеюсь это кому-то будет полезным. Работать мне еще 3 часа, а значит должен успеть.

Поехали

Нам потребуется:

  • Visual Studio
  • Голова
  • Руки 
  • Немного времени

Установка необходимых библиотек

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

  • gtanetwork.api
  • Microsoft.EntityFrameworkCore
  • Microsoft.EntityFrameworkCore.Tools
  • Npgsql
  • Npgsql.EntityFrameworkCore.PostgreSQL
  • Newtonsoft.Json

nuget-libs.png

     Вот общая картина:

Screenshot-7.png

Создание структуры классов

 Создадим следующие классы:

  • Character  - основной класс, модель которого мы и будем сохранять в базе
  •  Finances - будет хранить в себе информацию о финансах нашего игрока. 
  • States - будет содержать информацию о состоянии персонажа
  • Item - будет представлять класс вещи из нашего инвентаря
  • Login - отвечающий за авторизацию персонажа
  • Registration - ... за его регистрацию
  • Db - будет хранить подключение к базе данных
  • AppDbContext - основной класс который и будет содержать практически все настройки для нашей базы данных

     Посмотреть более подробно содержимое можно скачав проект с  репозитория на github.

     Для тех кто в теме:

git clone https://github.com/SirEleot/EFCore_Npgsql.git

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

    class AppDbContext : DbContext
    {

        //сторка подключения к бд.
        private static string ConnectionString = "Host=localhost;Port=5432;Database=test;Uid=test;Password=test;";

        //настройка подключения к базе данных
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseNpgsql(ConnectionString);
        }
        //Добавляем класс для сохранения в дб
        //дочерние классы добавлять не нужно EFCore сам их подхватит
        public DbSet<Character> Characters { get; set; }

        // тут находится конфигурация наших сохроняемых классов
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            //настроим игнорирование свойства Data  из класса Character
            //вот так игнорируются свойства класса
            modelBuilder.Entity<Character>().Ignore(c => c.Date);

            //также добавим будем игнорировать класс Item 
            //так как он входит в состав нашего класса, то EFCore попытается для него создать таблицу
            //мы же решили что будем сохраять инвентарь в виде json строки
            //вот так игнорируются классы
            modelBuilder.Ignore<Item>();

            //здесь здесь пример того как обработать данные при сохранении
            //мы будем преобразовывать массив с вещамивв строку json 
            modelBuilder
               .Entity<Character>()//выбираем объект из контекста у нас он 1
               .Property(c => c.Inventory)//выбираем свойство 
               .HasConversion( //определяем кастомные методы для обработки данных
                   i => JsonConvert.SerializeObject(i), // при сохранении
                   i => JsonConvert.DeserializeObject<List<Item>>(i) // при загрузке
               );

            //рассмотрим как сохранить отдельный объект в нашем случае Finance
            //в одной таблице с основным классом
            modelBuilder.Entity<Character>()//выбираем объект из контекста у нас он 1
                .OwnsOne(c => c.Finance);//определяем свойство которое мы хотим добавить к текущей таблице в базе дбазе данных    }

        }
    }

   Настройка базы данных

 Думаю что с ConnectionString проблемы возникнуть не должно.  Хотя :

  • Host=localhost;  - расположение базы данных
  • Port=5432; - прослушиваемый порт
  • Database=test; - название базы данных
  • Uid=test; - имя пользователя 
  • Password=test; - и соответственно пароль

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

    Давайте разберем содержимое нашего основного класса:

    class Character
    {
        //для элементов хранящихся в отдельных таблицах обязательно наличие поля с атрибутом primary key
        //но в EFCore достаточно создать для класса свойство  Id и он сделает все за вас
        public int Id { get; set; }
        public string Social { get; set; }
        public string Name { get; set; }
        public string Lasname { get; set; }
        public string Password { get; set; }

        // поместим финансовую модель в одну таблицу с персонажем 
        //состояние персонажа будет автоматом помещено в отдельную таблицу
        //все настройки производятся в классе AppDbContext 
        public Finances Finance { get; set; }
        public States State { get; set; }

        //так же мы сохраним список вещей в основную таблицу персонажа в виде строки JSON
        //настройка так же в классе AppDbContext 
        public List<Item> Inventory { get; set; }

        //это свойство создано для примера, мы его будем игнорировать при сохранении данных в базу
        //не поверите но это тоже настроим в  классе AppDbContext 
        public DateTime Date { get; set; }
    }

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

    Перейдем непосредственно к настройкам. Для начала нужно оповестить EF Core о том что мы собираемся сохранить данный класс, для этого добавим свойство Characters типа DbSet<T>, где Т - класс нашей модели Character

public DbSet<Character> Characters { get; set; }

 

     Составим небольшой план действий. Допустим, что мы хотим чтобы информация о финансах находилась в одной таблице с нашим персонажем, а его состояние наоборот было вынесено в отдельную таблицу. Так же мы хотим что бы коллекция Inventory  хранилась в виде строки Json. Еще у нас есть свойство Date и мы не хотим сохранять его в базе данных.

     План "накидали", теперь давайте попробуем воплотить его в код. Итак по порядку:

Первым рассмотрим включаемый класс Finances:

    class Finances
    {
        //для элементов сохраняющихся в оттдельных таблицах обязательно поле с primary key
        //Достаточно создать свойство  Id и EFCore сделает все за вас
        //В конкретном случае мы не добавляем свойство Id так как Finance будет помещен в одну таблицу с Character
        //смотрите настройки в классе AppDbContext
        //public int Id { get; set; }
        public int Bank { get; set; } = 5000;
        public int Cash { get; set; } = 500;

        //добавить деньги на счет
        public bool AddBank(int amount)
        {
            Bank += amount;
            return true;
        }

        //списать деньги со счета
        public bool SubBank(int amount)
        {
            if (amount > Bank) return false;
            Bank -= amount;
            return true;
        }
        //......
    }

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

      Настройки записываются в переопределенном методе OnModelCreating класса AppDbContext:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
	modelBuilder. .......
}

     Перейдем непосредственно к настройке Finance:

modelBuilder.Entity<Character>()//выбираем объект из контекста у нас он 1
     .OwnsOne(c => c.Finance);//определяем свойство которое мы хотим добавить к текущей таблице в базе дбазе данных

тут мы при помощи метода OwnsOne(c => c.Finance) говорим EF Core, что свойство Finance следует сохранять в одной таблице с Character.

Screenshot-15-1.png

     Обратите внимание на 6 и 7 колонки это и есть свойства нашего класса Finances включенного в состав таблицы Characters. Так же обратите внимание на 1 колонку Id с пометкой [PK], это и есть наше обязательное одноименное свойство.

     Дальше у нас идет класс States:

    class States
    {
        //для элементов сохраняющихся в отдельных таблицах обязательно поле с primary key
        //Достаточно создать свойство  Id и EFCore сделает все за вас
        public int Id { get; set; }
        public int Health { get; set; } = 100;
        public int Armor { get; set; } = 100;
    }

     Ef Core, по умолчанию, пытается создать отдельную таблицу для каждого класса, включенного в состав нашего основного объекта, поэтому нам остается только создать уникальное свойство Id.

Screenshot-16.png

     Для нашего класса States была автоматически создана соответствующая таблица в базе данных, а ссылка на нее была добавлена в основную таблицу в колонку 8 с названием StateId

     Далее давайте рассмотрим пользовательские преобразование данных. У нас есть свойство Inventory которое является коллекцией классов Item, и мы решили сохраить его в виде строки json. Для этого в метод OnModelCreating класса AppDbContext добавим следующие строки кода:

 modelBuilder
	.Entity<Character>()//выбираем объект из контекста у нас он 1
	.Property(c => c.Inventory)//выбираем свойство 
	.HasConversion( //определяем кастомные методы для обработки данных
		i => JsonConvert.SerializeObject(i), // при сохранении
		i => JsonConvert.DeserializeObject<List<Item>>(i) // при загрузке
	);

  За это отвечает метод HasConversion(),  где первым параметром передается метод обработки данных при сохранении, а вторым при загрузке. В нашем случае это будет сериализация коллекции в строку при сохранении и обратно при загрузке. Для примера мы использовали методы из библиотеки Newtonsoft.Json

Screenshot-15-1.png

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

     Последним пунктом по настройке базы данных у нас будет исключение ненужных нам свойства Date, и класса Item.  Так как класс Item включен в состав нашего основного класса Character, то EF Core попытается создать для него отдельную таблицу, а она нам не нужна.  Тут все просто:

modelBuilder.Ignore<Item>();

     Вот так мы добавляем класс Item в список игнорируемых классов

 modelBuilder.Entity<Character>().Ignore(c => c.Date);

      А вот так Свойство Date класса Character

     Так выглядит общая структура созданной нами базы данных:

Screenshot-12.png

 

Миграции базы данных

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

  Для начала нам нужно отобразить окно с названием "Консоль диспетчера пакетов" для этого жмякаем : Вид -> Другие окна -> Консоль диспетчера пакетов

Screenshot-8.png

 

     В левом нижнем углу появится окно с названием, как ни странно "Консоль диспетчера пакетов". Давайте там пропишем команду для создания миграции: add-migration test_001

 где add-migration это сама команда а test_001 это название будущей миграции и нажмем Enter

Screenshot-9.png

     Если мы все сделали правильно, мы должны увидеть сообщение следующего характера повествующее нам о том, что отменить это действие можно введя  в консоли команду  remove-migration:

Screenshot-10.png

     В проекте будет создана папка с файлами миграции

Screenshot-2.png

Далее, чтобы развернуть саму базу данных введем команду update-database. Если строка подключения настроена корректна и база данных доступна вы увидите следующее сообщение об успешной миграции:

Screenshot-3.png

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

Screenshot-12.png

      Дальнейшая работа с миграциями отличается больше чем никак: При изменении структуры класса мы создаем новую миграцию при помощи команды add-migration изменяя только имя самой миграции test_002 к примеру. Имя может быть произвольным, но уникальным. Для обновления базы введите команду update-database . В папке  Migrations будет вестись история ваших миграций и вы в любой момент сможете откатить базу данных к любому этапу, используя команду update-database с именем миграции до которой нужно произвести откат изменений. Например последняя миграция у нас test_002 а нам нужно откатить до версии test_001, для этого мы должны ввести команду: update-database test_001

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

 

Работа с базой данных

Сохранение

     Для того чтобы добавить данные о персонаже в базу, нам нужно создать экземпляр нашего класса и добавить его в контекст при помощи метода Add(), после чего нужно сохранить все изменения из контекста данных непосредственно в базу при помощи метода SaveChanges()

        public void OnRegistration(Client client, string name, string lastname, string password) {

            //не забываем про хеширование пароля перед сохранением в бд
            //создаем нового персонажа 
            Character Char = new Character
            {
                Social = client.SocialClubName,
                Name = name,
                Lasname = lastname,
                Password = password,
                Finance = new Finances(),
                State = new States(),
                Inventory = new List<Item>(),
                Date = DateTime.Now
            };

            //добавляем его в контекст данных
            Db.Instance.Add(Char);

                //сохранянем изменения в базе данных
            Db.Instance.SaveChanges();
        }

     Для обновления данных нужно воспользоваться методом Update() для обновления данных о персонаже, и так же зафиксировать их при помощи метода SaveChanges()

//обновляем данные в контексте
Db.Instance.Update(Char);
//и сохраняем в бд
Db.Instance.SaveChanges();

Загрузка

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

 

        public void OnRegistration(Client client, string pwd)
        {

            //Получаем персонажа из бд 
            Character Char = Db.Instance.Characters.SingleOrDefault(c=>c.Social == client.SocialClubName);
            //если записи соответствующей кретерию нашего запроса нет вернется Null
            if (Char == null) return;
            //проверяем пароль на совпадение (не забываем про хеш)
            if (Char.Password == pwd)
            {
                //подгружаем зависимые классы вынексеные в отдельную таблицу
                Db.Instance.Entry(Char).Reference(c => c.State).Load();
                //создаем ссылку на нашу модель игрока
                client.SetData("Character", Char);
                //загружаем игрока
                //..............
            }
            else
            {
                //если не прошел проверку отправляем на повторный логин
                //................
            }
            
        }

     В этом методе, в первую очередь, мы получаем первое значение соответствующее критерию нашего запроса и возвращаем экземпляр объекта Character со свойствами взятыми из бд. Свойство State, хранящееся в отдельно таблице, будет иметь значение null,его нужно будет явно запросить из базы, но на данном этапе нам достаточно данных чтобы сверить полученный от клиента пароль с паролем из бд (не забывайте про шифрование паролей). Если ни одна одна запись не будет соответствовать  критерию нашего запроса вернется Null, это скажет нам о том что пользователя с данным логином не существует. 

    Если значение пароля совпадает то пришло время подгрузить недостающие данные. Делаем это мы при помощи метода Load() для необходимого свойства - в нашем случае это State

 Db.Instance.Entry(Char).Reference(c => c.State).Load();

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

 

На этом думаю закончу, если что-то непонятно пишите, постараюсь дополнить. А я  устал - я ухожу...

  • Like 1

Share this post


Link to post
Share on other sites

Наконецто годнота приехала. Как тут руки целовать?
Жду продолжения, все ясно и понятно, воды нет👍

Share this post


Link to post
Share on other sites
Posted (edited)

На работе у меня часто бывает время для занятия какой-то херней (а хули "совок") и я  могу иногда потратить его на что-то полезное.  Предлагайте темы(С#, HTML, CSS, JS, Vue, php...)

Edited by Malboro

Share this post


Link to post
Share on other sites
2 hours ago, Malboro said:

На работе у меня часто бывает время для занятия какой-то херней (а хули "совок") и я  могу иногда потратить его на что-то полезное.  Предлагайте темы(С#, HTML, CSS, JS, Vue, php...)

Давай JavaScript

Share this post


Link to post
Share on other sites
28 минут назад, Sleimor сказал:

Давай JavaScript

Сам непосредственно js это слишком объемная тема чтобы быть изложеной в рамках одного гайда. Прошу более конкретно формулировать свои предложения

Share this post


Link to post
Share on other sites
On 3/27/2019 at 1:07 PM, Malboro said:

Сам непосредственно js это слишком объемная тема чтобы быть изложеной в рамках одного гайда. Прошу более конкретно формулировать свои предложения

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

Share this post


Link to post
Share on other sites
18 hours ago, Thomas Morgan said:

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

Хорошой, теперь давай о банальном. Купил ноут, хочу поставить VS на Диск: D, так как места на С нету, но никак не хочет. Что делать?

18 hours ago, Thomas Morgan said:

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

Прикол в том что на компе пошло хорошо.

Share this post


Link to post
Share on other sites
Posted (edited)
49 минут назад, Thomas Morgan сказал:

Круто написано, только почему логин и регистрация реализованы как отдельные классы, а не как методы класса Character?

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

Каждый сам для себя должен определить структуру своего приложения

Edited by Malboro

Share this post


Link to post
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Sign in to follow this  

  • Recently Browsing   0 members

    No registered users viewing this page.