前回(第5回) でリアルタイム表示ができました。でもアプリを止めると値は消えてしまいます。今回は SQLite+EF Core で温度を蓄積し、過去の推移を振り返れるようにします。
Table of contents
Open Table of contents
パッケージを追加
dotnet add package Microsoft.EntityFrameworkCore.Sqlite
モデルとDbContext
// Data/TemperatureReading.cs
public class TemperatureReading
{
public int Id { get; set; }
public DateTime TimestampUtc { get; set; }
public double Celsius { get; set; }
}
// Data/AppDbContext.cs
using Microsoft.EntityFrameworkCore;
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
public DbSet<TemperatureReading> Readings => Set<TemperatureReading>();
}
登録:Blazorでは IDbContextFactory を使う
BlazorのコンポーネントやバックグラウンドサービスはDbContextの寿命管理が難しく(DbContext はスレッドセーフでない・使い回すと壊れる)、Microsoftは AddDbContextFactory の利用を推奨しています。使うたびに短命なDbContextを生成する形にします。
// Program.cs
builder.Services.AddDbContextFactory<AppDbContext>(options =>
options.UseSqlite("Data Source=temps.db"));
初回にDBファイルを作成します(起動時)。
// Program.cs(app.Run(); の前あたり)
using (var scope = app.Services.CreateScope())
{
var factory = scope.ServiceProvider.GetRequiredService<IDbContextFactory<AppDbContext>>();
using var db = factory.CreateDbContext();
db.Database.EnsureCreated();
}
EnsureCreated()は手軽ですが、後からスキーマを変更できません。実運用では マイグレーション(dotnet ef migrations)が正解です。EF Coreの設計・N+1・性能改善は拙著『動かして学ぶ Entity Framework Core 実践入門』で深掘りしています。
バックグラウンドで保存する
第5回の TemperatureMonitor を拡張し、一定間隔でDBへ保存します(毎秒だと過剰なので10秒ごと)。
public sealed class TemperatureMonitor : BackgroundService
{
private readonly TemperatureService _temp;
private readonly IDbContextFactory<AppDbContext> _dbFactory;
private int _tick;
// (Current / history / Changed は第5回のまま)
public TemperatureMonitor(TemperatureService temp,
IDbContextFactory<AppDbContext> dbFactory)
{
_temp = temp;
_dbFactory = dbFactory;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(1));
while (await timer.WaitForNextTickAsync(stoppingToken))
{
Current = _temp.ReadCelsius();
// …history更新・Changed発火(第5回のまま)…
if (++_tick % 10 == 0) // 10秒ごとに保存
{
await using var db = await _dbFactory.CreateDbContextAsync(stoppingToken);
db.Readings.Add(new TemperatureReading
{
TimestampUtc = DateTime.UtcNow,
Celsius = Current
});
await db.SaveChangesAsync(stoppingToken);
}
}
}
}
履歴を時系列グラフで表示
保存した直近のデータを読み、SVGの折れ線で描きます(第5回のグラフの応用)。
@* Components/Pages/History.razor *@
@page "/history"
@inject IDbContextFactory<AppDbContext> DbFactory
@using Microsoft.EntityFrameworkCore
<h1>温度の履歴</h1>
<svg width="600" height="200" style="border:1px solid #ccc">
@if (!string.IsNullOrEmpty(_points))
{
<polyline points="@_points" fill="none" stroke="#006cac" stroke-width="2" />
}
</svg>
@code {
private string _points = "";
protected override async Task OnInitializedAsync()
{
await using var db = await DbFactory.CreateDbContextAsync();
var data = await db.Readings
.OrderByDescending(r => r.TimestampUtc)
.Take(100)
.OrderBy(r => r.TimestampUtc) // 古い順に並べ直す
.Select(r => r.Celsius)
.ToArrayAsync();
_points = BuildPoints(data);
}
private static string BuildPoints(double[] data)
{
if (data.Length < 2) return "";
const int w = 600, h = 200;
double min = data.Min(), max = data.Max();
double range = max - min < 0.1 ? 1 : max - min;
return string.Join(" ", data.Select((v, i) =>
$"{(double)i / (data.Length - 1) * w:F1},{h - (v - min) / range * h:F1}"));
}
}
実機で起動してしばらく放置すると、/history に時間とともに蓄積された温度の推移が表示されます。アプリを再起動しても、temps.db にデータが残っているのが確認できます。

つまずきポイント
- DBが作られない:起動時の
EnsureCreated()を通っているか。temps.dbの生成場所(実行ディレクトリ)に注意 - DbContextの例外(“A second operation…” 等):使い回さず、都度
CreateDbContext(factory方式)で回避 - 時刻がずれる:保存は UTC(
DateTime.UtcNow)で統一し、表示時にローカル変換すると混乱しない - ファイルが肥大化:古いデータの定期削除(保持期間)を設けると安心
まとめ
- 温度は SQLite+EF Core で永続化。アプリを止めても残る
- BlazorのEF Coreは
IDbContextFactory(都度生成)が推奨 - バックグラウンドで一定間隔に保存し、履歴はクエリしてグラフ表示
- 実運用のスキーマ変更は マイグレーションで
次回(第7回)は、MQTT連携。他のデバイスの値も集約し、しきい値を超えたらLEDで警告する、IoTダッシュボードらしい回にします。
