sh1’s diary

プログラミング、読んだ本、資格試験、ゲームとか私を記録するところ

PlanetScale - SELECT INSERT UPDATE を実行する (Entity Framework)

f:id:shikaku_sh:20220122172122p:plain

PlanetScale (MySQL 互換) の基本的な DML (select, insert, update) を C# の環境で実行してみた。今回は、Entity Framework を介してデータ操作をしてみる。

なお、EF を使わないやり方はこっち

接続文字列のパラメーターは、NuGet から「DotNetEnv」を使って隠しています。

DotNetEnv.Env.Load(".env");

var server = DotNetEnv.Env.GetString("SERVER");
var user = DotNetEnv.Env.GetString("USER");
var password = DotNetEnv.Env.GetString("PASSWORD");
var database = DotNetEnv.Env.GetString("DATABASE");
var port = DotNetEnv.Env.GetString("PORT");
var ssl = DotNetEnv.Env.GetString("SSLMODE");

var connectionString = $"server={server};user={user};database={database};port={port};password={password};SslMode={ssl}";

MySqlContext.ConnectionString = connectionString;

初期化

Entity Framework を利用するときは、App.configapp.config.transformADO.NET のプロバイダーの初期化などをすると思います。

コードから直接書いた一例は、こんな感じ:

public class MySqlConfiguration : DbConfiguration
{
    public MySqlConfiguration()
    {
        SetDefaultConnectionFactory(new MySqlConnectionFactory());

        // MySql.Data.MySqlClient
        string name = MySql.Data.EntityFramework.MySqlProviderInvariantName.ProviderName;

        SetProviderFactory(name, new MySql.Data.MySqlClient.MySqlClientFactory());
        SetProviderServices(name, new MySql.Data.MySqlClient.MySqlProviderServices());
    }
}

App ファイルにするのもよいですが、コードで書いておくとデータベース自体の切替なんかに対応しやすいかもしれないです。初期化のあと DbContext を作成します。

Users テーブル(サンプル)を開く DbContext の一例。

public class DbContextBase : DbContext
{
    public DbSet<Users> Users => Set<Users>();

    //public DbSet<Users> Users { get; set; } = null!;

    public DbContextBase(DbConnection connection) : base (connection, true)
    {

    }
}
[DebuggerDisplay("接続文字列:{ConnectionString}")]
public class MySqlContext : DbContextBase
{
    public static string ConnectionString { get; set; } = "";

    public MySqlContext() : base(new MySqlConnection(ConnectionString))
    {
    }
}
[Table("users")]
public class Users
{
    [Key]
    [Column("id")]
    public int? Id { get; set; }

    [Column("email")]
    public string Email { get; set; } = "";

    [Column("first_name")]
    public string FirstName { get; set; } = "";

    [Column("last_name")]
    public string LastName { get; set; } = "";
}

カラムではない拡張等が目的のプロパティは NotMapped をつける。

あとは各種 DML で操作してみるけど、実際の前準備は以下のとおり。MySqlConfiguration は(EF を利用するタイミングで)自動的に呼び出されるので気にしなくてよいです。

SELECT

SELECT 文を実行する例はこんな感じ。

private void SelectEF()
{
    using (var context = new MySqlContext())
    {
        foreach (var u in context.Users)
        {
            Console.WriteLine($"id={u.Id}, email={u.Email}");
        }
    }
}

INSERT

INSERT 文を実行する例はこんな感じ。

private void InsertEF()
{
    var newUser = new Users
    {
        Email = "EF@aa.bb",
        FirstName = "EF_firstname",
        LastName = "EF_lastname",
    };

    using (var context = new MySqlContext())
    {
        context.Users.Add(newUser);
        context.SaveChanges();
    }
}

User は id を持っています。割り振られた id は、SaveChanges() を実行したあとにパラメーターの値が更新されています。忘れがち。

f:id:shikaku_sh:20220204105031p:plain:w600 f:id:shikaku_sh:20220204105040p:plain:w600

UPDATE

UPDATE 文を実行する例はこんな感じ。

private void UpdateEF()
{
    using (var context = new MySqlContext())
    {
        var user = context.Users.SingleOrDefault(p => p.Id == 1);

        if (user == null) return;

        user.Email = "update@mail.com";
        user.FirstName = "update.name1";
        user.LastName = "update.name2";

        context.SaveChanges();
    }
}

サンプル

GitHub にサンプルを公開しています。

Entity Framework を利用したほうが、ほとんどのケースでは正解だと思います。おおきな理由は実行しなくてもコンパイルエラーのチェックをできる箇所が増えるから。

Command からだと文字列に値をセットするような処理は、実行するまでエラーチェックがあまりできなかったりします。Entity Framework だとクラスと変数に値を直接設定できるので安心。

参考

PlanetScale - SELECT INSERT UPDATE を実行する

f:id:shikaku_sh:20220122172122p:plain

PlanetScale (MySQL 互換) の基本的な DML (select, insert, update) を C# の環境で実行してみた。

接続文字列のパラメーターは、NuGet から「DotNetEnv」を使って隠しています。

DotNetEnv.Env.Load(".env");

var server = DotNetEnv.Env.GetString("SERVER");
var user = DotNetEnv.Env.GetString("USER");
var password = DotNetEnv.Env.GetString("PASSWORD");
var database = DotNetEnv.Env.GetString("DATABASE");
var port = DotNetEnv.Env.GetString("PORT");
var ssl = DotNetEnv.Env.GetString("SSLMODE");

var connectionString = $"server={server};user={user};database={database};port={port};password={password};SslMode={ssl}";

SELECT

SELECT 文を実行する例はこんな感じ。

private void Select(string connectionString)
{
    var data = new List<(int, string, string)>();

    using (var connection = new MySqlConnection(connectionString))
    {
        connection.Open();

        var command = new MySqlCommand("SELECT * FROM users;", connection);
        var result = command.ExecuteReader();

        while (result.Read())
        {
            int id = result.GetInt32("id");
            string email = result.GetString("email");
            string name1 = result.GetString("first_name");
            string name2 = result.GetString("last_name");

            data.Add((id, email, $"{name1} {name2}"));
        }
    }

    foreach (var d in data)
    {
        Console.WriteLine($"id={d.Item1}, email={d.Item2}, name={d.Item3}");
    }
}

特にこれといって特徴のないコードになります。

INSERT

INSERT 文を実行する例はこんな感じ。

private void Insert(string connectionString)
{
    using (var connection = new MySqlConnection(connectionString))
    {
        connection.Open();

        var sql1 = "INSERT INTO users (email, first_name, last_name) VALUES ('aa', 'bb', 'cc');";
        var sql2 = "SELECT LAST_INSERT_ID() FROM users;";

        var command = new MySqlCommand(sql1 + sql2, connection);

        // 挿入したカラムの ID を取得
        long id1 = (long)command.ExecuteScalar();
        long id2 = command.LastInsertedId;

        Console.WriteLine($"inserted id={id1}={id2}");
    }
}

挿入したテーブルに(auto increment をする)id がある場合、行に割り当てられた id を返却するコードにしておくこと。

例だと Insert 文の後すぐに、LAST_INSERT_ID() FROM *** を呼び出して、id を入手しています。Transaction を使ったコードがわかりやすい連携になります。

おおよそのデータ操作は、ひとつのテーブルにデータを挿入するだけではないです。いくつかのテーブルにパラメーターを保存するため、このような操作をするとよいです。

Entity Framework のような DbSet では、LastInsertedId を取得する際にこの種のコードは表現しなくても自動的に id が取得できます。

UPDATE

UPDATE 文を実行する例はこんな感じ。

private void Update(string connectionString)
{
    using (var connection = new MySqlConnection(connectionString))
    {
        connection.Open();

        var command = new MySqlCommand("UPDATE users SET email = 'rance@rudras.wld', first_name = 'あてな', last_name = '2号' WHERE id = 1;", connection);

        command.ExecuteNonQuery();
    }
}

特にこれといって特徴のないコードになります。

Transaction & Insert

Transaction を使ったときのサンプル。

private void InsertUsingTransaction(string connectionString, bool raiseException)
{
    using (var connection = new MySqlConnection(connectionString))
    {
        connection.Open();

        using (var transaction = connection.BeginTransaction())
        {
            try
            {
                var sql1 = "INSERT INTO users (email, first_name, last_name) VALUES ('test', 'using', 'transaction');";
                var sql2 = "SELECT LAST_INSERT_ID() FROM users;";

                var command1 = new MySqlCommand(sql1 + sql2, connection);

                // 挿入したカラムの ID を取得
                long id1 = (long)command1.ExecuteScalar();
                long id2 = command1.LastInsertedId;

                var sql3 = $"INSERT INTO sample (data, data2) VALUES ('{id1}', 'related value');";
                var sql4 = "SELECT LAST_INSERT_ID() FROM users;";

                var command2 = new MySqlCommand(sql3 + sql4, connection);

                // 挿入したカラムの ID を取得
                long id3 = (long)command2.ExecuteScalar();
                long id4 = command2.LastInsertedId;

                // 例外を発生させるサンプル
                if (raiseException)
                {
                    throw new Exception();
                }

                transaction.Commit();
            }
            catch
            {
                transaction.Rollback();
            }
        }
    }
}

おまけ1 日付を取得する

下記のコードは、時間が日本だとズレて取得してしまうようです。(日本時間ではなく UTC+0000)

f:id:shikaku_sh:20220204092700p:plain:w600

private void Now(string connectionString)
{
    using (var connection = new MySqlConnection(connectionString))
    {
        connection.Open();

        var command = new MySqlCommand("SELECT NOW();", connection);
        var result = (DateTime)command.ExecuteScalar();

        Console.WriteLine(result);
    }
}

どうしてそうなるのか。PlanetScale のコンソールに問い合わせてみた結果がこちら:

f:id:shikaku_sh:20220204092718p:plain:w600

JSTasis/tokyo を設定していないようです。かといって、/etc/my.cnf のようなものを直接編集できるわけでもないので、どうしようもなさそうです。

仕方ないので対応の一例がこちら:

CREATE TABLE `user` (
  `id` int NOT NULL AUTO_INCREMENT,
  `created_datetime` datetime DEFAULT ((now() + interval 9 hour)),
  `updated_datetime` datetime DEFAULT ((now() + interval 9 hour)),
  PRIMARY KEY (`id`)
)

now() で取得した時刻のズレに対して、9時間を足して日本時間の UTC+0900 にします。ほかにも、データの記録は UTC+0000 にしておいて、取得のタイミングで +0900 を加えて日本時間にする、といった方法などがあると思います。(アプリケーションの規模や利用範囲で検討する)

おまけ2

CREATE TABLE sample
...
CONSTRAINT `fk_1` FOREIGN KEY (`no`) REFERENCES `main` (`no`) ON DELETE CASCADE

のような外部キーを利用することができないみたいです。

これは結構面倒で、テーブルの整合性には注意がより必要になるはずです。

サンプル

GitHub にサンプルを公開しています。

次回で EF (entity framework) を利用した DML をメモします。

参考

PlanetScale と Navicat を接続する

f:id:shikaku_sh:20220122172122p:plain

PlanetScale (MySQL 互換) と navicat をテスト接続する機会があったので、その内容をメモ。

特に理由がなければ Arctype が推奨みたいです。

Planetscale は MySQL をサポートするプラットフォームであれば、どんなものでも接続することが可能です。Navicat との接続は「General」で対応できます。

「New Password」で必要な情報を取得することができます。

f:id:shikaku_sh:20220126142057p:plain:w600

f:id:shikaku_sh:20220126142123p:plain:w207

  • ホスト名
  • ユーザー名
  • パスワード

パスワードの管理は「Settings > Passwords」で調べることができます。

Branches の development/production

使用しているブランチを Production に一度変更すると、そのブランチはもう開発用ブランチ (development) に戻れないみたいです。

再度開発が必要になったときは、素直に新しいブランチを作って、ブランチを切り替えよう。(新しいブランチを production にするわけではない。新しいブランチで変更修正した結果を元のブランチに対してマージする)

新しいブランチは、過去のブランチのテーブルの情報を継承して持つけれど、テーブル内のデータ(カラム)は空の状態になる。データがほしいときは、マニュアルでエクスポート/インポートする必要がある。(と思う)

Navicat

ホスト名、ユーザー名、パスワードを以下のように設定。

f:id:shikaku_sh:20220126142257p:plain:w562

SSL を設定。

f:id:shikaku_sh:20220126142450p:plain:w562

接続テストで成功。

f:id:shikaku_sh:20220126142546p:plain

参考

PlanetScale のクイックスタートガイド

f:id:shikaku_sh:20220122172122p:plain

この記事は PlanetScale のクイックスタートガイドを個人的に和訳して、サンプルを実行した記録です。

はじめに

PlanetScale は、サーバーレスのデータベースです。公式では、日本語の対応がまだありません。

ざっくりとした話だと MySQL 互換のある Managed RDB サービス、ということになりますが、詳細はドキュメントを参照してみましょう。

PlanetScale クイックスタートガイド

Overview

つぎのガイドでは、PlanetScale を数分で始める方法を紹介します:

  • PlanetScale のデータベースを作成する
  • スキーマの変更
  • データの挿入
  • データベースのブランチを本番環境にプロモート(昇級のような意味に近い「切替」のこと)する

このガイドは、PlanetScale アプリケーション ダッシュボード(ブラウザ)、または、PlanetScale CLI を使用する2つの方法を別々に記述してあります。

PlanetScale アプリケーション ダッシュボード(ブラウザ)による操作

アカウントを作成する

アカウントを作成します。

メールアドレス、または、GitHub アカウントで登録できます。

データベースを作成する

最初のデータベースを作成するためには、次のステップで進めます:

  • 「Create new organization」で新しい組織をつくって、「Create database」ボタンをクリック
  • データベースの名前は、小文字の英数字(-_ も OK)が使用可能
  • リージョンを選択する。アプリケーションをホスティングロケーションと近いリージョンがおすすめ。
  • 最後に「Create database」ボタンをクリック

f:id:shikaku_sh:20220122172228p:plain:w500

f:id:shikaku_sh:20220122172247p:plain

作成したデータベースは、Overview で紹介した「スキーマの変更」と「データの挿入」で使用する、初期の開発ブランチ main に作成してあります。

データベースにスキーマを追加する

作成した「organization」から作成したデータベースを選択し、ナビゲーションから「Branches」を選択します。これでデータベースのブランチのページを表示します。

main ブランチを選択すると、main ブランチのページが表示されます。

今度は「Console」タブを選択して、次のコマンドを実行してテーブルを作成してみます:

CREATE TABLE `users` (
  `id` int NOT NULL AUTO_INCREMENT PRIMARY KEY,
  `email` varchar(255) NOT NULL,
  `first_name` varchar(255),
  `last_name` varchar(255)
);

次のコマンドを実行することで、テーブルの作成コマンドを確認することができます:

SHOW TABLES;

f:id:shikaku_sh:20220122172353p:plain:w500

データを挿入する

users テーブルを作成したので、テーブルにデータを挿入することができます。

次のコマンドを実行して、テーブルにユーザーのデータを追加してみます:

INSERT INTO `users` (id, email, first_name, last_name)
VALUES  (1, 'hp@test.com', 'Harry', 'Potter');

次のコマンドを実行して、ユーザーのデータがテーブルに挿入されたかどうかを確認します:

SELECT * FROM users;

f:id:shikaku_sh:20220122172414p:plain:w500

次のコマンドを実行して、スキーマを確認することができます:

DESCRIBE users;

f:id:shikaku_sh:20220122172430p:plain:w500

「Schema」タブを選択して、「Refresh schema」ボタンを選択すると、さきほど作成した users テーブルが表示されます。

データベースのブランチを本番環境にプロモート

PlanetScale にデータベースを作成したとき、main という名前の(開発用)ブランチが自動的に作成されました。

開発用のブランチ (development branch) は、スキーマの変更を目的としているので、本番環境で使用することを意図していません。

本番用のブランチ (production branch) とは、本番環境で使用するために設計された自動スケジュールバックアップを備え、高可用性があり、保護されたデータベースのブランチです。

CREATE, ALTER, DELETE などのスキーマ変更は、不慮のデータ損失を防ぐため、本番用のブランチでは許可されていません。

開発用ブランチで作ったデータベースの内容に満足できたら、本番用ブランチにプロモートさせましょう。

ブランチを本番用にプロモートさせるには:

  • 「Overview」をクリックして、「Promote to production」に関する情報が書かれたバナーを表示
  • 「Promote branch」のボタンを選択
  • 開かれる画面で本番用にプロモートしたいブランチを選択(つまり main
  • 再び「Promote branch」のボタンを選択すれば、開発用ブランチから本番用ブランチへのプロモートは完了

f:id:shikaku_sh:20220122172514p:plain:w500 f:id:shikaku_sh:20220122172533p:plain

これで開発用ブランチの本番環境へのプロモートは完了です。これによって、次のメリットがあります:

  • スキーマの直接変更を保護できる
  • 高可用性を得る
  • 毎日の自動バックアップをスケジュール

f:id:shikaku_sh:20220122172548p:plain:w500

次にやることは?

データベースを作成し、スキーマの変更を適用し、データを挿入し、ブランチを本番環境に移行できたら、次はアプリケーションに接続しましょう。

一般的な step-by-step なアプローチだと「Connect Any Application チュートリアル」を参照します。接続文字列の作成については、「Connection Strings」を参照します。

PlanetScale CLI による操作

まず、PlanetScale CLIダウンロード&インストールします。

Windows 版 scoop で設定するなら PowerShell を使って次のコマンドです:

scoop bucket add pscale https://github.com/planetscale/scoop-bucket.git
scoop install pscale mysql

アップデートしたいときは:

scoop update pscale

scoop ダウンロード方法の補足 (Windows)

[scoop] は Windows 版の Homebrew みたいなもので、アプリのパッケージ管理です。

scoop 自体のインストールには、PowerShell を起動して、次のコマンドを実行します:

Invoke-Expression (New-Object System.Net.WebClient).DownloadString('https://get.scoop.sh')
# or shorter
iwr -useb get.scoop.sh | iex

PowerShell requires an execution policy in [Unrestricted, RemoteSigned, ByPass] to run Scoop.

のようなメッセージが出る場合は、下記コマンドで権限を設定します。

Set-ExecutionPolicy RemoteSigned -scope CurrentUser

f:id:shikaku_sh:20220122172627p:plain:w500

アカウントを作成する

PlanetScale のアカウントを作成します。すでにアカウントがある場合はログインするか、CLI からアカウントを直接作成することができます。

アカウントを新規登録する場合は、ターミナルで次のコマンドを実行します:

pscale signup

メールアドレス、パスワードの入力します。登録を完了するためには、E-mail のアカウント認証を忘れないこと。

アカウントにサインインする

CLIでアカウントにログイン(サインイン)するときは、次のコマンドです:

pscale auth login

ブラウザに PlanetScale の認証画面に ID が表示されます。PowerShell に表示される ID と一致することを確認してから「Confirm code」を選択します。

ログインに成功したら、ブラウザの認証画面を閉じます。以上で、ターミナル (PowerShell) で操作を進めることができるようになりました。

データベースを作成する

PlanetScale にログインできたら、データベースを作成することができます。

次のコマンドを実行して、データベースを作成します。

pscale database create databasename --region regionslug
# sample
pscale database create users2 --region ap-northeast
  • databasename - データベースの名前
  • regionslug - リージョンの名前(調べる

リージョンを指定しなかった場合は、US East - Northern Virginia が選択されます。

データベースはやはり main ブランチに作成されます。(Organization はデフォルトです)

f:id:shikaku_sh:20220122172655p:plain:w500

データベースにスキーマを追加する

データベースにスキーマを追加するためには、pscale シェルを使用して、MySQL に接続します。次のコマンドを実行します:

pscale shell databasename main

これで main ブランチに接続して、MySQL クエリーを実行できます。次のコマンドを実行して、テーブルを作成します:

CREATE TABLE `users` (
  `id` int NOT NULL AUTO_INCREMENT PRIMARY KEY,
  `email` varchar(255) NOT NULL,
  `first_name` varchar(255),
  `last_name` varchar(255)
);

次のコマンドを実行することで、テーブルの作成コマンドを確認することができます:

SHOW TABLES;

データを挿入する

次のコマンドを実行して、テーブルにユーザーのデータを追加してください:

INSERT INTO `users` (id, email, first_name, last_name)
VALUES  (1, 'hp@test.com', 'Harry', 'Potter');

次のコマンドを実行して、ユーザーのデータがテーブルに追加されたことを確認します:

SELECT * FROM users;

次のコマンドを実行して、スキーマを確認することができます:

DESCRIBE users;

CLI で実行した内容は、PlanetScale アプリケーション ダッシュボード(ブラウザ)からも確認することができます。

次にやることは?

データベースを作成し、スキーマの変更を適用し、データを挿入し、ブランチを本番環境に移行できたら、次はアプリケーションに接続しましょう。

一般的な step-by-step なアプローチだと「Connect Any Application チュートリアル」を参照します。接続文字列の作成については、「Connection Strings」を参照します。

参考

PlanetScale

C# PostcodeJP の API を利用して郵便番号から住所を取得する

f:id:shikaku_sh:20210823173353p:plain:w300

郵便番号から住所を取得するやり方のひとつに PostcodeJP がある。

郵便番号から住所を取得する API は、いくつか無料のものが公開されています。PostcodeJP も無料で利用できますが、ユーザー登録および API キーを利用することで、1日384回まで郵便番号の情報を取得できます。

郵便番号

郵便番号から住所などの情報を取得するサンプルはこんな感じ。

public class PostcodeClient
{
    private const string _BaseUrl = "https://apis.postcode-jp.com/";

    public static async Task<ApiResult> GetPostcodeAsync(string key, int postcode)
    {
        string postcodeUri = "api/v5/postcodes/";
        string url = _BaseUrl + postcodeUri + postcode.ToString("D7"); // 7桁
        string response = await GetHttpResponse(key, url);

        ApiResult[]? result = null;

        if (response != null)
        {
            var options = new JsonSerializerOptions();

            result = JsonSerializer.Deserialize<ApiResult[]>(response, options);
        }

        return (result?.Count() ?? 0) == 1 ? result?[0] ?? new () : new ();
    }

    private static async Task<string> GetHttpResponse(string key, string url)
    {
        using (var client = new HttpClient())
        {
            // client.DefaultRequestHeaders.Add("-G", $"-v \\");
            client.DefaultRequestHeaders.Add("-d", $"apiKey={key}");
            client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

            var response1 = await client.GetAsync(url);
            var response2 = await response1.Content.ReadAsStringAsync();

            return response2;
        }
    }
}

以下のような API からは以下の JSON データが返却されるので、丁度良いクラスで受け取るとよいです。

https://apis.postcode-jp.com/api/v5/postcodes/xxxxxxx?apiKey=zzzzzzzz 形式でも動作しますが、HTTP ヘッダーに API キーを設定すると、SSL/TLS により API キーが保護されるはずです。

[
  {
    "prefCode": "13",
    "cityCode": "402",
    "postcode": "1001701",
    "oldPostcode": "10017",
    "pref": "東京都",
    "city": "青ヶ島村",
    "town": "",
    "allAddress": "東京都青ヶ島村",
    "hiragana": {
      "pref": "とうきょうと",
      "city": "あおがしまむら",
      "town": "",
      "allAddress": "とうきょうとあおがしまむら"
    },
    "halfWidthKana": {
      "pref": "トウキョウト",
      "city": "アオガシマムラ",
      "town": "",
      "allAddress": "トウキョウトアオガシマムラ"
    },
    "fullWidthKana": {
      "pref": "トウキョウト",
      "city": "アオガシマムラ",
      "town": "",
      "allAddress": "トウキョウトアオガシマムラ"
    },
    "generalPostcode": true,
    "officePostcode": false,
    "location": {
      "latitude": 32.45510482788086,
      "longitude": 139.76348876953125
    }
  }
]

最初は [] で始まっているため、コレクション(配列)の返却になっています。

JSON 対応クラスの例

public class ApiResult
{
    [JsonPropertyName("prefCode")]
    public string PrefectureCode { get; set; } = "";

    [JsonPropertyName("cityCode")]
    public string CityCode { get; set; } = "";

    [JsonPropertyName("postcode")]
    public string PostCode { get; set; } = "";

    [JsonPropertyName("oldPostcode")]
    public string OldPostCode { get; set; } = "";

    [JsonPropertyName("pref")]
    public string Prefecture { get; set; } = "";

    [JsonPropertyName("city")]
    public string City { get; set; } = "";

    [JsonPropertyName("town")]
    public string Town { get; set; } = "";

    [JsonPropertyName("allAddress")]
    public string Address { get; set; } = "";

    [JsonPropertyName("hiragana")]
    public Ruby Hiragana { get; set; } = new ();

    [JsonPropertyName("halfWidthKana")]
    public Ruby KanaHalf { get; set; } = new ();

    [JsonPropertyName("fullWidthKana")]
    public Ruby Katakana { get; set; } = new ();

    [JsonPropertyName("generalPostcode")]
    public bool IsGeneral { get; set; }

    [JsonPropertyName("officePostcode")]
    public bool IsOffice { get; set; }

    [JsonPropertyName("location")]
    public Location Location { get; set; } = new ();
}
public class Location
{
    [JsonPropertyName("latitude")]
    public double? Latitude { get; set; }
    [JsonPropertyName("longitude")]
    public double? Longitude { get; set; }
}

public class Ruby
{
    [JsonPropertyName("pref")]
    public string Prefecture { get; set; } = "";

    [JsonPropertyName("city")]
    public string City { get; set; } = "";

    [JsonPropertyName("town")]
    public string Town { get; set; } = "";

    [JsonPropertyName("allAddress")]
    public string Address { get; set; } = "";
}

テスト

PostcodeJP API を利用するためには、ユーザーごとの API キーが必要です。 API キーの管理については、過去に記述したとおり、アプリケーションでの利用に気をつけてください。

安易に API キーをコード中に含めないことを強く推奨します。(テストコードでも)

public class Tests
{
    private string ApiKey { get; set; } = "";

    [OneTimeSetUp]
    public void OneTimeSetUp()
    {
        // var api = Environment.GetEnvironmentVariable("RESAS_API", EnvironmentVariableTarget.User);
        DotNetEnv.Env.Load(".env");
        var key = DotNetEnv.Env.GetString("POSTCODE_API");

        ApiKey = key;
    }

    [TestCase(1001701, "東京都青ヶ島村")]
    [TestCase(0121100, "秋田県雄勝郡羽後町")]
    public async Task 郵便番号_To_住所(int postcode, string address)
    {
        var result = await PostcodeClient.GetResourceAsync(ApiKey, postcode);

        Assert.AreEqual(result.Address, address);
    }
}

例外のテストも追加したほうがよい。

サンプル

GitHub にサンプルを公開しています。

参考

C# RESAS API を利用する

f:id:shikaku_sh:20210823173353p:plain:w300

都道府県コードや産業分類など、地域の情報(地域経済分析システム)を取得するやり方のひとつに RESAS がある。

RESAS は、2015 年から内閣府の地方創生推進室が開始したサービスで、2016 年から API の提供を開始しており、利用目的などのユーザー登録をすれば誰でも無料で利用できます。

中には都道府県コードなどの単純なデータもあるので、とりあえず使ってみるサンプルです。

都道府県コード

都道府県コードを取得するサンプルはこんな感じ。

public class ResasClient
{
    private string _BaseUrl = "https://opendata.resas-portal.go.jp/";

    public async Task<IEnumerable<Prefecture>> GetPrefecturesAsync(string key)
    {
        string prefecturesUri = "api/v1/prefectures";
        string url = _BaseUrl + prefecturesUri;
        string response = await GetHttpResponse(key, url);

        ApiResult<Prefecture>? result = null;

        if (response != null)
        {
            var options = new JsonSerializerOptions();

            result = JsonSerializer.Deserialize<ApiResult<Prefecture>>(response, options);
        }

        return result?.Result ?? new List<Prefecture>();
    }

    private async Task<string> GetHttpResponse(string key, string url)
    {
        using (var client = new HttpClient())
        {
            client.DefaultRequestHeaders.Add("X-API-KEY", key);
            client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

            var response1 = await client.GetAsync(url);
            var response2 = await response1.Content.ReadAsStringAsync();

            return response2;
        }
    }
}

以下のような API からは以下の JSON データが返却されるので、丁度良いクラスで受け取るとよいです。

{
    "message":null,
    "result":[{
        "prefCode":1,"prefName":"北海道"},
        ...
        {"prefCode":46,"prefName":"鹿児島県"},
        {"prefCode":47,"prefName":"沖縄県"}
    ]
}

JSON 対応クラスの例

public class ApiResult<T>
{
    [JsonPropertyName("message")]
    public string Message { get; set; } = "";

    [JsonPropertyName("result")]
    public IEnumerable<T>? Result { get; set; }
}

public class Prefecture
{
    /// <summary>
    /// 都道府県コードを取得または設定します。
    /// </summary>
    [JsonPropertyName("prefCode")]
    public int Code { get; set; } = 0;
    /// <summary>
    /// 都道府県名を取得または設定します。
    /// </summary>
    [JsonPropertyName("prefName")]
    public string Name { get; set; } = "";
}

テスト

RESAS API を利用するためにはユーザーごとの API キーが必要です。 API キーの管理については、過去に記述したとおり、アプリケーションでの利用に気をつけてください。

安易に API キーをコード中に含めないことを強く推奨します。(テストコードでも)

public class Tests
{
    private string ApiKey { get; set; } = "";

    [OneTimeSetUp]
    public void OneTimeSetUp()
    {
        // var api = Environment.GetEnvironmentVariable("RESAS_API", EnvironmentVariableTarget.User);
        DotNetEnv.Env.Load(".env");
        var key = DotNetEnv.Env.GetString("RESAS_API");

        ApiKey = key;
    }

    [Test]
    public async Task RESAS_都道府県コード()
    {
        var client = new ResasClient();
        var prefectures1 = await client.GetPrefecturesAsync(ApiKey);

        Assert.AreEqual(prefectures1.Count(), 47);
    }
}

サンプル

GitHub にサンプルを公開しています。サンプルでは、産業大分類を追加しています。

単純にテキストファイルから読み込む例も追加。(ネットから正しくデータを取得できなかった場合などに利用するとよいかも)

参考

C# Null 非参照型と許容参照型 (CS8600)

f:id:shikaku_sh:20210823173353p:plain:w300

C# 8.0 から「C の null 値を許容する参照型」という機能が実装されています。

もともと、C# の参照型は null 値を許容しているので、改めて実装される意味がわかりづらいかもしれないです。

現在、null 参照をすることは避けられるようになっていて、C# でも、C# 8.0 から参照型であってもデフォルトでは null を参照しないようになりました。そのため、Visual Studio 2022 なんかでプログラムを書くと、null を参照する(許容する)クラスなんかで、そこそこ CS8600 の警告と出くわすことになります。

f:id:shikaku_sh:20220108153242p:plain

実際は、もともと null が許容されていた言語なので、そこから null を完全に排除することは難しいです。なんで、「型によって null の許容/拒否」がわかるように表現されるようになった、というところを押さえておく必要があります。

null を許容する例

テキストを System.Text.JsonSerializer で変換する場合、戻り値は null を許容する型になっています。なので、以下のようになります:

public static class JsonSerializer
{
    ...
    public static TValue? Deserialize<TValue>(string json, JsonSerializerOptions? options = null);
    ...
}
// CS8600 の警告が出る
SampleData data = JsonSerializer.Deserialize<SampleData>(text, options);

// OK
SampleData? data = JsonSerializer.Deserialize<SampleData>(text, options);

従来は SampleData はクラスなので参照型。なので SampleData 型は null を扱えるため問題なかったのですが、今は見ただけでこの型は null を許容しているのかどうかわかるように書くことになりました。よって、null 値を許容する場合は値型と同じで ? を付ける必要があります。

これにメリットがあるのかどうか、わかりづらいと思いましたが、あるメソッドから何らかの参照型(クラス)IEnumerable<T> を返却する場合、取得に失敗したときに返却するデータは、空の IEnumerable<T> 、または null を返却するのか、XML ドキュメント コメントや仕様書に記載が無い場合、従来だと動作がわからないことがありました。(例外を投げる場合も同様)

正常な動作は書いてあるのだけど、上手くいかない場合の動作がわからず、data.Count() > 0 のようなことをして null 参照エラーになる、または、(data?.Count() ?? 0) > 0 のようなことになります。

それが、C#8.0 からは null を許容する参照型は ? を付けることになったため、デフォルトは null を許容しない参照型になりました。コードを見れば読み取れることが増えてますね。こんな XML ドキュメント コメントは不要になりそう。

/// <summary>
/// とある要素の <see cref="SampleData"/> コレクションを取得します。
/// </summary>
/// <returns><see cref="SampleData"/> のコレクション。取得に失敗した場合 <value>null</value> を返却します。</returns>
public IEnumerable<SampleData> GetSample() => ...;

C# 8.0 からは、SampleData は null 非許容型なので、取得に失敗したときは空の配列を返却するか、例外を投げるだろうと予測できそうです。

補足

null 値を許容する(null-forgiving)

一時的に null を許容する場合は null-forgiving operator を使って警告を回避することもできます。(他言語は not-null assertion, force unwrap など)

! 演算子 (null-forgiving) を利用すると、CS8600 のチェックから外れるので、NullReferenceException がどこで発生してもおかしくない状態になります。

エラーが発生するのは従来どおり、その値を参照したタイミングなので、コードに問題がある箇所よりも後ろになるはずで、変数が渡っていってしまうと、まったく別のところでエラーが発生するのも従来どおり。

SampleData data = JsonSerializer.Deserialize<SampleData>(text, options)!;

アノテーション属性

属性でもいろいろな準備があって、部分的に null を許容したりするような動きを作れる。

public struct Int32 
{
    ...
    public static bool TryParse([NotNullWhen(true)] string? s, out int result);
    ...
}

int.TryParse 戻り値の戻り値が true は、翻って s は not null という制約。 string.IsNullOrEmpty[NotNullWhen(false)]コンパイラの最適化に役立ちます。

値/参照型で null 参照型の実装は全然違う

? の実装は、値型は Nullable<T> でしたが、参照型は元々から null を代入できるので整合性の面で微妙なところがあります。

そのあたりはオーバーロードtypeof() で悩ましい部分が出ていますが、そうそう入り込んだコードでもなければ遭遇しない自体だと思いますが、実装は異なっている、という点は重要です。

ジェネリクス

T を非 null 参照型だと明示できるようになっています。

class ResasClient<T> where T: notnull
{
    ...
}

まとめ

  • null を参照できるコードは損失をもたらしてきた歴史がある
  • C# も null 参照問題の対策として「型で null 参照/非参照」がわかるようにした
  • 基本的には、null 参照問題を避けたいので null を許容しない
    • でも、完全に無くすコストは大きいので、部分的に null を許容する

参考