ASP.NET Core APIでリストを一括更新したいときはAutoMapperが便利だった

APIで扱うデータの中にリストが含まれる場合に更新(PUT)でリストを一括で対応してほしいとき、EntityFrameworkの追跡機能とAutoMapperを使えば楽に実装できた、という話です。

例えば GET した時が以下のようなデータとします。

GET /api/TodoItems/1
{
    "id": 1,
    "name": "Task1",
    "isComplete": false,
    "todoSubItems": [
        {
            "id": 1,
            "name": "Sub task1",
            "isComplete": false,
            "todoItemId": 1
        },
        {
            "id": 2,
            "name": "Sub task2",
            "isComplete": false,
            "todoItemId": 1
        },
    ]
}

このような場合PUT の時は TodoSubItems に追加、変更、削除があってもそのまま上書きしてもらいたいということです。

PUT /api/TodoItems/1
{
    "id": 1,
    "name": "Task1",
    "isComplete": false,
    "todoSubItems": [
        {
            "id": 2,
            "name": "Sub task2 (updated)"
            "isComplete": false,
            "todoItemId": 1
        },
        {
            "id": 0,
            "name": "New task"
            "isComplete": false,
            "todoItemId": 1
        }
    ]
}

この例では TodoSubItem の1を削除、2を編集、一つを新たに追加しています。

補足: サブタスクでAPIのエンドポイント作ればいいんじゃない?

確かに TodoItems と TodoSubItems でエンドポイントを分ける設計もあります。ですが今回サブタスクはメインタスクに従属するもので、サブタスクの変更もメインタスクの更新時に行いたい、というユースケースを想定しています。例えば、伝票と伝票明細の関係、記事と記事タグなど、1対多の関係で1の属性の一つとして多(リスト)を持っている場合には実用的なパターンかと思っています。

準備

今回はデータベースにSQL Serverを利用します。今はDockerで簡単にSQL Serverを起動できます。

docker run -e "ACCEPT_EULA=Y" -e "SA_PASSWORD=<your_strong_password>" -p 1433:1433 --name todosubapi -d mcr.microsoft.com/mssql/server:2019-latest

たったこれだけ。いい時代になりました。

実装

ここから実装していきます。サンプルとして公式サイトのチュートリアルにあるToDoアプリを使います。このアプリにToDoタスクが複数のサブタスクを持てる機能を追加します。基本的にチュートリアル通りに進めて、サブタスク機能を実現するのに必要な最低限の実装を付け足す方針です。

Webプロジェクトの作成

公式チュートリアル通りです。詳細な作成方法は割愛します。プロジェクト名は「 TodoSubApi 」としました。

テスト

初期状態で正常に起動するか確認します。

  • Ctrl + F5 を押して実行する
  • Webブラウザが起動しSwaggerの画面が表示されることを確認する

モデルクラスの追加

基本公式チュートリアル通りですが、サブタスクを表す TodoSubItem を追加します。

  • ソリューションエクスプローラーを右クリックして [追加] -> [新しいフォルダー] を選択する
  • 「 Models 」という名前をつける
  • Models フォルダーを右クリックして [追加] -> [クラス] を選択する
  • クラスに「 TodoItem 」という名前をつける
  • 同様に Models 配下に「 TodoSubItem 」というクラスをつくる
  • TodoItem.cs と TodoSubItem.cs を以下のように作成する
namespace TodoSubApi.Models
{
    public class TodoItem
    {
        public long Id { get; set; }
        public string Name { get; set; }
        public bool IsComplete { get; set; }
        public List<TodoSubItem> TodoSubItems { get; set; }
    }
}
namespace TodoSubApi.Models
{
    public class TodoSubItem
    {
        public long Id { get; set; }
        public string Name { get; set; }
        public bool IsComplete { get; set; }
        public long TodoItemId { get; set; }
    }
}
ちょっと説明

TodoItem クラスの TodoSubItems プロパティはコレクションナビゲーションプロパティと呼ばれるものです。TodoSubItem クラスの TodoItemId は外部キーです。(参照

データベースコンテキストの追加

公式チュートリアルではインメモリデータベースを採用していますが、今回はSQL Serverで試すので、NuGetパッケージ「 Microsoft.EntityFrameworkCore.SqlServer 」を追加します。その後以下のようにデータベースコンテキストを追加します。

  • Models フォルダーを右クリックして [追加] -> [クラス] を選択する
  • クラスに「 TodoContext 」という名前をつける
  • TodoContext.cs を以下のように作成する
using Microsoft.EntityFrameworkCore;

namespace TodoSubApi.Models
{
    public class TodoContext : DbContext
    {
        public TodoContext(DbContextOptions<TodoContext> options)
            :base(options)
        {
        }

        public DbSet<TodoItem> TodoItems { get; set; }
        public DbSet<TodoSubItem> TodoSubItems { get; set; }
    }
}

データベースコンテキストの登録

公式チュートリアルとは異なり、SQL Serverを使う形に Startup.cs を書き換えます。

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<TodoContext>(opt =>
        opt.UseSqlServer("Server=tcp:127.0.0.1,1433;Initial Catalog=todoapi;User ID=sa;Password=<your_strong_password>;Persist Security Info=False;"));

    ...
}

マイグレーションとテーブル作成

この作業は公式チュートリアルにはありません。今回はSQL Serverを使うので事前にSQL Serverにテーブルを作成しておきます。

  • Visual Studioの [パッケージマネージャーコンソール] を開く
  • 以下のコマンドを実行し、マイグレーションを作成する
PM> Add-Migration InitialCreate
Build started...
Build succeeded.
To undo this action, use Remove-Migration.
  • プロジェクト配下に Migrations フォルダーがあることを確認する
  • 以下のコマンドを実行し、データベースを更新する
PM> Update-Database
Build started...
Build succeeded.
Done.

コントローラーのスキャフォールディング

公式チュートリアル通りです。詳細な方法は割愛しますがスクリーンショットだけ載せておきます。

PostTodoItem 作成メソッドの更新

公式チュートリアル通りです。

GetTodoItems メソッドとGetTodoItem メソッドの更新

これは公式チュートリアルにはありません。GET してきたときに TodoSubItems も取得できるように処理を追加します。

  • TodoItemsController.cs を以下のように編集する
[HttpGet]
public async Task<ActionResult<IEnumerable<TodoItem>>> GetTodoItems()
{
    return await _context.TodoItems.Include(x => x.TodoSubItems).ToListAsync();
}

...
[HttpGet("{id}")]
public async Task<ActionResult<TodoItem>> GetTodoItem(long id)
{
    var todoItem = await _context.TodoItems.Include(x => x.TodoSubItems).FirstOrDefaultAsync(x => x.Id == id);

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

    return todoItem;
}
ちょっと説明

データコンテキストからデータを取得する処理に Include メソッドを追加して TodoSubItems を読み込んでいます。

PutTodoItem メソッドの更新

この記事のメインです。データ更新時に TodoSubItems の中身を自由に変えても適切に更新してもらうためAutoMapperを使う方法に変えます。

  • NuGetパッケージ「 AutoMapper 」を追加する
  • まず TodoItemsController.cs のコンストラクタにAutomapperの定義と設定を書く
private readonly IMapper _mapper;

...

public TodoItemsController(TodoContext context)
{
    _context = context;

    var config = new MapperConfiguration(cfg =>
    {
        cfg.CreateMap<TodoItem, TodoItem>();
        cfg.CreateMap<TodoSubItem, TodoSubItem>();
    });
    _mapper = config.CreateMapper();
}
  • PutTodoItem メソッドを書き換える
[HttpPut("{id}")]
public async Task<IActionResult> PutTodoItem(long id, TodoItem todoItem)
{
    if (id != todoItem.Id)
    {
        return BadRequest();
    }

    // コメントアウトする
    //_context.Entry(todoItem).State = EntityState.Modified;

    try
    {
        var found = _context.TodoItems.Include(t => t.TodoSubItems).FirstOrDefault(x => x.Id == id);

        _mapper.Map(todoItem, found);

        _context.TodoItems.Update(found);

        await _context.SaveChangesAsync();
    }
    catch (DbUpdateConcurrencyException)
    {
        if (!TodoItemExists(id))
        {
            return NotFound();
        }
        else
        {
            throw;
        }
    }

    return NoContent();
}
ちょっと説明

UPDATE処理のメインは try 句の中です。一度データベースから該当するIDのデータを持ってきて、更新したいデータをAutoMapperで上書きします。Entity Frameworkの追跡機能が変更箇所を判別してくれるのでいい感じにデータ更新してくれます。正直この辺の仕組みは完全には理解できていないですが、とりあえず想定していた動きをしてくれます。

Postman を使ってテスト

最後にAPIが想定通り動作するか Postmanを使って確かめます。

  • Ctrl + F5 で起動する
  • 作成 (POST)。Postmanで以下のクエリを送信する
    • ※:ポート番号は環境によって異なります。
POST: https://localhost:44366/api/TodoItems
{
    "id": 0,
    "name": "Task1",
    "isComplete": false,
    "todoSubItems": [
        {
            "id": 0,
            "name": "Sub task1",
            "isComplete": false,
            "todoItemId": 0
        },
        {
            "id": 0,
            "name": "Sub task2",
            "isComplete": false,
            "todoItemId": 0
        }
    ]
}
  • レスポンス例
{
    "id": 1,
    "name": "Task1",
    "isComplete": false,
    "todoSubItems": [
        {
            "id": 1,
            "name": "Sub task1",
            "isComplete": false,
            "todoItemId": 1
        },
        {
            "id": 2,
            "name": "Sub task2",
            "isComplete": false,
            "todoItemId": 1
        }
    ]
}
  • 取得 (GET)。Postmanで以下のクエリを送信する
GET: https://localhost:44366/api/TodoItems/1
  • レスポンス例
{
    "id": 1,
    "name": "Task1",
    "isComplete": false,
    "todoSubItems": [
        {
            "id": 1,
            "name": "Sub task1",
            "isComplete": false,
            "todoItemId": 1
        },
        {
            "id": 2,
            "name": "Sub task2",
            "isComplete": false,
            "todoItemId": 1
        }
    ]
}
  • 更新 (PUT)。Postmanで以下のクエリを送信する
    • ※:サブタスク1を削除、サブタスク2を変更、新しいサブタスクの一つ追加した、という想定です。
PUT: https://localhost:44366/api/TodoItems/1
{
    "id": 1,
    "name": "Task1",
    "isComplete": false,
    "todoSubItems": [
        {
            "id": 2,
            "name": "Sub task2 (updated)",
            "isComplete": false,
            "todoItemId": 1
        },
        {
            "id": 0,
            "name": "New task",
            "isComplete": false,
            "todoItemId": 1
        }
    ]
}
  • 再取得 (GET)。Postmanで以下のクエリを送信する
GET: https://localhost:44366/api/TodoItems/1
  • レスポンス例
{
    "id": 1,
    "name": "Task1",
    "isComplete": false,
    "todoSubItems": [
        {
            "id": 2,
            "name": "Sub task2 (updated)",
            "isComplete": false,
            "todoItemId": 1
        },
        {
            "id": 3,
            "name": "New task",
            "isComplete": false,
            "todoItemId": 1
        }
    ]
}

サブタスクの削除、変更、追加が全て反映されているのが分かります。

まとめと感想

APIのデータの中にリスト(配列)を設けた場合の実装方法をまとめました。リスト自体をAPIのエンドポイントにして一つずつCRUDさせる設計もありますが、リストが完全に親の要素に従属している場合は今回のような設計のほうが簡単に利用できるので、ここで書いた内容が役立つ場面は意外とあると思います。この記事では書きませんでしたが、実際のAPIのモデルクラスとDBに記録するデータモデルクラスに同一のものを使う必要はありません。今回のサンプルだとサブタスクの外部キー TodoItemId はAPIに含める必要がない項目ですし、サブタスクのIDも本質的には不要です。そのため実際にはモデルクラスとデータモデルクラスを分けて定義します。その場合はクラス間のデータ変換方法が課題になりますが、やはりAutoMapperが便利なのでこの記事の方法と同じようなAutoMapperを使った実装で課題に対応できます。

ASP.NET Core SignalR でルーム付きチャットアプリを作ってみた

最近SignalRが気になったので公式チュートリアルやってみました。
分かりやすくていいんだけどリアルタイムアプリにありがちなチャットルーム毎に通信を分ける機能がなかったので自分で実装してみました。
余計な機能はつけずに、最低限ルーム内通信が成り立つ程度のシンプルな実装にしています。
尚、ルーム内通信を実現するのにSignalRのグループ機能を利用しています。
実装済みのコードはGitHubに上げてます。

バージョン

  •  Visual Studio 2019
  • .NET 5.0

プロジェクト作成

基本公式チュートリアル 通りです。

  • Visual Studioで新しいプロジェクトを作成する
  • 「ASP.NET Core Webアプリ」を選択する
    • ※同じVS2019でもインストール環境によっては名称が少し違うことがあります。
  • プロジェクト名は「SignalRGroupChat」とした

とりあえず実行

プロジェクトが作成出来たら、まずは初期状態で動くことを確認するため「F5」キーを押して実行します。

SignalRクライアントライブラリを追加する

公式チュートリアル 通りです。

  • ソリューションエクスプローラーで、プロジェクトを右クリックし、[追加] -> [クライアント側のライブラリ]を選択する
  • [クライアント側のライブラリを追加します]ダイアログで以下の通り選択する
    • プロバイダー: unpkg
    • ライブラリ:@microsoft/signalr@latest
    • [特定のファイルの選択]を選択する
    • dist/browser フォルダを展開し、 signalr.jssignalr.min.js を選択する
    • [ターゲットロケーション]に wwwroot/js/signalr/ と入力する
    • [インストール]をクリック

SignalRハブを作成する

基本は公式チュートリアル 通りだけど、ハブに実装するコードについてはグループ機能に関する処理に書き換えます。

  • プロジェクト直下に Hubs フォルダを作成する
  • Hubs フォルダ内に ChatHub.cs を作成し、以下のように編集する
using Microsoft.AspNetCore.SignalR;
using System.Threading.Tasks;

namespace SignalRGroupChat.Hubs
{
    public class ChatHub : Hub
    {
        public async Task SendMessageToGroup(string group, string user, string message)
        {
            await Clients.Group(group).SendAsync("ReceiveMessage", user, message);
        }

        public async Task AddToGroup(string groupName)
        {
            await Groups.AddToGroupAsync(Context.ConnectionId, groupName);
        }
    }
}
ちょっと説明
  • SendMessageToGroup 関数について
    • チュートリアルでは全クライアントに送信するが、今回は指定したグループに送信する(参考
  • AddToGroup 関数について
    • クライアントをグループに追加する処理(参考

SignalRを構成する

公式チュートリアル 通りです。

  • Startup.cs を以下のように変更する
using SignalRGroupChat.Hubs;
...
public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages();
    services.AddSignalR();
}
...
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    ...

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
        endpoints.MapHub<ChatHub>("/chatHub");
    });
}

SignalRクライアントコードを追加する

ここは公式チュートリアルとは少し異なります。
まず、トップページはグループ名を入力してもらう画面にします。
そして、チャットルームページを新たに作成してチャット画面を実装します。

  • Pages\Index.cshtml を以下のように変更する
@page
@model IndexModel
@{
    ViewData["Title"] = "Home page";
}

<div class="container">
    <div class="row">
        <div class="col-2">Room name</div>
        <div class="col-4"><input type="text" id="roomName" /></div>
    </div>
    <div class="row"> </div>
    <div class="row">
        <div class="col-6">
            <input type="button" id="moveToRoom" value="Move to room" />
        </div>
    </div>
</div>
<script src="~/js/index.js"></script>
  • wwwroot\js\index.js を作成し、以下のように編集する
document.getElementById("moveToRoom").addEventListener("click", function (event) {
    var roomName = document.getElementById("roomName").value;
    window.location.href = "Room/" + roomName;
});
  • Pages\Room.cshtml を作成し、以下のように編集する
    • VSを使っている場合はソリューションエクスプローラーで Pages フォルダを右クリックして[追加] -> [新しい項目] -> [Razorページ – 空]を選択すると良い(C#コードビハインドを自動生成してくれる)
@page "{roomname?}"
@model RoomModel
@{
    ViewData["Title"] = "Room";
}

<h1>@ViewData["Title"] : @Model.RoomName</h1>
<input type="hidden" id="roomName" value="@Model.RoomName" />
<div class="container">
    <div class="row"> </div>
    <div class="row">
        <div class="col-2">User</div>
        <div class="col-4"><input type="text" id="userInput" /></div>
    </div>
    <div class="row">
        <div class="col-2">Message</div>
        <div class="col-4"><input type="text" id="messageInput" /></div>
    </div>
    <div class="row"> </div>
    <div class="row">
        <div class="col-6">
            <input type="button" id="sendButton" value="Send Message" />
        </div>
    </div>
</div>
<div class="row">
    <div class="col-12">
        <hr />
    </div>
</div>
<div class="row">
    <div class="col-6">
        <ul id="messagesList"></ul>
    </div>
</div>
<script src="~/js/signalr/dist/browser/signalr.js"></script>
<script src="~/js/chat.js"></script>
  • Pages\Room.cshtml.cs を以下のように変更する
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace SignalRGroupChat.Pages
{
    public class RoomModel : PageModel
    {
        public string RoomName;

        public void OnGet(string roomName)
        {
            RoomName = roomName;
        }
    }
}
ちょっと説明

ここにはURLの末尾をルーム名として設定する処理を書いています。

  • wwwroot\js\chat.js を作成し、以下のように編集する
"use strict";

var connection = new signalR.HubConnectionBuilder().withUrl("/chatHub").build();

document.getElementById("sendButton").disabled = true;

connection.on("ReceiveMessage", function (user, message) {
    var li = document.createElement("li");
    document.getElementById("messagesList").appendChild(li);
    li.textContent = `${user} says ${message}`;
});

connection.start().then(function () {
    document.getElementById("sendButton").disabled = false;

    var roomName = document.getElementById("roomName").value;
    connection.invoke("AddToGroup", roomName).catch(function (err) {
        return console.error(err.toString());
    });

}).catch(function (err) {
    return console.error(err.toString());
});

document.getElementById("sendButton").addEventListener("click", function (event) {
    var roomName = document.getElementById("roomName").value;
    var user = document.getElementById("userInput").value;
    var message = document.getElementById("messageInput").value;
    connection.invoke("SendMessageToGroup", roomName, user, message).catch(function (err) {
        return console.error(err.toString());
    });
    event.preventDefault();
});
ちょっと説明
  • chat.js は公式チュートリアルのコードを参考にしています。
  • 公式チュートリアルに追加したのは以下の2点です。
    • SignalR接続開始時にグループに参加する処理を追加しています。
    • 送信ボタンを押したときにグループを指定してチャットメッセージ送信するようにしています。

アプリを実行する

  • 「F5」キーを押して実行します。
  • ブラウザが起動します。
  • トップページでルーム名を入力するとルーム専用のチャットページに遷移します。
  • 下の画像のようにルームに分かれてメッセージをリアルタイムで送受信できます。
    • 上2つのブラウザウィンドウはRoom1に、下2つはRoom2に参加している、という想定です。
チャットアプリ実行画面
チャットアプリ実行画面

まとめと感想

公式チュートリアルのSignalRサンプルアプリにルーム別メッセージ送受信機能を追加しました。そのためにSignalRのグループの仕組みを使って実装しました。
実装して感じたのはSignalRは高水準ライブラリだということ。クライアントとサーバで情報を送信しあうというより、お互いの関数を実行させる、というイメージでプログラミングできました。
ネットワーク上の流れを意識せずにプログラムの関数レベルでプログラミングできるのは処理内容に集中できるので使いやすいですね。

データベースにMySQLを使ってASP.NET CoreでWeb APIを作成する

公式サイトのチュートリアルをMySQLで実現してみたのでその記録

検証環境

  • OS: Windows 10
  • IDE: Visual Studio 2019

MySQLはDocker Composeで起動

docker-compose.yml を作成し、以下の内容を記載する。
(今回は検証なのでパスワードは一律で password にしておく)

version: '3'
services:
    db:
        image: mariadb:10
        command: mysqld --character-set-server=utf8 --collation-server=utf8_general_ci
        environment:
            MYSQL_DATABASE: todoapi
            MYSQL_USER: todoapi
            MYSQL_PASSWORD: password
            MYSQL_ROOT_PASSWORD: password
        ports:
            - "3306:3306"

以下のコマンドを実行し、MySQLサービスを起動しておく。

docker-compose up -d

以下のコマンドで起動しているか確認できる。

> docker-compose ps   
    Name                  Command               State           Ports
------------------------------------------------------------------------------
todoapi_db_1   docker-entrypoint.sh mysql ...   Up      0.0.0.0:3306->3306/tcp

Webプロジェクトの作成

ここは公式サイト通り

  • [ファイル] -> [新規作成] -> [プロジェクト]
  • テンプレートの中から [ASP.NET Core Webアプリケーション] を選択して [次へ] をクリック
  • プロジェクト名に TodoApi と入力して [作成] をクリック
  • [新しいASP.NET Core Webアプリケーションの作成] ダイアログで [.NET Core][ASP.NET Core 3.1] が選択されていることを確認
  • [API] テンプレートを選択して [作成] をクリック

モデルクラスの追加

ここも公式サイト通り

  • プロジェクト直下に Models フォルダを作成
  • Models フォルダ直下に TodoItem クラスを作成
  • TodoItem クラスに以下のコードを追加
public class TodoItem
{
    public long Id { get; set; }
    public string Name { get; set; }
    public bool IsComplete { get; set; }
}

データベースコンテキストの追加

公式サイトではSQLServerをインストールするが、今回はMySQLをインストールする。

  • [ツール] -> [NuGetパッケージマネージャー] -> [ソリューションのNuGetパッケージの管理]
  • [参照] タブを選択し、検索ボックスに Pomelo.EntityFrameworkCore.MySql と入力
  • 左側のウィンドウで [Pomelo.EntityFrameworkCore.MySql] を選択
  • 右側のウィンドウで [プロジェクト] チェックボックスをオンにして [インストール] をクリック

TodoContextデータベースコンテキストの追加

ここは公式サイト通り

  • Models フォルダ直下に TodoContext クラスを作成
  • TodoContext クラスに以下のコードを追加
using Microsoft.EntityFrameworkCore;

namespace TodoApi.Models
{
    public class TodoContext : DbContext
    {
        public TodoContext(DbContextOptions<TodoContext> options)
            : base(options)
        {
        }

        public DbSet<TodoItem> TodoItems { get; set; }
    }
}

データベースコンテキストの登録

データベース接続情報を記載するため、公式サイトとは異なる設定をする。

  • appsettings.json を編集して以下を追加
  "ConnectionStrings": {
    "MySQL": "Server=127.0.0.1;Database=todoapi;User=todoapi;Password=password;"
  }
  • Startup.cs を編集して以下を追加
using Microsoft.EntityFrameworkCore;
using TodoApi.Models;

(中略)

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<TodoContext>(opt =>
                opt.UseMySql(Configuration.GetConnectionString("MySQL")));
            services.AddControllers();
        }

コントローラーのスキャフォールディング

公式サイト通り

  • Controllers フォルダを右クリック
  • [追加] -> [新規スキャフォールディングアイテム]
  • [Entity Frameworkを使用したアクションがあるAPIコントローラー] を選択して [追加] をクリック
  • [Entity Frameworkを使用したアクションがあるAPIコントローラーの追加] ダイアログで以下を選択
    • モデルクラスで [TodoItem (TodoApi.Models)] を選択
    • データコンテキストクラスで [TodoContext (TodoApi.Models)] を選択
  • [追加] をクリック

PostTodoItem作成メソッドの確認

公式サイト通り

  • TodoItemsController.csPostTodoItem メソッドの return文を以下のように変更
return CreatedAtAction(nameof(GetTodoItem), new { id = todoItem.Id }, todoItem);

データベースのマイグレーション

ここは公式サイトにはない手順。
マイグレーションはコマンドラインから行う。
Visual Studioのプロジェクトを作成したフォルダで以下のコマンドを実行する。

dotnet ef migrations add DbInit
dotnet ef database update

(参考) dotnet-ef サブコマンドがインストールされていなければ以下のコマンドでインストールする

dotnet tool install --global dotnet-ef

POSTメソッドの確認

公式サイトと同じようにPostmanを使って確認する。

GETメソッドの確認

公式サイトと同じようにPostmanを使って確認する。

データベースを直接確認する

以下のコマンドでデータが作成されていることを確認する。

> docker exec -t todoapi_db_1 mysql -u todoapi -ppassword todoapi -e "SELECT * FROM TodoItems;"
+----+----------+------------+
| Id | Name     | IsComplete |
+----+----------+------------+
|  1 | walk dog |          1 |
+----+----------+------------+

今回はここまで