Open31

web系初学者奮闘ログ(Blazor, ASP.NET, SQL Server)

けめるけめる

詰まったところ

  • RiderのDB接続は"Microsoft SQL Server"を選択(localDBではない)
  • ConnectionString は"Server="から全部コピペ
けめるけめる

別にlocalDBでも行けた
ConnectionStringをちゃんとSSMSやVSから取ってきてコピペする必要がある

けめるけめる

今度はBookOriginalTitleを削除してもう一度DB更新してみる

Book
using System.ComponentModel.DataAnnotations;

namespace SoundWhat.Shared;

public class Book
{
    public int ID { get; set; }
    [Required]
    [StringLength(100)]
    public string Title { get; set; }
    [StringLength(100)]
    public string Author { get; set; }
    [Range(1900, 2100)]
    public int PublishYear { get; set; }
}
Program
using Microsoft.EntityFrameworkCore;
using SoundWhat.Server.Data;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllersWithViews();
builder.Services.AddRazorPages();
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("AppDbContext")));

await DbInitializer.SeedingAsync(builder.Services.BuildServiceProvider().GetRequiredService<AppDbContext>());


var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseWebAssemblyDebugging();
}
else
{
    app.UseExceptionHandler("/Error");
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

app.UseHttpsRedirection();

app.UseBlazorFrameworkFiles();
app.UseStaticFiles();

app.UseRouting();


app.MapRazorPages();
app.MapControllers();
app.MapFallbackToFile("index.html");

app.Run();

この状態でefコマンドを再度実行

dotnet ef migrations add Secondary
dotnet ef database update
けめるけめる

今回の要件

  • SoundにGenre/Tagを複数付けたい
  • SoundはID, Name, FilePathを持つ
  • Genre/Tagは複数付けられる
  • Genreは階層になっている
    • あるGenreには親Genreが存在する
  • GenreやTagは検索・フィルタ用に使用
けめるけめる

Genreは階層構造になりそうだけど、DBで階層構造を表現するのはちょっと難しいっぽい

一旦大きなジャンル分けだけして、どうしてもやりたくなったら挑戦してもいいのかもしれない(設計的にしんどい目見るかは今は考えないことにする)

https://qiita.com/uchinami_shoichi/items/5fa52f340003107d46c1

けめるけめる

注意点

  • DBは複数全て1つのContextクラスに記述する
  • Riderでは接続文字列が上手くいかないことがあるので、この辺の作業はVSでやるのが無難
    • VSから接続文字列だけ拾ってRiderのDatabaseの追加する際の文字列に貼り付ければ、Databaseに認識される
  • RiderのDatabaseタブで閲覧する際、見たいフォルダ・ファイルが隠れていることがあるので注意!

    ↑ここが1 of 13みたいになってたら1/13しか表示されてない。クリックすると編集できる。
けめるけめる

Controllerを作成し、そこに各種apiを定義していくらしい。
ControllerはScaffoldingによって作成できる。

using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using SoundWhat.Server.Data;
using SoundWhat.Shared;

namespace SoundWhat.Server.Controllers;

[Route("api/[controller]")]
public class SoundController : Controller
{
    private readonly SoundWhatContext _context;

    public SoundController(SoundWhatContext context)
    {
        _context = context;
    }

    [HttpGet]
    [Route("sound")]
    public async Task<ActionResult<IEnumerable<Sound>>> GetSounds()
    {
        return await _context.Sounds.ToListAsync();
    }

    [HttpGet]
    [Route("sound/{id}")]
    public async Task<ActionResult<Sound>> GetSound(int id)
    {
        var sound = await _context.Sounds.FindAsync(id);

        if (sound == null)
        {
            return NotFound();
        }

        return sound;
    }

    [HttpPost]
    [Route("sound")]
    public async Task<ActionResult<Sound>> PostSound(Sound sound)
    {
        _context.Sounds.Add(sound);
        await _context.SaveChangesAsync();

        return CreatedAtAction(nameof(GetSound), new { id = sound.Id }, sound);
    }

    [HttpPut]
    [Route("sound/{id}")]
    public async Task<IActionResult> PutSound(int id, Sound sound)
    {
        if (id != sound.Id)
        {
            return BadRequest();
        }

        _context.Entry(sound).State = EntityState.Modified;
        await _context.SaveChangesAsync();

        return NoContent();
    }

    [HttpDelete]
    [Route("sound/{id}")]
    public async Task<IActionResult> DeleteSound(int id)
    {
        var sound = await _context.Sounds.FindAsync(id);

        if (sound == null)
        {
            return NotFound();
        }

        _context.Sounds.Remove(sound);
        await _context.SaveChangesAsync();

        return NoContent();
    }
}
けめるけめる

Index.razorにて実際にDBからデータを拾ってみる。
まずは名前表示。

HttpClientinjectと、必要なアセンブリのusingを忘れずに。

Index.razor
@page "/"
@inject HttpClient httpClient;
@using SoundWhat.Shared;

<PageTitle>Index</PageTitle>

<h1>Hello, world!</h1>

@if (_sounds is { Count: > 0 })
{
    <ul>
        @foreach (var sound in _sounds)
        {
            <li>音の名前: @sound.Name</li>
        }
    </ul>
}

@code
{
    private List<Sound>? _sounds;

    protected override async Task OnInitializedAsync()
    {
        _sounds = await httpClient.GetFromJsonAsync<List<Sound>>("api/Sound/sound");
    }
}
けめるけめる

DBのバイナリデータをmp3に変換して表示する。

Index.razor
@page "/"
@inject HttpClient httpClient;
@using SoundWhat.Shared;

<PageTitle>Index</PageTitle>

<h1>Hello, world!</h1>

@if (_sounds is { Count: > 0 })
{
    <ul>
        @foreach (var sound in _sounds)
        {
            <li>音の名前: @sound.Name</li>
            <audio controls autobuffer="autobuffer" autoplay="autoplay">
                <source src="@($"data:audio/mp3;base64,{ConvertToAudio(@sound.File)}")" type="audio/mpeg"/>
            </audio>
        }
    </ul>
}

@code
{
    private List<Sound>? _sounds;

    private static string ConvertToAudio(byte[] bytes)
    {
        return Convert.ToBase64String(bytes);
    }

    protected override async Task OnInitializedAsync()
    {
        _sounds = await httpClient.GetFromJsonAsync<List<Sound>>("api/Sound/sound");
    }
}
けめるけめる

多分C#上で全部DBを操作しようとするとデータめっちゃミスりそうなので、専用の追加ボタンや削除ボタンをクライアント側に作成しておいた方がいい気がする

けめるけめる

DBに自動採番させてから取得しないとIDが適切に取れないことに注意しながらタグとの紐づけを行う

using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using SoundWhat.Server.Data;
using SoundWhat.Shared;

namespace SoundWhat.Server.Controllers;

[Route("api/[controller]/sound")]
[ApiController]
public class SoundController : Controller
{
    private readonly SoundWhatContext _context;

    public SoundController(SoundWhatContext context)
    {
        _context = context;
    }

    [HttpGet]
    [Route("")]
    public async Task<ActionResult<IEnumerable<Sound>>> GetSounds()
    {
        return await _context.Sounds.ToListAsync();
    }

    [HttpGet]
    [Route("{soundId}")]
    public async Task<ActionResult<Sound>> GetSound(int id)
    {
        var sound = await _context.Sounds.FindAsync(id);

        if (sound == null)
        {
            return NotFound();
        }

        return sound;
    }

    [HttpPost]
    [Route("")]
    public async Task<ActionResult<Sound>> PostSound(Sound soundPost)
    {
        _context.Sounds.Add(soundPost);

        var tagNames = soundPost.Tags.Select(t => t.Name).ToList();
        await _context.SaveChangesAsync();

        foreach (var tagName in tagNames)
        {
            var tag = _context.Tags.FirstOrDefault(t => t.Name == tagName);
            if (tag == null)
            {
                tag = new Tag { Name = tagName };
                _context.Tags.Add(tag);
            }

            await _context.SaveChangesAsync();

            var newTag = _context.Tags.FirstOrDefault(t => t.Name == tagName);
            if (newTag == null) return NotFound();
            
            var newSound = _context.Sounds.FirstOrDefault(s => s.Name == soundPost.Name);

            var soundToTag = new SoundToTag { SoundId = newSound.Id, TagId = newTag.Id };
            _context.SoundToTags.Add(soundToTag);
        }


        await _context.SaveChangesAsync();

        return CreatedAtAction(nameof(GetSound), new { id = soundPost.Id }, soundPost);
    }


    [HttpDelete]
    [Route("{soundId}")]
    public async Task<IActionResult> DeleteSound(int soundId)
    {
        var sound = await _context.Sounds.FindAsync(soundId);

        if (sound == null)
        {
            return NotFound();
        }

        _context.Sounds.Remove(sound);

        _context.SoundToTags.RemoveRange(_context.SoundToTags.Where(stt => stt.SoundId == soundId));

        await _context.SaveChangesAsync();

        return NoContent();
    }
}
けめるけめる

曲のジャンルのグラフ構造、普通に無限ループだけ気を付けてグラフをSQL側で作成すればいいのかも
構築を非同期で行えばいいのかも
ただ、ちょっと面倒だし後回しできる作業