web系初学者奮闘ログ(Blazor, ASP.NET, SQL Server)
RiderにはPackage Consoleがないらしいので、代わりに以下を実行。
dotnet ef migrations add Initial
dotnet ef database update
DBの名前についてはここが参考になるかも
RiderでDB接続し、データ入ってるとこまで確認!
詰まったところ
- RiderのDB接続は"Microsoft SQL Server"を選択(localDBではない)
-
ConnectionString
は"Server="から全部コピペ
別にlocalDBでも行けた
ConnectionString
をちゃんとSSMSやVSから取ってきてコピペする必要がある
今度はBook
のOriginalTitle
を削除してもう一度DB更新してみる
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; }
}
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
ちゃんと変更が反映されている!
複数タグのDBはこうやるっぽい
今回の要件
- SoundにGenre/Tagを複数付けたい
- SoundはID, Name, FilePathを持つ
- Genre/Tagは複数付けられる
- Genreは階層になっている
- あるGenreには親Genreが存在する
- GenreやTagは検索・フィルタ用に使用
Genreは階層構造になりそうだけど、DBで階層構造を表現するのはちょっと難しいっぽい
一旦大きなジャンル分けだけして、どうしてもやりたくなったら挑戦してもいいのかもしれない(設計的にしんどい目見るかは今は考えないことにする)
一方複数タグの表現はかなり単純にできそう
注意点
- DBは複数全て1つのContextクラスに記述する
- Riderでは接続文字列が上手くいかないことがあるので、この辺の作業はVSでやるのが無難
- VSから接続文字列だけ拾ってRiderのDatabaseの追加する際の文字列に貼り付ければ、Databaseに認識される
- RiderのDatabaseタブで閲覧する際、見たいフォルダ・ファイルが隠れていることがあるので注意!
↑ここが1 of 13みたいになってたら1/13しか表示されてない。クリックすると編集できる。
DBでmp3を管理すべきかどうか問題
多分後々のことを考えると、DBでバイナリ管理し、C#の呼び出し側でbyte[]
に格納するのが良さそう
ちょっと別方向だけど参考になる記事
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からデータを拾ってみる。
まずは名前表示。
HttpClient
のinject
と、必要なアセンブリのusing
を忘れずに。
@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に変換して表示する。
@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を操作しようとするとデータめっちゃミスりそうなので、専用の追加ボタンや削除ボタンをクライアント側に作成しておいた方がいい気がする
GET, DELETEはできたけどPOSTだけ動かん…つらい
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側で作成すればいいのかも
構築を非同期で行えばいいのかも
ただ、ちょっと面倒だし後回しできる作業