Quantcast
Channel: C#タグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 9691

BlazorアプリからPostGraphile(GraphQL)とTelerikのGridコンポーネントを使ってみた

$
0
0

記事の内容

  • 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アプリのテンプレートを選択します。

a01.png

プロジェクト名を「SamplePostGraphile」、ソリューション名を「SamplePostGraphile_sol」にしましたが、名前は何でも良いです。

a02.png

Blazor WebAssembly Appを選択します。今回はhttpsは外しました。

a03.png

以下のファイルが自動生成されました。
ソースコードを読むと、データソースとしてwwwroot\sample-data\weather.jsonが使用されています。

a04.png

このままビルドして動かしてみます。
左メニューから「Fetch data」を選択すると、以下のように右側にjsonファイルのデータが一覧表示されました。

a05.png

この時点でgit commitしました。
手順は、Visual Studioのgitメニューからgitリポジトリを作成し、GitHubにpushしました。
以下のコミットメッセージは、Visual Studioが自動生成したものです。

a55.png

PostgreSQLにデータを用意し、PostGraphileを立ち上げる

ERモデリングツールでの作業

ERモデリングツールはA5:SQL Mk-2を使用します。

それでは、BlazorアプリのデータソースをPostGraphileに置き換えます。
以下のjsonファイルの中を見ながら、これと類似のテストデータをPostgreSQLに用意します。

wwwroot\sample-data\weather.json
[{"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つだけですね。

a10.png

ER図メニューから「DDLを作成する」を選択します。

a11.png

RDBMS種類でPostgreSQLを選択し、DDL生成ボタンを押します。

a12.png

以下のDDLが生成されました。

-- RDBMS Type   : PostgreSQL-- Application  : A5:SQL Mk-2/*
  BackupToTempTable, RestoreFromTempTable疑似命令が付加されています。
  これにより、drop table, create table 後もデータが残ります。
  この機能は一時的に $$TableName のような一時テーブルを作成します。
*/-- WeatherForecast--* BackupToTempTableDROPTABLEifexistsweather_forecastsCASCADE;--* RestoreFromTempTableCREATETABLEweather_forecasts(idintegerNOTNULL,dtdateNOTNULL,temperature_cdoubleprecisionNOTNULL,summarycharactervaryingNOTNULL,CONSTRAINTweather_forecasts_PKCPRIMARYKEY(id));COMMENTONTABLEweather_forecastsIS'WeatherForecast';COMMENTONCOLUMNweather_forecasts.idIS'Id';COMMENTONCOLUMNweather_forecasts.dtIS'Date';COMMENTONCOLUMNweather_forecasts.temperature_cIS'TemperatureC';COMMENTONCOLUMNweather_forecasts.summaryIS'Summary';

PostgreSQLの作業

本記事ではLinux上のPostgreSQLを使用します。
psqlを起動します。

psql --host=localhost --username=postgres --password

a09.png

データベースを作成します。名前を「sample_db」にしましたが、何でも良いです。

CREATEDATABASEsample_db;

a13.png

カレントデータベースを、作成したsample_dbに切り替えます。

\c sample_db

a14.png

先ほどERモデリングツールが生成したDDLをpsqlにコピペして実行します。

a15.png

以下のINSERT文を流して、2000年1月1日から150日分のテストデータを作成します。
テーブルにはidの降順でINSERTしてみます。

INSERTINTOweather_forecasts(id,dt,temperature_c,summary)SELECTid,('1999-12-31'::DATE+(id::TEXT||' days')::INTERVAL)::DATEASdt,(random()*75-20)::INTAStemperature_c,CASE(random()*1000)::INT%10WHEN0THEN'Freezing'WHEN1THEN'Bracing'WHEN2THEN'Chilly'WHEN3THEN'Cool'WHEN4THEN'Mild'WHEN5THEN'Warm'WHEN6THEN'Balmy'WHEN7THEN'Hot'WHEN8THEN'Sweltering'WHEN9THEN'Scorching'ENDASsummaryFROMgenerate_series(1,150)ASidORDERBYidDESC;

a16.png

以下のSELECT文を流して、データが作成されたか確認します。

SELECT*FROMweather_forecasts;

以下のようにidの降順で表示されましたが、順番に意味はありません。

a17.png

psqlから抜けます。

a18.png

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

起動画面

a28.png

本記事ではBlazorアプリでのCORSエラーを避けるために、単に「--cors」オプションを付けてPostGraphileを起動しましたが、本番環境では安全な方法でCORSエラーを回避してください。

postgraphileコマンドを起動するだけで、PostgreSQLのスキーマを読み取ってGraphQLエンドポイントを自動生成してくれます。
とても楽で、これもノーコードと言えるかもしれません。

起動画面によれば、URLは

となっています。
本記事では、このLinuxホストのIPアドレスは「192.168.1.7」です。
GraphQLエンドポイントのURLは、後ほどBlazorアプリから使用します。
ここではブラウザからGraphiQLにアクセスして、クエリーを発行したりドキュメントを見たりしてみましょう。

クエリー例

queryallWeatherForecasts{allWeatherForecasts{nodes{iddttemperatureCsummary}}}

ブラウザ画面

a27.png

レスポンス

{
  "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ディレクトリごと削除します。
削除後のファイルは以下の通り。

a07.png

パッケージのインストール

NuGetで以下の3パッケージをインストールします。

a29.png

ファイル新規作成:Shared/WeatherForecast.cs

GraphQL APIのレスポンスデータを格納するデータ構造を作成します。
Sharedディレクトリ配下に「WeatherForecast.cs」を追加します。

a31.png

a32.png

以下の内容にします。

Shared/WeatherForecast.cs
usingSystem;usingSystem.Collections.Generic;usingSystem.Linq;usingSystem.Threading.Tasks;namespaceSamplePostGraphile.Shared{// クエリー// query allWeatherForecasts {//   allWeatherForecasts {//     nodes {//       id//       dt//       temperatureC//       summary//     }//   }// }publicclassWeatherForecast{publicintId{get;set;}publicDateTimeDt{get;set;}privatedouble_tempC;publicdoubleTemperatureC{get{return_tempC;}set{_tempC=value;}}publicdoubleTemperatureF{get{return32+(_tempC/0.5556);}set{_tempC=(value-32)*0.5556;}}publicstringSummary{get;set;}publicWeatherForecast(){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"//         },////         (中略)////       ]//     }//   }// }publicclassAllWeatherForecastsResponse{publicAllWeatherForecastsContentallWeatherForecasts{get;set;}publicclassAllWeatherForecastsContent{publicList<WeatherForecast>Nodes{get;set;}}}}

ファイル新規作成:Services/WeatherForecastService.cs

GraphQLクエリーを発行して、そのレスポンスからデータを取り出してリターンするメソッドを持つクラスを作成します。
本記事では、CRUDのうちR(Read)のみ実装しました。
プロジェクト配下に「Services」というディレクトリを作成します。

a22.png

Servicesディレクトリ配下に「WeatherForecastService.cs」を追加します。

a23.png

以下の内容にします。

SamplePostGraphile/Services/WeatherForecastService.cs
usingSamplePostGraphile.Shared;usingSystem;usingSystem.Collections.Generic;usingSystem.Linq;usingSystem.Threading.Tasks;usingGraphQL.Client.Http;usingGraphQL.Client.Serializer.Newtonsoft;usingGraphQL;namespaceSamplePostGraphile.Services{publicclassWeatherForecastService{// 行儀が良くないですが、今回はここにGraphQLエンドポイントのURLを書いてしまいますprivateconststringgraphql_http="http://192.168.1.7/15000/graphql";publicasyncTask<List<WeatherForecast>>GetForecastListAsync(){usingvargraphQLClient=newGraphQLHttpClient(graphql_http,newNewtonsoftJsonSerializer());varallWeatherForecasts=newGraphQLRequest{Query=@"
query allWeatherForecasts {
  allWeatherForecasts {
    nodes {
      id
      dt
      temperatureC
      summary
    }
  }
}
",OperationName="allWeatherForecasts",};vargraphQLResponse=awaitgraphQLClient.SendQueryAsync<AllWeatherForecastsResponse>(allWeatherForecasts);returngraphQLResponse.Data.allWeatherForecasts.Nodes;}//public async Task UpdateForecastAsync(WeatherForecast forecastToUpdate)//{//    未実装//}//public async Task DeleteForecastAsync(WeatherForecast forecastToRemove)//{//    未実装//}//public async Task InsertForecastAsync(WeatherForecast forecastToInsert)//{//    未実装//}}}

変更:Program.cs

プロジェクト内でWeatherForecastServiceクラスを使えるようにします。
変更内容は以下の通りです。

Program.cs
+usingSamplePostGraphile.Services;usingMicrosoft.AspNetCore.Components.WebAssembly.Hosting;usingMicrosoft.Extensions.Configuration;usingMicrosoft.Extensions.DependencyInjection;usingMicrosoft.Extensions.Logging;usingSystem;usingSystem.Collections.Generic;usingSystem.Net.Http;usingSystem.Text;usingSystem.Threading.Tasks;namespaceSamplePostGraphile{publicclassProgram{publicstaticasyncTaskMain(string[]args){varbuilder=WebAssemblyHostBuilder.CreateDefault(args);builder.RootComponents.Add<App>("app");builder.Services.AddScoped(sp=>newHttpClient{BaseAddress=newUri(builder.HostEnvironment.BaseAddress)});+builder.Services.AddScoped<WeatherForecastService>();awaitbuilder.Build().RunAsync();}}}

変更:Pages/FetchData.razor

データソースをjsonファイルからPostGraphileに置き換えるようにソースコードを変更します。
変更内容は以下の通りです。

Pages/FetchData.razor
@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データベースから取得したデータが一覧表示されました。

a33.png

以上でデータソースの置き換えは完了です。
この時点で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アプリケーションにコンバートします。

a51.png

NuGetパッケージの管理画面で、Telerik.UI.for.Blazorがインストールされたことを確認します。

a52.png

コンバート完了時点で、一旦git commitしました。

git add -A
git commit -m "(2)プロジェクトをTelerikアプリケーションにコンバートします"

ファイル新規作成:Pages/Grid.razor

TelerikのGridコンポーネントを使用して、データを一覧表示するページを作成します。
Pagesディレクトリ配下に「Grid.razor」を追加します。

a42.png

a43.png

以下の内容にします。

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を追加します。
変更内容は以下の通りです。

Shared/NavMenu.razor
<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コンポーネントでデータが一覧表示されました。

a53.png

CRUDのうちRしか実装していませんが、試しに任意の行のEditボタンを押してDate列の右端をクリックしてみます。以下のようにカレンダー入力が出てきました。

a54.png

以上で作業が完了しましたので、git commitしました。

git add -A
git commit -m "(3)Grid.razorページを追加します"

GitHubにもpushしました。

a56.png

今後

TelerikのGridコンポーネントは機能がリッチだそうですので、深堀りしてみたいですね。
サードパーティーのコンポーネントに習熟すれば、ノーコードに劣らないスピード感でアプリを開発できそうです。
むしろ、数多あるNoCodeから適切なものを選ぶ→NoCodeで開発する→場合によってはYesCodeで作り直す、というステップを踏むより負担が少ない気がします。

以上です。


Viewing all articles
Browse latest Browse all 9691

Trending Articles