Бекенд на C# Web Api

To Kaiten

Запросы к базе данных

Внедрение зависимости

База данных внедряется в конструктор с помощью механизма внедрения зависимостей. Обычно базой данных пользуются в сервисах.

public class AuthService
{
    // база данных внедряется в конструктор
    public AuthService(MyDbContext database)
    { ... }
}

Например, сервис аутентификации может обращаться к БД чтобы проверять логин и пароль.


Запросы к базе данных

Низкоуровневый подход

Запросы к базе данных обычно пишутся с использованием языка SQL. Можно использовать ADO.NET - низкоуровневый подход для работы с базой данных. Его преимуществом является полный контроль над SQL запросами.

Высокоуровневый подход

Однако мы будем использовать более высокоуровневый подход и писать запросы с помощью Entity Framework. Таким образом всеми запросами можно управлять с помощью обычных C# методов, лямбда выражений или с помощью языка LINQ.

Запросы на добавление и удаление

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

public async Task ExampleMethod()
{
    await _dbContext.PlayerProfiles.AddAsync(playerProfile1);
    await _dbContext.PlayerProfiles.AddAsync(playerProfile1);
    _dbContext.PlayerProfiles.Remove(oldPlayerProfile);
    _dbContext.PlayerProfiles.Update(updatePlayerProfile);
    await _dbContext.SaveChangesAsync();
}

После всех операций необходимо обязательно вызвать метод SaveChanges() или SaveChangesAsync() чтобы все изменения применились.

Выборка данных

Для выборки данных можно воспользоваться методом Select или запросом с помощью LINQ.

public async Task ExampleMethod()
{
    // выборка всех профилей, в которых указан мужской пол
    _dbContext.PlayerProfiles.Select(profile => profile.gender == Gender.Male);
    
    // выборка всех карт с LEFT JOIN по таблице загрузок
    // для подсчета числа скачиваний
    var result = await (from map in _dbContext.CustomMaps
                    join download in _dbContext.Downloads
                    on map.map_id equals download.map_id into downloadsGroup
                    from download in downloadsGroup.DefaultIfEmpty()
                    group download by map into grouped
                    select new MapInfoDto
                    {
                        map_id = grouped.Key.map_id,
                        name = grouped.Key.name,
                        description = grouped.Key.description,
                        player_id = grouped.Key.player_id,
                        downloads = grouped.Count(d => d != null),
                    }).ToArrayAsync();
}

Загрузка связей при обращении к внешним ключам

Явная загрузка (Eager Loading)

Явная загрузка — наиболее эффективный и предпочитаемый способ загружать данные, связанные через внешние ключи, поскольку он позволяет сразу подгружать связанные данные с использованием одного SQL-запроса.

Пример: предположим я загружаю данные subscriptions и хочу вместе с ними сразу же получить связанные данные player_profiles и player_credentials.

Для этого необходимо в запросе использовать метод IncludeThenInclude, если большая вложенность). Пример запроса приведен ниже.

public async Task<Subscriptions> GetWithProfile(string playerId)
{
    return await RepositoryContext.Subscriptions
        .Include(p => p.PlayerProfile)
        .ThenInclude(c => c.PlayerCredential)
        .FirstOrDefaultAsync(data => data.player_id == playerId);
}

В результате такой метод сгенерирует один SQL запрос с JOIN, что позволяет эффективно получать данные.

SELECT s.player_id, s.expires_at, p.player_id, p.created_at, p.gender, p.name, p0.player_id, p0.email, p0.password
FROM subscriptions AS s
INNER JOIN player_profiles AS p ON s.player_id = p.player_id
INNER JOIN player_credentials AS p0 ON p.player_id = p0.player_id
WHERE s.player_id = @__playerId_0
LIMIT 1

Отложенная загрузка (Lazy Loading)

Отложенная загрузка позволяет автоматически загружать связанные данные при первом обращении к свойству. Чтобы использовать отложенную загрузку в Entity Framework Core, необходимо выполнить следующие шаги:

  • Убедитесь, что у вас установлен пакет Microsoft.EntityFrameworkCore.Proxies.

  • В методе конфигурации бд включите использование прокси-объектов:

    services.AddDbContext<MyDbContext>(options =>
        options.UseLazyLoadingProxies()
               .UseSqlServer(Configuration.GetConnectionString("...")));
  • Все свойства, которые нужны для внешних ключей, должны быть помечены как virtual, чтобы Entity Framework мог создать прокси-объекты и выполнять отложенную загрузку:

    [Table("downloads")]
    [PrimaryKey(nameof(player_id), nameof(map_id))]
    public class Downloads
    {
        [Required]
        [ForeignKey(nameof(PlayerProfile))]
        public string player_id { get; set; }
        
        [Required]
        [ForeignKey(nameof(CustomMap))]
        public string map_id { get; set; }
    
        public virtual PlayerProfiles PlayerProfile { get; set; } // virtual
    
        public virtual CustomMaps CustomMap { get; set; } // virtual
    }

Когда вы получите объект Downloads, доступ к downloads.PlayerProfile или downloads.CustomMap автоматически загрузит объект PlayerProfiles или CustomMaps соответственно, если его еще не загрузили.

Важно: Отложенная загрузка может привести к "N+1" проблеме, когда для каждой сущности делается отдельный запрос к базе данных. Поэтому данный способ загрузки не рекомендуется использовать (либо использовать с особой осторожностью).


Ссылки

Microsoft — EF Core — Beware of lazy loading

C# Corner — CRUD Operations In ASP.NET Core Web API Using ADO.NET


Автор документа: Артём Ветик