Malboro

[TUTORIAL] C# Microsoft Entiy Farmework (Postgresql)

Recommended Posts

Posted (edited)

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

Увидел тутер на английском по 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();

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

 

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

Edited by Malboro
Дополнение 3
  • Like 2
  • Take2 1

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

  • Recently Browsing   0 members

    No registered users viewing this page.