LoginSignup
2
0

More than 3 years have passed since last update.

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

Last updated at Posted at 2021-03-02

記事の内容

  • 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
--* 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

a09.png

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


CREATE DATABASE sample_db;

a13.png

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

\c sample_db

a14.png

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

a15.png

以下の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;

a16.png

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


SELECT
    *
FROM
    weather_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の「localhost」を「192.168.1.7」に書き換えて、後ほどBlazorアプリで使用します。

ここでブラウザからGraphiQLにアクセスして、クエリーを発行したりドキュメントを見たりしてみましょう。

クエリー例


query allWeatherForecasts {
  allWeatherForecasts {
    nodes {
      id
      dt
      temperatureC
      summary
    }
  }
}

ブラウザ画面

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

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」というディレクトリを作成します。

a22.png

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

a23.png

以下の内容にします。

SamplePostGraphile/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クラスを使えるようにします。
変更内容は以下の通りです。

Program.cs

+ 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に置き換えるようにソースコードを変更します。
変更内容は以下の通りです。

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で作り直す、というステップを踏むより負担が少ない気がします。

以上です。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0