Увидел тутер на английском по 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
Character - основной класс, модель которого мы и будем сохранять в базе.
Finances - будет хранить в себе информацию о финансах нашего игрока.
States - будет содержать информацию о состоянии персонажа.
Item - будет представлять класс вещи из нашего инвентаря.
Login - отвечающий за авторизацию персонажа.
Registration - ... за его регистрацию.
Db - будет хранить подключение к базе данных.
AppDbContext - основной класс который и будет содержать практически все настройки для нашей базы данных.
Посмотреть более подробно содержимое можно скачав проект с репозитория на [github](https://github.com/SirEleot/EFCore_Npgsql).
Для тех кто в теме:
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, где Т - класс нашей модели Character
public DbSet<Character> Characters { get; set; }
Составим небольшой план действий. Допустим, что мы хотим чтобы информация о финансах находилась в одной таблице с нашим персонажем, а его состояние наоборот было вынесено в отдельную таблицу. Так же мы хотим что бы коллекция Inventory хранилась в виде строки Json. Еще у нас есть свойство Date и мы не хотим сохранять его в базе данных. План "накидали", теперь давайте попробуем воплотить его в код. Итак по порядку:
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. .......
}
modelBuilder.Entity<Character>()//выбираем объект из контекста у нас он 1
.OwnsOne(c => c.Finance);//определяем свойство которое мы хотим добавить к текущей таблице в базе дбазе данных
тут мы при помощи метода OwnsOne(c => c.Finance) говорим EF Core, что свойство Finance следует сохранять в одной таблице с Character.
Обратите внимание на 6 и 7 колонки это и есть свойства нашего класса Finances включенного в состав таблицы Characters. Так же обратите внимание на 1 колонку Id с пометкой [PK], это и есть наше обязательное одноименное свойство.
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.
Для нашего класса 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
В результате у нас добавилась колонка Inventory с типом text которая хранит нашу сериализованую коллекцию в виде строки, конечно пустой массив это не лучший пример, позже сделаю новый скриншот, если не забуду.Последним пунктом по настройке базы данных у нас будет исключение ненужных нам свойства Date, и класса Item. Так как класс Item включен в состав нашего основного класса Character, то EF Core попытается создать для него отдельную таблицу, а она нам не нужна. Тут все просто:
modelBuilder.Ignore<Item>();
Вот так мы добавляем класс Item в список игнорируемых классов
modelBuilder.Entity<Character>().Ignore(c => c.Date);
А вот так Свойство Date класса Character
Миграция базы данных это автоматическое созданная структура базы данных для нашего класса, в нашем случае SQL. Миграцию можно настроить как в автоматическом режиме, при запуске приложения, так и производить миграцию в ручном режиме. В рамках этого гайда мы не будем настраивать автоматический режим. В ручном режиме вы наглядно увидите что происходит. Для начала нам нужно отобразить окно с названием "Консоль диспетчера пакетов" для этого жмякаем : Вид -> Другие окна -> Консоль диспетчера пакетов.
В левом нижнем углу появится окно с названием, как ни странно "Консоль диспетчера пакетов". Давайте там пропишем команду для создания миграции: add-migration test_001 где add-migration это сама команда а test_001 это название будущей миграции и нажмем Enter
Если мы все сделали правильно, мы должны увидеть сообщение следующего характера повествующее нам о том, что отменить это действие можно введя в консоли команду remove-migration:
В проекте будет создана папка с файлами миграции
Далее, чтобы развернуть саму базу данных введем команду update-database. Если строка подключения настроена корректна и база данных доступна вы увидите следующее сообщение об успешной миграции:
В базе данных у нас должны появится таблицы соответствующие нашим настройкам.
Дальнейшая работа с миграциями отличается больше чем никак: При изменении структуры класса мы создаем новую миграцию при помощи команды 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();
На этом этапе наш класс загружен в полном объеме и готов к употреблению. Нам осталось сохранить ссылку на него что бы в дальнейшем можно было манипулировать данными. На этом думаю закончу, если что-то непонятно пишите, постараюсь дополнить. А я устал - я ухожу...








