記事の内容
- Visual Studio 2019でBlazorアプリのテンプレートを選んでプロジェクトを新規作成すると、データソースがjsonファイルになりましたが、それをPostGraphile(GraphQL API)に置き換えます。
- その後、Telerik UI for Blazor(有償商品です)のGridコンポーネントを使用して、データをグリッドで一覧表示するWebページを追加します。サードパーティーのコンポーネントを導入すると、定型的な処理の開発時間を大幅に短縮でき、プログラマが追加でコードを記述すれば小回りも利きますので、ローコードの感覚になります。
※ PostGraphileのトップページに「No N+1 problem」と書かれています。いいですね~
※ HasuraもPostGraphileと類似のプロダクトと認識していますが、本記事では私が利用経験のあるPostGraphileを使用しました。
※ 私はTelerikの回し者ではありません。
ソースコード
GitHubに置きました。
参考ページ(感謝します)
C#(ASP.NET Core)で GraphQL API を提供する
Blazorアプリのプロジェクトを新規作成する
Visual Studio 2019のプロジェクト新規作成画面で、以下のようにBlazorアプリのテンプレートを選択します。
プロジェクト名を「SamplePostGraphile」、ソリューション名を「SamplePostGraphile_sol」にしましたが、名前は何でも良いです。
Blazor WebAssembly Appを選択します。今回はhttpsは外しました。
以下のファイルが自動生成されました。
ソースコードを読むと、データソースとしてwwwroot\sample-data\weather.jsonが使用されています。
このままビルドして動かしてみます。
左メニューから「Fetch data」を選択すると、以下のように右側にjsonファイルのデータが一覧表示されました。
この時点でgit commitしました。
手順は、Visual Studioのgitメニューからgitリポジトリを作成し、GitHubにpushしました。
以下のコミットメッセージは、Visual Studioが自動生成したものです。
PostgreSQLにデータを用意し、PostGraphileを立ち上げる
ERモデリングツールでの作業
ERモデリングツールはA5:SQL Mk-2を使用します。
それでは、BlazorアプリのデータソースをPostGraphileに置き換えます。
以下のjsonファイルの中を見ながら、これと類似のテストデータをPostgreSQLに用意します。
[
{
"date": "2018-05-06",
"temperatureC": 1,
"summary": "Freezing"
},
{
"date": "2018-05-07",
"temperatureC": 14,
"summary": "Bracing"
},
{
"date": "2018-05-08",
"temperatureC": -13,
"summary": "Freezing"
},
{
"date": "2018-05-09",
"temperatureC": -16,
"summary": "Balmy"
},
{
"date": "2018-05-10",
"temperatureC": -2,
"summary": "Chilly"
}
]
以下のER図を描きました。エンティティ1つだけですね。
ER図メニューから「DDLを作成する」を選択します。
RDBMS種類でPostgreSQLを選択し、DDL生成ボタンを押します。
以下のDDLが生成されました。
-- RDBMS Type : PostgreSQL
-- Application : A5:SQL Mk-2
/*
BackupToTempTable, RestoreFromTempTable疑似命令が付加されています。
これにより、drop table, create table 後もデータが残ります。
この機能は一時的に $$TableName のような一時テーブルを作成します。
*/
-- WeatherForecast
--* BackupToTempTable
DROP TABLE if exists weather_forecasts CASCADE;
--* RestoreFromTempTable
CREATE TABLE weather_forecasts (
id integer NOT NULL
, dt date NOT NULL
, temperature_c double precision NOT NULL
, summary character varying NOT NULL
, CONSTRAINT weather_forecasts_PKC PRIMARY KEY (id)
) ;
COMMENT ON TABLE weather_forecasts IS 'WeatherForecast';
COMMENT ON COLUMN weather_forecasts.id IS 'Id';
COMMENT ON COLUMN weather_forecasts.dt IS 'Date';
COMMENT ON COLUMN weather_forecasts.temperature_c IS 'TemperatureC';
COMMENT ON COLUMN weather_forecasts.summary IS 'Summary';
PostgreSQLの作業
本記事ではLinux上のPostgreSQLを使用します。
psqlを起動します。
psql --host=localhost --username=postgres --password
データベースを作成します。名前を「sample_db」にしましたが、何でも良いです。
CREATE DATABASE sample_db;
カレントデータベースを、作成したsample_dbに切り替えます。
\c sample_db
先ほどERモデリングツールが生成したDDLをpsqlにコピペして実行します。
以下のINSERT文を流して、2000年1月1日から150日分のテストデータを作成します。
テーブルにはidの降順でINSERTしてみます。
INSERT INTO weather_forecasts (
id,
dt,
temperature_c,
summary
)
SELECT
id,
('1999-12-31'::DATE + (id::TEXT || ' days')::INTERVAL)::DATE AS dt,
(random() * 75 - 20)::INT AS temperature_c,
CASE (random() * 1000)::INT % 10
WHEN 0 THEN 'Freezing'
WHEN 1 THEN 'Bracing'
WHEN 2 THEN 'Chilly'
WHEN 3 THEN 'Cool'
WHEN 4 THEN 'Mild'
WHEN 5 THEN 'Warm'
WHEN 6 THEN 'Balmy'
WHEN 7 THEN 'Hot'
WHEN 8 THEN 'Sweltering'
WHEN 9 THEN 'Scorching'
END AS summary
FROM
generate_series(1, 150) AS id
ORDER BY id DESC;
以下のSELECT文を流して、データが作成されたか確認します。
SELECT
*
FROM
weather_forecasts;
以下のようにidの降順で表示されましたが、順番に意味はありません。
psqlから抜けます。
PostGraphileの作業
本記事ではPostGraphileをPostgreSQLと同じLinuxホストにインストールします。
このページを参考にして、PostGraphileをインストール&起動します。
Dockerを使う方法もあります。
インストール
npm install -g postgraphile
起動コマンド例
postgraphile --connection postgres://postgres:secret@localhost/sample_db --port 15000 --schema public --export-schema-graphql ~/schema.graphql --cors
起動画面
本記事ではBlazorアプリでのCORSエラーを避けるために、単に「--cors」オプションを付けてPostGraphileを起動しましたが、本番環境では安全な方法でCORSエラーを回避してください。
postgraphileコマンドを起動するだけで、PostgreSQLのスキーマを読み取ってGraphQLエンドポイントを自動生成してくれます。
とても楽で、これもノーコードと言えるかもしれません。
起動画面によれば、URLは
- GraphQLエンドポイント:http://localhost:15000/graphql
- GraphiQL:http://localhost:15000/graphiql
となっています。
本記事では、このLinuxホストのIPアドレスは「192.168.1.7」です。
GraphQLエンドポイントのURLの「localhost」を「192.168.1.7」に書き換えて、後ほどBlazorアプリで使用します。
ここでブラウザからGraphiQLにアクセスして、クエリーを発行したりドキュメントを見たりしてみましょう。
クエリー例
query allWeatherForecasts {
allWeatherForecasts {
nodes {
id
dt
temperatureC
summary
}
}
}
ブラウザ画面
レスポンス
{
"data": {
"allWeatherForecasts": {
"nodes": [
{
"id": 1,
"dt": "2000-01-01",
"temperatureC": 7,
"summary": "Hot"
},
{
"id": 2,
"dt": "2000-01-02",
"temperatureC": -16,
"summary": "Cool"
},
{
"id": 3,
"dt": "2000-01-03",
"temperatureC": 17,
"summary": "Hot"
},
(中略)
{
"id": 150,
"dt": "2000-05-29",
"temperatureC": 14,
"summary": "Freezing"
}
]
}
}
}
psqlからSELECT文を実行したときはid列の降順で表示されましたが、今回のレスポンスを見ると昇順になっていますね。
この順番は気にしないことにして、先に進みます。
BlazorアプリのデータソースをjsonファイルからPostGraphileに置き換える
Visual Studioでの作業に戻ります。
ファイル削除:wwwroot\sample-data\weather.json
Blazorアプリのデータソースは「wwwroot\sample-data\weather.json」でしたが、もう使用しませんのでsample-dataディレクトリごと削除します。
削除後のファイルは以下の通り。
パッケージのインストール
NuGetで以下の3パッケージをインストールします。
ファイル新規作成:Shared/WeatherForecast.cs
GraphQL APIのレスポンスデータを格納するデータ構造を作成します。
Sharedディレクトリ配下に「WeatherForecast.cs」を追加します。
以下の内容にします。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace SamplePostGraphile.Shared
{
// クエリー
// query allWeatherForecasts {
// allWeatherForecasts {
// nodes {
// id
// dt
// temperatureC
// summary
// }
// }
// }
public class WeatherForecast
{
public int Id { get; set; }
public DateTime Dt { get; set; }
private double _tempC;
public double TemperatureC
{
get
{
return _tempC;
}
set
{
_tempC = value;
}
}
public double TemperatureF
{
get
{
return 32 + (_tempC / 0.5556);
}
set
{
_tempC = (value - 32) * 0.5556;
}
}
public string Summary { get; set; }
public WeatherForecast()
{
Dt = DateTime.Now.Date;
}
}
// レスポンス例
// {
// "data": {
// "allWeatherForecasts": {
// "nodes": [
// {
// "id": 1,
// "dt": "2000-01-01",
// "temperatureC": 7,
// "summary": "Hot"
// },
// {
// "id": 2,
// "dt": "2000-01-02",
// "temperatureC": -16,
// "summary": "Cool"
// },
//
// (中略)
//
// ]
// }
// }
// }
public class AllWeatherForecastsResponse
{
public AllWeatherForecastsContent allWeatherForecasts { get; set; }
public class AllWeatherForecastsContent
{
public List<WeatherForecast> Nodes { get; set; }
}
}
}
ファイル新規作成:Services/WeatherForecastService.cs
GraphQLクエリーを発行して、そのレスポンスからデータを取り出してリターンするメソッドを持つクラスを作成します。
本記事では、CRUDのうちR(Read)のみ実装しました。
プロジェクト配下に「Services」というディレクトリを作成します。
Servicesディレクトリ配下に「WeatherForecastService.cs」を追加します。
以下の内容にします。
using SamplePostGraphile.Shared;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using GraphQL.Client.Http;
using GraphQL.Client.Serializer.Newtonsoft;
using GraphQL;
namespace SamplePostGraphile.Services
{
public class WeatherForecastService
{
// 行儀が良くないですが、今回はここにGraphQLエンドポイントのURLを書いてしまいます
private const string graphql_http = "http://192.168.1.7/15000/graphql";
public async Task<List<WeatherForecast>> GetForecastListAsync()
{
using var graphQLClient = new GraphQLHttpClient(graphql_http, new NewtonsoftJsonSerializer());
var allWeatherForecasts = new GraphQLRequest
{
Query = @"
query allWeatherForecasts {
allWeatherForecasts {
nodes {
id
dt
temperatureC
summary
}
}
}
",
OperationName = "allWeatherForecasts",
};
var graphQLResponse = await graphQLClient.SendQueryAsync<AllWeatherForecastsResponse>(allWeatherForecasts);
return graphQLResponse.Data.allWeatherForecasts.Nodes;
}
//public async Task UpdateForecastAsync(WeatherForecast forecastToUpdate)
//{
// 未実装
//}
//public async Task DeleteForecastAsync(WeatherForecast forecastToRemove)
//{
// 未実装
//}
//public async Task InsertForecastAsync(WeatherForecast forecastToInsert)
//{
// 未実装
//}
}
}
変更:Program.cs
プロジェクト内でWeatherForecastServiceクラスを使えるようにします。
変更内容は以下の通りです。
+ using SamplePostGraphile.Services;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
namespace SamplePostGraphile
{
public class Program
{
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("app");
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
+ builder.Services.AddScoped<WeatherForecastService>();
await builder.Build().RunAsync();
}
}
}
変更:Pages/FetchData.razor
データソースをjsonファイルからPostGraphileに置き換えるようにソースコードを変更します。
変更内容は以下の通りです。
@page "/fetchdata"
- @inject HttpClient Http
+ @using SamplePostGraphile.Shared
+ @using SamplePostGraphile.Services
+ @inject WeatherForecastService ForecastService
<h1>Weather forecast</h1>
- <p>This component demonstrates fetching data from the server.</p>
+ <p>This component demonstrates fetching data from the postgraphile server.</p>
@if (forecasts == null)
{
<p><em>Loading...</em></p>
}
else
{
<table class="table">
<thead>
<tr>
<th>Date</th>
<th>Temp. (C)</th>
<th>Temp. (F)</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
@foreach (var forecast in forecasts)
{
<tr>
- <td>@forecast.Date.ToShortDateString()</td>
+ <td>@forecast.Dt.ToShortDateString()</td>
<td>@forecast.TemperatureC</td>
<td>@forecast.TemperatureF</td>
<td>@forecast.Summary</td>
</tr>
}
</tbody>
</table>
}
@code {
- private WeatherForecast[] forecasts;
+ List<WeatherForecast> forecasts { get; set; }
protected override async Task OnInitializedAsync()
{
- forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("sample-data/weather.json");
+ await GetForecasts();
}
+ async Task GetForecasts()
+ {
+ forecasts = await ForecastService.GetForecastListAsync();
+ }
- public class WeatherForecast
- {
- public DateTime Date { get; set; }
-
- public int TemperatureC { get; set; }
-
- public string Summary { get; set; }
-
- public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
- }
}
ビルドして動かしてみます。
左メニューから「Fetch data」を選択すると、以下のように右側にPostgreSQLデータベースから取得したデータが一覧表示されました。
以上でデータソースの置き換えは完了です。
この時点でgit commitしました。
git add -A
git commit -m "(1)データソースをweather.jsonからPostGraphileに変更します"
ここまでのソースコードは、Telerikのコンポーネントがなくてもビルド/実行できます。
Telerik UI for BlazorのGridコンポーネントを使ってデータを一覧表示する
これ以降は、Telerikのプロダクトがインストールされた環境で作業します。
プロジェクトをTelerik UI for Blazorのアプリケーションにコンバートする
以下のように、Visual Studioの拡張機能メニューからTelerikアプリケーションにコンバートします。
NuGetパッケージの管理画面で、Telerik.UI.for.Blazorがインストールされたことを確認します。
コンバート完了時点で、一旦git commitしました。
git add -A
git commit -m "(2)プロジェクトをTelerikアプリケーションにコンバートします"
ファイル新規作成:Pages/Grid.razor
TelerikのGridコンポーネントを使用して、データを一覧表示するページを作成します。
Pagesディレクトリ配下に「Grid.razor」を追加します。
以下の内容にします。
@page "/grid"
@using SamplePostGraphile.Shared
@using SamplePostGraphile.Services
@inject WeatherForecastService ForecastService
<div class="container-fluid">
<div class='row my-4'>
<div class='col-12 col-lg-9 border-right'>
<TelerikGrid Data="@forecasts" Height="550px" FilterMode="@GridFilterMode.FilterMenu"
Sortable="true" Pageable="true" PageSize="20" Groupable="true" Resizable="true" Reorderable="true"
OnUpdate="@UpdateHandler" OnDelete="@DeleteHandler" OnCreate="@CreateHandler" EditMode="@GridEditMode.Inline">
<GridColumns>
<GridColumn Field="Id" Title="Id" Width="100px" Editable="false" Groupable="false" />
<GridColumn Field="Dt" Title="Date" Width="220px" DisplayFormat="{0:dddd, dd MMM yyyy}" />
<GridColumn Field="TemperatureC" Title="Temp. C" Width="100px" DisplayFormat="{0:N1}" />
<GridColumn Field="TemperatureF" Title="Temp. F" Width="100px" DisplayFormat="{0:N1}" />
<GridColumn Field="Summary" />
<GridCommandColumn Width="200px" Resizable="false">
<GridCommandButton Command="Save" Icon="@IconName.Save" ShowInEdit="true">Update</GridCommandButton>
<GridCommandButton Command="Edit" Icon="@IconName.Edit" Primary="true">Edit</GridCommandButton>
<GridCommandButton Command="Delete" Icon="@IconName.Delete">Delete</GridCommandButton>
<GridCommandButton Command="Cancel" Icon="@IconName.Cancel" ShowInEdit="true">Cancel</GridCommandButton>
</GridCommandColumn>
</GridColumns>
<GridToolBar>
<GridCommandButton Command="Add" Icon="@IconName.Plus" Primary="true">Add Forecast</GridCommandButton>
<GridCommandButton Command="ExcelExport" Icon="@IconName.FileExcel">Export to Excel</GridCommandButton>
</GridToolBar>
<GridExport>
<GridExcelExport FileName="weather-forecasts" AllPages="true" />
</GridExport>
</TelerikGrid>
</div>
<div class='col-12 col-lg-3 mt-3 mt-lg-0'>
<h3>Telerik UI for Blazor Grid</h3>
</div>
</div>
</div>
@code {
List<WeatherForecast> forecasts { get; set; }
protected override async Task OnInitializedAsync()
{
await GetForecasts();
}
async Task GetForecasts()
{
forecasts = await ForecastService.GetForecastListAsync();
}
public async Task DeleteHandler(GridCommandEventArgs args)
{
//WeatherForecast currItem = args.Item as WeatherForecast;
//await ForecastService.DeleteForecastAsync(currItem);
//await GetForecasts();
}
public async Task CreateHandler(GridCommandEventArgs args)
{
//WeatherForecast currItem = args.Item as WeatherForecast;
//await ForecastService.InsertForecastAsync(currItem);
//await GetForecasts();
}
public async Task UpdateHandler(GridCommandEventArgs args)
{
//WeatherForecast currItem = args.Item as WeatherForecast;
//await ForecastService.UpdateForecastAsync(currItem);
//await GetForecasts();
}
}
変更:Shared/NavMenu.razor
実行時の左メニューにGridを追加します。
変更内容は以下の通りです。
<div class="top-row pl-4 navbar navbar-dark">
<a class="navbar-brand" href="">SamplePostGraphile</a>
<button class="navbar-toggler" @onclick="ToggleNavMenu">
<span class="navbar-toggler-icon"></span>
</button>
</div>
<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
<ul class="nav flex-column">
<li class="nav-item px-3">
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
<span class="oi oi-home" aria-hidden="true"></span> Home
</NavLink>
</li>
<li class="nav-item px-3">
<NavLink class="nav-link" href="counter">
<span class="oi oi-plus" aria-hidden="true"></span> Counter
</NavLink>
</li>
<li class="nav-item px-3">
<NavLink class="nav-link" href="fetchdata">
<span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data
</NavLink>
</li>
+ <li class="nav-item px-3">
+ <NavLink class="nav-link" href="grid">
+ <span class="oi oi-grid-four-up" aria-hidden="true"></span> Grid
+ </NavLink>
+ </li>
</ul>
</div>
@code {
private bool collapseNavMenu = true;
private string NavMenuCssClass => collapseNavMenu ? "collapse" : null;
private void ToggleNavMenu()
{
collapseNavMenu = !collapseNavMenu;
}
}
ビルドして動かしてみます。
以下のように左メニューに「Grid」が追加されました。これを選択すると、右側にTelerikのGridコンポーネントでデータが一覧表示されました。
CRUDのうちRしか実装していませんが、試しに任意の行のEditボタンを押してDate列の右端をクリックしてみます。以下のようにカレンダー入力が出てきました。
以上で作業が完了しましたので、git commitしました。
git add -A
git commit -m "(3)Grid.razorページを追加します"
GitHubにもpushしました。
今後
TelerikのGridコンポーネントは機能がリッチだそうですので、深堀りしてみたいですね。
サードパーティーのコンポーネントに習熟すれば、ノーコードに劣らないスピード感でアプリを開発できそうです。
むしろ、数多あるNoCodeから適切なものを選ぶ→NoCodeで開発する→場合によってはYesCodeで作り直す、というステップを踏むより負担が少ない気がします。
以上です。