vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

コンパイルを自動化したい話 + (Pikaday + TypeScript) で IE でも DatePicker を表示したい話

はじめに

ここまで tsc による TypeScript -> JavaScript 変換、 webpack によるバンドルや PostCSS -> CSS 変換など、ツールによるコンパイルをいくつか試してきたわけですが、正直これを手動でやり続けるのは大変です。

ということで、これを自動で実行してくれるようにしてほしかった話と、前回の DatePicker の話の続きです。

npm scripts で自動的にコンパイル

npm scripts を使って面倒なコンパイル処理をもう少し簡素化できるようです。

npm scripts 以外にも自動で処理を実行してくれるツールは色々あるようですが、全く別のツールを導入しなくて良い、ということと、今回やりたいことは十分実現できそう、ということで今回はこれでいくことにしました。

とりあえず npm scripts でコンパイルを実行

まずはこれまで実行してきた下記二つのコマンドを npm scripts で実行してみます。

  • npx webpack
  • npx postcss pcss/*.css -c postcss.config.js -d src

これらを package.json に scripts として追加します。

package.json

{
    
    "scripts": {
        "css": "npx postcss pcss/*.css -c postcss.config.js -d src",
        "webpack": "npx webpack"
    },
    
    "devDependencies": {
        ~省略~
    }
}

で、あとはターミナルから下記のように実行すれば OK です。

npm run css

webpack の方は

npm run webpack

まとめて実行

今は二つですが、今後増えてくるとやっぱり面倒なので、一括で実行したいところ。

npm-run-all を使ってみます。

npm install --save-dev npm-run-all 

package.json

{
    "scripts": {
        
        "all": "run-s css webpack",
        
        "css": "npx postcss pcss/*.css -c postcss.config.js -d src",
        "webpack": "npx webpack"
    },
    "devDependencies": {
        ~省略~
    }
}

これで npm run all とすれば両方実行してくれます。

自動で実行してほしい

2 回実行するものが 1 つにまとまって、少しは楽になったもののやっぱり変更のたびに都度実行するのはちょっと。。。

Rider で tscコンパイルを、ファイルの変更を感知して自動で実行してくれたように、 webpack や PostCSS も自動実行してほしいですね。

通常 npm scripts で実現するためには、 watch を使うことができます。

npm install --save-dev watch

ですが、 webpack 、 PostCSS の場合、 watch を使わなくてもオプションとして watch の機能を持っています。

ということで、 package.json を下記のように変更するだけで、ファイルの変更の監視、コンパイルの自動実行をしてくれるのでした。

package.json

{
    "scripts": {
        "all": "run-s css webpack",
        "css": "npx postcss pcss/*.css -c postcss.config.js -d src -w",
        "webpack": "npx webpack -w"
    },
    "devDependencies": {
        ~省略~
    }
}

別でターミナルを立ち上げて実行

今回の開発は Visual Studio Code (以下 VS Code )を使っているのですが、 VS Code ではメニューからターミナル( PowerShell )を立ち上げることができます。

コンパイルや npm install をする場合はこれを使うのが便利なのですが、上記の処理については別で PowerShell などを立ち上げて実行するのが良いかと思います。

理由は明示的に処理を終了するまでずーっと監視し続けるため、他のコマンドが打てなくなるためです。

IE11 での input type='date'

前回の話で、 DatePicker を表示するのは input の type を date にするだけ。

えらい簡単だな~凄いな~と思っていたわけですよ。

IE で確認するまでは。

まぁ当然のように動かない、と/(^o^)\

自分で作るのは厳しそうなので、良さげなものないかな~と思っていたところ、 Pikaday を見つけました。

シンプルに使えそうとか良いところは色々あるわけですが、とりわけ IE の対応が ver.7 からというのがありがたい (..)_ (今回は IE7 には対応していませんが)

ということで試してみます。

準備

とにかくインストールから。

色々方法はあるようですが、 npm install することにしました。

npm install --save-dev pikaday

CSS の読み込み

Pikaday を使うにあたり、必要なファイルは 2 つです。

  • Pikaday.js
  • Pikaday.css

JavaScript の方は import なりなんなりすれば利用できるので問題ありません。

ただ、 CSS をどうするか。

ファイル自体は node_modules > pikaday > css に置かれています。

HTML から node_modules 以下のパスを指定して読み込む、または手動あるいは npm scripts でコピーという手もなくはないですが、自分で設定する CSS ファイル程更新は頻繁ではないでしょうし、かといって放置というわけにもいかずいまいち。

などと思っていたら、 postcss-import を使う方法が見つかりました。

これを使うことで、 JavaScript の import / export で分割したファイルを webpack でひとまとめにするのと同じようなことができます。

postcss-import を使う

上記リンク参照というところではありますが、一応ここにも書き残しておきます。

まずはインストール。

npm install --save-dev postcss-import

PostCSS から使用するので、 postcss.config.js に追加します。

postcss.config.js

module.exports = {
    plugins: [
        
      require('postcss-import')(),
      
      require('autoprefixer')({
        "grid": true,
        "browsers": [
          "last 2 versions"
        ]
      }),
      require('precss'),
    ]
  }

で、表示するページで使用する CSS に import を追加してやります。

page.css

@import '../node_modules/pikaday/css/pikaday.css';

~省略~

Pikaday の README と違ってパスが ../ になっているのは、 page.css が pcss というディレクトリ内にあるからです(想定されている CSS ファイルとパスが異なる)。

これでコンパイルすると、 page.css の内容に pikaday.css が追加されたものが出力されます。

HTML から node_modules 以下のパスを指定する場合、 ASP.NET Core (というか Web アプリ側)の静的ファイルの公開範囲をいじる必要が出てきますが、 PostCSS のコンパイル時だけ参照するのであれば問題はぐっと減ってくれそうです。

なお、何らかの理由で pikaday.css の内容と他のファイルを分けたい場合は、 @import ~ だけ書いた

Pikaday を使う

Pikaday を使う場合、対象の要素を指定する、日付のフォーマットを指定する、といった初期化処理を行う必要があります。

< script src="pikaday.js">< /script>
< script>
    var picker = new Pikaday({ 
        field: document.getElementById('datepicker') 
    });
< /script>

注意点としては document.getElementById('datepicker') が実行されるより前に該当の DOM 要素がロードされている必要があるため、この処理は HTML の下部に書く必要がある、ということです。

フォーマットを変更する

デフォルトだと下記のデモのように曜日や月が英語で表示されます。

例えばこれを日本語に変更したい場合、初期化処理時に指定することができます( 項目は pikaday.js の defaults で初期値が設定されています)。

で、これらを DatePicker 使うたびに設定するのはツラすぎるので、クラスにまとめてみました。

import Pikaday from 'pikaday';

class CustomDatePicker{
    public static getPikaday(elementName: string): Pikaday{
        const today = new Date();
        const yearFrom = 1999;
        const yearTo = today.getFullYear() + 1;

        return new Pikaday({
            field: document.getElementById(elementName),
            firstDay: 1, // 月曜始まりにする
            minDate: new Date(yearFrom.toString() + '-1-1'), // 選択できる最小範囲
            maxDate: new Date(yearTo + '-12-31'),
            yearRange: [yearFrom, yearTo], // 選択できる年の範囲。 1999 - 一年後までとする
            yearSuffix: '年', // 2019年 と表示されるようにする
            showMonthAfterYear: true, // 2019年 四月 と表示されるようにする
            i18n: {
                previousMonth : '前月',
                nextMonth     : '次月',
                months        : ['1月','2月','3月','4月','5月','6月','7月','8月','9月','10月','11月','12月'],
                weekdays      : ['日曜','月曜','火曜','水曜','木曜','金曜','土曜'],
                weekdaysShort : ['日','月','火','水','木','金','土'],
            }
        })
    }
}
export {CustomDatePicker}

Pikaday を import する

@types/pikaday のインストールが必要そうです。

npm install --save-dev @types/pikaday

field で渡せる要素

getElementById となっている通り、一つの Pikaday インスタンスで設定できる要素は一つだけのようです。

ということで、 ID は外から渡せるようにしています。

複数ファイルをバンドルして出力する

例えば今回の CustomDatePicker を ページのスクリプトとは別にバンドルしたい場合。

webpack.config.js の entry で複数パスを渡してやることと、 output で名前が重複しないよう名前を指定する必要があります。

webpack.config.js

var path = require('path');

module.exports = {
    mode: 'development',

    entry: {
        'main':'./ts/mainPage.ts',
        'datePicker':'./ts/customDatePicker.ts',
    },
    
    module: {
        rules: [
            {
                test: /\.tsx?$/,
                use: 'ts-loader',
                exclude: /node_modules/
            }
        ]
    },
    resolve: {
        extensions: [ '.tsx', '.ts', '.js' ]
    },
    output: {
        filename: '[name].bundle.js',
        path: path.resolve(__dirname, 'src/js'),
        library: 'Page',
        libraryTarget: 'umd'
    }
};

課題としては、(今回使っていませんが) SourceMap を生成すると一つ分しか生成されない、ということと、 npm run all で TypeScript を変更した場合に処理がうまく動いていない気がする、という二点は把握しています。

この辺りは解決したら追記、または別記事で投稿します。

flexbox でレイアウトを組みたい話

はじめに

とりあえずベンダープレフィックスの件は解決できました(多分)。

ということで、 flexbox を使ってレイアウトを組んでみたいと思います。

まずはレスポンシブデザインにはしないことにして、かつベンダープレフィックス無しでの指定に対応している・個人的に一番よく使っている Firefox をベースに作ってみたいと思います。

Autoprefixer や IE は?というところですが、まずはレイアウトを組んで、そのあと対応としたいと思います。
(その方法が良いのかどうかはわかりませんが)

外枠を作る

まず各エリアの外枠を作ってみることにします。

mainPage.html

< !DOCTYPE html>
< html lang="jp">
    < head>
        < meta charset="utf-8">
        < title>main
        < link rel="stylesheet" type="text/css" href="../src/page.css" >
    < /head>
    < body>
        < header id="global_header">
        < /header>
        < section id='page_title'>page title< /section>
        < div id='container'>
            < section id='search_area'>
            < /section>
            < section id='item_view'>
                < div id='search_result'>
                < /div>
                < div id='image_view'>
                < /div>
            < /section>  
        < /div>
    < /body>
< /html>

page.css

html{
  height: 100%;
  width: 100%;
  margin: 0px;
}
body{
  height: 100%;
  width: 100%;
  margin: 0px;
  min-width: 900px;
  min-height: 600px;
}
header{
  width: 100%;
  height: 5%;
  background-color: cornflowerblue;
}
#page_title{
  width: 100%;
  height: 5%;
  background-color: aqua;
}
#container {
  display: flex;
  flex-direction: row;
  width: 100%;
  height: 90%;
  background-color: gray;
}
#search_area{
  background-color: pink;
  flex: 1;
}
#item_view{
  flex: 3;
}
#search_result{
  height: 50%;
  background-color: darkgoldenrod;
}
#image_view{
  height: 50%;
  background-color: greenyellow;
}

表示結果はこちら。

f:id:mslGt:20190419230542j:plain

何となくで書いた割には良い感じですね( ´∀`)bグッ!

中のアイテムを追加していく

では中身を追加していきます。

まずは header から。

タイトルとボタンを追加してみます。

mainPage.html

~省略~
< header id="global_header">
    < h1>Hello world!< /h1>
    < button id='header_share'>Share< /button>
< /header>
~省略~

結果はこちら。

f:id:mslGt:20190419230638j:plain

。。。謎のスペースが生まれています。

この理由は先ほど追加した h1 。

margin-top がデフォルトで追加されているらしく、その分が白いスペースとして表示されていたようです。

ということで、 h1 の margin-top を 0 にします。

ついでに margin-left 、 テキストカラーも変えておきます。

page.css

~省略~
header h1{
  color: whitesmoke;
  margin-top: 0;
  margin-left: 10px;
}
~省略~

両端に並べたい

今のままだと文字とボタンが左寄せで表示されているので、これを両端に並べてみます。

page.css

~省略~
header{
  width: 100%;
  height: 5%;
  background-color: cornflowerblue;
  display: flex;
  flex-direction: row;
  justify-content: space-between;
}
~省略~

mainPage.html

~省略~
< header id="global_header">
    < h1>Hello world!< /h1>
    < nav id='header_share_area'>
        < button class='header_share'>Share1< /button>
        < button class='header_share'>Share2< /button>
    < /nav>
< /header>
~省略~

justify-content: space-between を使うことで、子どもの要素を両端 → 中間の順で並べてくれます。

f:id:mslGt:20190419230751j:plain

左側のレイアウト

左の領域は検索条件を指定する領域にしたいと思います。

ということで、思い付きで要素を並べます。

mainPage.html

~省略~
< section id='search_area'>
    < div id='search_frame'>
        < div id='search_title'>検索< /div>
        < div id='search_query'>
            < label id='search_title_query'>キーワードで検索< /label>
            < input type='text' id='search_query_text'>< /input>
        < /div>
        < div id='search_check'>                        
            < label>チェック< /label>
            < ul>
                < li>
                    < input type='checkbox' value='search_availale'>available< /input>
                < /li>
                < li>
                    < input type='checkbox' value='search_availale_2'>available2< /input>
                < /li>
            < /ul>
        < /div>
        < div id='release_data'>
            < label>発売日< /label>
            < input type='date' value='search_relase_date_from'>< /input>
            < label>~< /label>
            < input type='date' value='search_relase_date_to'>< /input>
        < /div>
        < div id='search_submit'>
            < button id='search_button'>検索< /button>
        < /div>
    < /div>
< /section>
~省略~

そのまま表示するとこんな感じ。

f:id:mslGt:20190419230913j:plain

これを縦に並べていきたいと思います。

テキストボックス

とりあえず簡単そうかつ上の要素であるテキストやテキストボックスから。

page.css

~省略~
#search_frame{
  width: 90%;
  height: 90%;
  margin: 5%;
  background-color: white;
}
#search_title{
  text-align: center;
  color: aliceblue;
  background-color: green;
  height: 4%;
  margin-top: 2%;
}
#search_query{
  height: 10%;
  padding-top: 5%;
  width: 80%;
  margin-left: 10%;
}
#search_query input{
  width: 100%;
}
~省略~

input (テキストボックス) の幅を 100% にしているのは、他の要素の幅と合わせたかったためです。

チェックボックス

次はチェックボックス

横に並んでいるため、まず縦並びにする必要があります。

最初は下記のように flexbox を使って並べようと考えていました。

mainPage.html

~省略~
< div id='search_check'>                        
    < label>チェック< /label>
    < input type='checkbox' value='search_availale'>available< /input>
    < input type='checkbox' value='search_availale_2'>available2< /input>
< /div>
~省略~

page.css

~省略~
#search_check{
  height: 14%;
  width: 80%;
  margin-left: 10%;

  display: flex;
  flex-direction: column;
}
~省略~

結果はこの通り。

f:id:mslGt:20190419231003j:plain

チェックボックスの □ とラベル部分が別々に並べられてしまいました/(^o^)\

flexbox を使って正しく並べる方法もあるだろうとは思うのですが、今回はリストを使って並べてみることにしました。

mainPage.html

~省略~
< div id='search_check'>                        
    < label>チェック< /label>
    < ul>
        < li>
            < input type='checkbox' value='search_availale'>available< /input>
        < /li>
        < li>
            < input type='checkbox' value='search_availale_2'>available2< /input>
        < /li>
    < /ul>
< /div>
~省略~

page.css

~省略~
#search_check{
  height: 14%;
  width: 80%;
  margin-left: 10%;
}
#search_check ul{
  list-style: none;
  padding-top: 0;
  margin-top: 0;
  padding-left: 0;
}
#search_check li{
  margin-top: 1%;
}
~省略~

ul において、 list-style を none にすることで・をなくし、デフォルトでは左のラインより右に表示されてしまうので、 padding-left を 0 にしています。

残り(テキストボックス、ボタン)

残りの日付入力用のテキストボックスと、検索ボタンも配置します。

mainPage.html

~省略~
< div id='release_data'>
    < label>発売日< /label>
    < input type='date' value='search_relase_date_from'>< /input>
    < label>~< /label>
    < input type='date' value='search_relase_date_to'>< /input>
< /div>
< div id='search_submit'>
    < button id='search_button'>検索< /button>
< /div>
~省略~

page.css

~省略~
#release_data{
  display: flex;
  flex-direction: column;
  height: 18%;
  width: 80%;
  margin-left: 10%;
}
#release_data input{
  width: 100%;
}
#search_submit{
  width: 80%;
  margin-left: auto;
  margin-right: 10%;
  text-align: right;
}
#search_submit button{
  width: 80px;
}
~省略~

ボタンがなかなか右寄せできずにあれこれ試していたのですが、 text-align で一発という。。。

ラベルはともかく、 text-align が何に有効なのかを把握しておかないと地味にハマりそうです。

結果

結果としてこうなりました。

f:id:mslGt:20190419231105j:plain

もう少しデザイン的にこだわった方がとかある気はしますが、いったんこれで止めて次に進めることにします。
(白い枠の部分はもう少し縦を短くしても良さそうですが)

ほとんど触れませんでしたが、 input の type を date にするだけで、 DatePicker まで表示してくれるのはすごいですね!

f:id:mslGt:20190419231147j:plain

ただ、これブラウザごとに違いがあったりするようです
(下記は 2017 年の情報であり、 Firefox を含めた今のモダンブラウザは違う状況だとは思いますが)。

。。。ん? ブラウザごとの違い。。。?

PostCSS 、 Autoprefixer の力を借りたかった話

はじめに

相変わらず HTML や TypeScript と戯れているわけなんですが、動きだけでなく見た目の部分も重要です。

で、まず全体の配置を指定したいと思ったのですが、これには CSS Grid を使うのが良さそうな気がしました。

で、調べてみると IE11 でも使うことができるらしい、と。

サンプルをコピペで動かしてみると、確かに IE11 でも正しく表示されている。。。! いいですね(゚∀゚)!!

(割と新しめの技術 + IEググるとだいたいどれも情報がいっぱい出てくるあたり、皆様の苦労が感じられます)

一点、サンプルの中に登場する Autoprefixer というものが気になりました。

今回必要となる -ms~ といったベンダープレフィックスを自動でつけてくれるとのこと(また不要なものを外してもくれるらしいです)。

じゃあこれも使ってみようか、と試してみたのが今回の話です。

コンパイラーを選ぶ

サイト上でコピペするとベンダープレフィックスをつけてくれるものもあるのですが、ちょっと試すのはともかく開発中やり続けるのは少々大変ですね。

https://autoprefixer.github.io/

ということで、 TypeScript における tsc などのように、コマンドなどで自動実行してくれるものが欲しいところ。

すると、 Autoprefixer は単体でというよりは AltCSS と呼ばれる CSS を便利にしたもののプラグインとして提供されているらしい、というところがわかりました。

じゃあそれも使っちゃえ!ということで、 Autoprefixer を使った情報が比較的多く、かつ Node.js でコンパイルできる PostCSS を使ってみることにしました。

コンパイル方法多すぎ問題

※ 手段が豊富なことは色々な利用シーンに対応してくれる、ということで、素晴らしいことであり、否定しているわけではありません。
 念のため。

PostCSS の情報を調べると、非常にたくさんのコンパイル方法が紹介されています。

が、素人には選べない。。。

WebPack

とりあえず TypeScript で使っているし、ということで WebPack を試してみることにしたのですが、失敗しました。

というのも、 PostCSS のファイルを JavaScript(TypeScript) から呼び出しているわけではなかったためです。

じゃあ他は。。。とググった結果、 postcss-cli を使うことにしました。

ということでインストールします。

 npm install --save postcss postcss-cli precss autoprefixer

PostCSS ファイルを postcss-cliコンパイルする

WebPack と同じように、まずプロジェクト直下に設定ファイルを作成します。

postcss.config.js

module.exports = {
    plugins: [
      require('precss'),
      require('autoprefixer')
    ]
  }

Autoprefixer と合わせて require で指定しているのは、 PostCSS から CSS に変換してくれる(多分) PreCSS です。

下記のコマンドでコンパイルすることができます。

npx postcss 変換前の PostCSSファイルパス -c postcss.config.js -d 出力先のディレクトリパス

または

npx postcss 変換前の PostCSSファイルパス -c postcss.config.js -o 出力先のファイルパス

とりあえず変換用にファイルを作る

pcss/page.css

.container {
  display: grid;
  grid-template-columns: 200px 1fr;
  grid-template-rows: 50px 1fr 50px;
}
p {
    span {
        grid-column: 1;
        grid-row: 2;
        color: deepskyblue;
    }
}

PostCSS とわかるように p span を親子関係にし、 Autoprefixer を試すため、 grid を使っています。

これをコンパイルしました。

npx postcss pcss/*.css -c postcss.config.js -d src

そして動かない Autoprefixer

実行した結果は下記のようになりました。

src/page.css

.container {
  display: grid;
  grid-template-columns: 200px 1fr;
  grid-template-rows: 50px 1fr 50px;
}
p span {
        grid-column: 1;
        grid-row: 2;
        color: deepskyblue;
    }

むぅ。。。 PreCSS が通常の CSS に変換していることは確認できましたが、 ベンダープレフィックスが付いていない。。。

あれこれ調べてみた結果、(なぜか) postcss.config.js を下記のように変更すると正しく動作しました。

postcss.config.js

module.exports = {
    plugins: [
      require('autoprefixer')({ "grid": true }),
      require('precss')
    ]
  }

grid を明示的に true にする必要があるようです。

また、これも理由がわからないのですが、 require('precss') を Autoprefixer より上に書くと、 Autoprefixer は効くのに PreCSS が効かない、という問題が発生しました。

腑に落ちないところは多くありますが、とにかくベンダープレフィックスも自動でつけられるようになりました。

やったね。

src/page.css

.container {
  display: -ms-grid;
  display: grid;
  -ms-grid-columns: 200px 1fr;
  grid-template-columns: 200px 1fr;
  -ms-grid-rows: 50px 1fr 50px;
  grid-template-rows: 50px 1fr 50px;
}
p span {
        -ms-grid-column: 1;
        grid-column: 1;
        -ms-grid-row: 2;
        grid-row: 2;
        color: deepskyblue;
    }

flexbox の場合

こうして CSSGrid が使えるようにはなったのですが、どうもレイアウトを組んでいく内に、これ flexbox の方が合ってるのでは? と思い始めたので、 flexbox に切り替えることにしました。

具体的なところは次回にまわしますが、 grid の場合と同じく Autoprefixer が効かない、という問題が発生したため、 postcss.config.js を下記のように変更しました。

postcss.config.js

module.exports = {
    plugins: [
      require('autoprefixer')({
        "grid": true,
        "browsers": [
          "last 2 versions"
        ]
      }),
      require('precss')
    ]
  }

フロントエンドエンジニアの方々の苦労や工夫の賜物であるとはいえ、いっぺんに取り入れすぎて消化不良、というところ。。。

とはいえ、少しずつでも理解していきたいところです。

ASP.NET Core + TypeScript ってみる 3

はじめに

今回は少~しだけ fetch の話と、 webpack を使った import / export の話です。

全然 ASP.NET Core が出てこない? 

。。。(..)_

fetch について

IE 対応

※前回使用した fetch は、 Firefox などでは問題なく動作するのですが、 IE ではまた fetch が見つからないとエラーになります。

ということで、 polyfill 版である whatwg-fetch を使用することにします。

npm install --save-dev whatwg-fetch

下記のように使うことができます。

import 'whatwg-fetch'

public async function loadBooks(): Promise< Array< Book>> {
        return await window.fetch("/books",
            {
                mode: 'cors',
            })
            .then((response) => response.json())
            .then((myJson) => JSON.parse(JSON.stringify(myJson)) as Array< Book>);
    }

(互換性モード?知らない子ですね)

import については後述します

fetch のオプション

fetch API の中で特に気になるのが、第二引数として渡しているオプション。

ここについて調べて。。。と思ったのですが、 MDN に詳しく載っているのでそこ参照ということで。。。

ただ、例えば mode や credentials といった設定で、特にログイン時にどうなるの?といった疑問はあるため次回以降で色々試してみたいと思います。

webpack を使う

さて、次は取得した Book データを Table に出力したいのですが、そろそろコード量が多くなってきました。

C# でクラスを分割するように、 import / export を使うことで TypeScript のコードも分割することができます。

その。。。はずだったのです。

WebAccessor.ts

class Book {
    public id: number = -1;
    public authorId : string = "";
    public name: string = "";
    public available: boolean = false;
}
class WebAccessor {
    public async loadBooks(): Promise< Array< Book>> {
        return await fetch("/books",
            {
                mode: 'cors',
            })
            .then((response) => response.json())
            .then((myJson) => JSON.parse(JSON.stringify(myJson)) as Array< Book>);
    }
}
export {WebAccessor, Book};

一部ラムダ式に変更してはいるものの、やっていることは前回と同じです。

Page.ts

import {WebAccessor} from "./WebAccessor";

async function greeting(){
    var accessor = new WebAccessor();
    let books = await accessor.loadBooks();
    if(books !== null &&
        books.length > 0){
        alert(books[0].name);
    }
    else{
        alert('failed');
    }
}

npx tsc -b のコンパイルは問題なく完了します。

で、これを実行するとどうなるか。

ReferenceError: exports is not defined

/(^o^)\

どうやら、 import / export がサポートされているのは ECMAScript 6 からで、今回のように古いバージョンに対応するためには RequireJS などを使って動的に解決するか、 webpack などを使って依存する JavaScript のファイルを一つのファイルにまとめる(バンドルする)必要があるようです。

そこで前から気になっていたこともあり、 webpack を使ってみることにしました。

準備

↑などを参考に、まずは必要なものをインストールします。

npm install --save-dev webpack webpack-cli ts-loader

設定ファイルをプロジェクト直下に作成します。

tsconfig は JSON ですが、こちらは普通の JavaScript ファイルなのでちょっと注意が必要?かもしれません。

webpack.config.js

var path = require('path');

module.exports = {
    mode: 'development',
    entry: './wwwroot/ts/Page.ts',
    module: {
        rules: [
            {
                test: /\.tsx?$/,
                use: 'ts-loader',
                exclude: /node_modules/
            }
        ]
    },
    resolve: {
        extensions: [ '.tsx', '.ts', '.js' ]
    },
    output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, 'wwwroot/js')
    }
};
  • mode: 無くても実行はできますが、警告が出るため素直に development / production を指定します。
  • entry: Page.ts をベースに、 ts-loader を使って依存している '.tsx' 、 '.ts' 、 '.js' ファイルをバンドルするようです。
  • output: 出力先のファイル名、ディレクトリを指定しています。

実行、そして次の課題

↓のコマンドを実行すると、 wwwroot > js に bundle.js というファイルが出来上がります。

npx webpack

後は、 HTML で読み込む JavaScript ファイルを bundle.js に置き換えます。

Index.html

< !DOCTYPE html>
< html lang="jp">
< head>
    < meta charset="UTF-8">
    < title>Home< /title>
< /head>
< body>
    Hello
    < button onclick="greeting()">Message< /button>
    < script src="../js/bundle.js">< /script>
< /body>
< /html>

これを実行すると...

ReferenceError: greeting is not defined

/(^o^)\

どうやら、デフォルトでグローバル変数や関数は定義できない(呼べない?)ようになっているようです。

なんとか解決

結局このように書くことで無事呼ぶことができるようになりました。

webpack.config.js

~省略~
    output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, 'wwwroot/js'),
        library: 'Page',
        libraryTarget: 'umd'
    }
};

Index.html

~省略~
< body>
    Hello
    < button onclick="Page.greeting()">Message< /button>
    < script src="../js/bundle.js">< /script>
< /body>
< /html>

動いた~、は良いのですが、この library とか libraryTarget って何なのでしょうか。

output.library について

によると、 JavaScript (今回は TypeScript ですが)の関数をライブラリとして外部( HTML )に公開することができる仕組みである、とのこと。

webpack のドキュメントを見ると、書き方がいくつかあり (libraryTarget で指定)、今回使用した umd の他、 var 、 amd が紹介されています。

例えば libraryTarget として var を指定した場合、下記のようになります ( Index.html は同じです)。

webpack.config.js

~省略~
    output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, 'wwwroot/js'),
        library: 'Page',
        libraryTarget: 'var'
    }
};

Page.ts

import {WebAccessor} from "./WebAccessor";

async function greeting(){
    try {
        var accessor = new WebAccessor();
        let books = await accessor.loadBooks();
        if (books !== null &&
            books.length > 0) {
            alert(books[0].name);
        }
    }catch (e) {
        alert(e.message)
    }
}
var Page = greeting();

TypeScript だと書き方が異なるということなのか、 amd の方法は静的解析の時点でエラーになってしまい、うまくできませんでした。

ドキュメントが読み込めていないのだと思いますが、 var と umd それぞれの利点がよく分かっていないため、なんとなく umd の方がまとまってて良さそうだな~くらいに思ってはいます。

参照

ASP.NET Core + TypeScript ってみる 2

はじめに

さくさく進めていきますよ。

とりあえず環境は整ったっぽい(本当はバンドルとか minify とあるだろうけど)ので、実際に動かしてみますよ。

。。。とその前に

IE11 で async/await を使う

もう平成も終わるというのに。。。

まぁお察しくだしあ。くだしあ。

async/await を使ってみる

まず雑に async/await を使って TypeScript のコードを変更してみます。

Page.ts

async function greeting(){
    await setTimeout(() => {
        alert("hello");
    }, 1000);
}

これで greeting() が呼ばれた後 1 秒後にアラートが表示されるはずです。

が、 tsc -b を実行するとエラーが発生します。

wwwroot/ts/Page.ts:1:16 - error TS2705: An async function or method in ES5/ES3 requires the 'Promise' constructor.  Make sure you have a declaration for the 'Promise' constructor or include 'ES2015' in your `--lib` option.

1 async function greeting(){

Found 1 error.

Promise が無いからライブラリに追加しろと。

↑のような情報を参考に、 tsconfig.json を下記のように変更しました。

tsconfig.json

{
  "compilerOptions": {
    /* Basic Options */
    "target": "es5",                          /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
    "module": "commonjs",                     /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
    
    "lib": ["dom", "es2015"],                             /* Specify library files to be included in the compilation. */
    
    ~省略~
  }
}

これでコンパイルできるようになり、 Firefox などでは問題なく動作するようになりました。

ただし、 IE11 ではまだ Promise が見つからないとエラーになります( target を es3 にしても同様)。

↑を参考に、 Promise-X.X.X.min.js を追加で読み込むことで IE11 でも問題なく動くようになりました。

Index.html

< !DOCTYPE html>
< html lang="jp">
< head>
    < meta charset="UTF-8">
    < title>Home< /title>
< /head>
< body>
    Hello
    < button onclick="greeting()">Message< /button>

    < script src="https://www.promisejs.org/polyfills/promise-7.0.4.min.js">< /script>

    < script src="../js/Page.js">< /script>
< /body>
< /html>

IE11 だけでなく、互換性表示を有効にしている場合でも問題なく動作することが確認できました。

ありがたやありがたや(..)_

fetch でアイテムを Get する

まずは CRUD ! 。。。と思ったのですが、まず先行してサーバーにアクセスする部分を書いてみたいと思います。

せっかくなので async / await を使って。

↑ などを見ると、 fetch または axios を使うのが良さそうです。

まずは fetch を試してみます。

https 問題再び

以前触れた気がしますが、現状 ASP.NET Core ではデバッグ実行するとデフォルトで https://localhost:5001 を開きます。

で、何も考えずに下記のようなコードを書いたところ、 loadBooks() でエラーになりました orz

Page.ts

async function loadBooks(): Promise< string>{
    return await fetch("http://localhost:5000/books",
        {
            mode: 'cors',
        })
        .then(function(response) {
            return response.json();
        })
        .then(function(myJson) {
            
            console.log(myJson);
            return myJson;
        });
}

async function greeting(){
    let message = await loadBooks();
    alert(message);
}

エラーの内容は下記の通り。

混在アクティブコンテンツ “http://localhost:5000/books” の読み込みをブロックしました[詳細] Page.js:41:45
TypeError: NetworkError when attempting to fetch resource.

↑によると、 HTTP リクエストと HTTPS リクエストが混在してしまうとセキュリティ上の問題が発生しうるためエラーになる、と。

なるほど。

というわけで、下記のように変更しました。

Page.ts

async function loadBooks(): Promise< string>{
    return await fetch("/books",
        {
            mode: 'cors',
        })
        .then(function(response) {
            return response.json();
        })
        .then(function(myJson) {
            
            console.log(myJson);
            return myJson;
        });
}
~省略~

なお Controller (サーバー側)はこちら。

HomeController.cs

        ~省略~
        [Route("/books")]
        [Produces("application/json")]
        public List< Book> GetBooks()
        {
            return new List< Book>
            {
                new Book
                {
                    Id = 1,
                    AuthorId = 1,
                    Name = "Sample",
                    Available = false,
                }
            };
        }
        ~省略~

Book.cs

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Runtime.Serialization;
using Newtonsoft.Json;

namespace EfCoreNpgsqlSample.Models
{
    [Table("Book")]
    public class Book
    {
        [Key]
        [JsonProperty("id")]
        public int Id { get; set; }
        [JsonProperty("authorId")]
        public int AuthorId { get; set; }
        [JsonProperty("name")]
        public string Name { get; set; }
        [JsonProperty("available")]
        public bool Available { get; set; }       
    }
}

JSON から Model に変換

細かいところはさておき、とりあえずここまでのコードでサーバーから送られた Book クラスのリストを受け取ることができました。

で、せっかくの TypeScript なので、型を持ったデータとして扱いたいですね。

Page.ts

class Book{
    public id: number = -1;
    public authorId : string = "";
    public name: string = "";
    public available: boolean = false;
}
async function loadBooks(): Promise< Book[]>{
    return await fetch("/books",
        {
            mode: 'cors',
        })
        .then(function(response) {
            return response.json();
        })
        .then(function(myJson) {
            var newBooks = JSON.parse(JSON.stringify(myJson)) as Array< Book>;
            return newBooks;
        });
}

async function greeting(){
    let message = await loadBooks();
    alert(message[0].name);
}

結果を見るとシンプルですね。

注意点としては下記の二つぐらいでしょうか。

1. 2 つ目の then の myJson は直接 JSON に変換できない

response.json() の戻り値は Promise< any> です。

で、 2 つ目の myJson は any になるわけですが、これをそのまま JSON.parse に渡すとエラーになります。

そのため、 JSON.stringify でいったん string に変換して渡しています。

2. as T でキャストに失敗しても null にならない

C# に慣れすぎた。。。orz

C# の as は、キャストに失敗すると null になります。

で、そのオブジェクト(今回は newBooks )にアクセスすれば、 NullReferenceException になります。

が、 TypeScript では null にならず、下記は NullReferenceException ではなく、エラーも発生しません。

// 本当は Array< Book>
var newBooks = JSON.parse(JSON.stringify(myJson)) as Book;
            
if(newBooks == null){
    console.log("null death 1");
}
if(newBooks === null){
    console.log("null death 2");
}
// undefined. ただしエラーは発生しない.
var name = newBooks.name;

メソッドを呼んだ場合はエラー( undefined )が発生しますが、最初 JSON からの変換に失敗しているのかと確認に時間を費やしてしまいました/(^o^)\

C# と同じように書ける部分がありつつも、違うところは当然あるので、しっかり見分けないとハマりこんでしまいそうです。

次回は fetch の option ( fetch() の第二引数)からスタートの予定。

あと、 DOM 周りも触っておきたいところ。

ASP.NET Core + TypeScript ってみる 1

はじめに

今回からちょっと間(挫折するか落ち着くまで) フロントエンドに挑戦してみます。

今のトレンドで言えば、以前触れた Angular のようなフレームワークを駆使して SPA ! 。。。って時代ももう落ち着いてきた?というところですが、今回はあえてフレームワークは使わず、 TypeScript + HTML + CSS(こっちはもう少し何か試すかも) で試してみたいと思います。

理由は使う先で Node.js が使えるとは限らないことと、(プライベートは良いのですが)頻繁にアップデートできるとは限らないことです。

まぁ後者については、 TypeScript 使っていいのか?という気がしないでもないですが。

準備

とにかく準備を進めていきますよ。

サーバーアプリは ASP.NET Core MVCこのへん で使っていたプロジェクトを再利用しています。

Node.js

で、まず Node.js なのですが、 Windows だと nodist などのバージョン管理マネージャーを使うのが良さそうですね?

ただ、何も考えず Node.js をそのままインストールしてしまったので、今回はこれでいきます。

TypeScript

Rider の Terminal か PowerShell などでプロジェクト直下に移動し、 npm で TypeScript 、 tsc をインストールします(先に空で package.json 作っておいた方が良い?)。

npm install --save-dev typescript tsc

package.json の中身はこうなりました。

package.json

{
  "devDependencies": {
    "tsc": "^1.20150623.0",
    "typescript": "^3.4.2"
  }
}

最初グローバルにインストールしていたのですが、下記などを見るとローカルインストールの方が良さそうに思えます。

tsc --init

tsc を使って、 tsconfig.json を生成します。

npx tsc --init

今回はローカルインストールしているため、 npx tsc ~ のように実行します。

これでデフォルトの tsconfig.json が生成されます。

tsconfig.json

{
  "compilerOptions": {
    /* Basic Options */
    "target": "es5",                          /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
    "module": "commonjs",                     /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
    // "lib": [],                             /* Specify library files to be included in the compilation. */
    ~省略~
    // "outDir": "./",                        /* Redirect output structure to the directory. */
   ~省略~
    /* Strict Type-Checking Options */
    "strict": true,                           /* Enable all strict type-checking options. */
    ~省略~
    "esModuleInterop": true                   /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
    ~省略~
  }
}

省略した項目も含め、一つずつ内容を見ていきたいところですが、とりあえず動かしてみる、ということで TypeScript -> JavaScript へ変換した出力先を wwwroot/js に変更します。

tsconfig.json

{
  "compilerOptions": {
    ~省略~
    "outDir": "wwwroot/js",                        /* Redirect output structure to the directory. */
   ~省略~
  }
}

TypeScript ファイルを作る

順番が逆な気もしますが、コンパイル元となる TypeScript を用意します。

wwwroot/ts/Page.ts

function greeting(){
    alert("hello world");
}

HTML ファイルを作る

とりあえずボタン押下で JavaScript を呼ぶだけとします。

wwwroot/pages/Index.html

< !DOCTYPE html>
< html lang="jp">
< head>
    < meta charset="UTF-8">
    < title>Home< /title>
< /head>
< body>
    Hello
    < button onclick="greeting()">Message< /button>
    < script src="../js/Page.js">< /script>
< /body>
< /html>

wwwroot 以下の静的ファイルを有効にする

UseStaticFiles で wwwroot 以下の静的ファイルを有効にしておきます。

Startup.cs

using EfCoreNpgsqlSample.Models;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace EfCoreNpgsqlSample
{
    public class Startup
    {
        public IConfiguration Configuration { get; }
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }
        public void ConfigureServices(IServiceCollection services)
        {
            services.Configure(options =>
            {
                options.CheckConsentNeeded = context => true;
                options.MinimumSameSitePolicy = SameSiteMode.None;
            });

            services.AddDbContext(options =>
                options.UseNpgsql("Host=localhost;Database=BookStore;Username=postgres;Password=XXX"));

            services.AddMvc();
        }
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseStaticFiles();
            app.UseMvc();
        }
    }
}

これで下記のような構成になりました。

EfCoreNpgsqlSample
 L EfCoreNpgsqlSample
  L bin
  L Controllers
   L HomeController.cs
  L Models
   L Author.cs
   L Book.cs
   L Store.cs
   L EfCoreNpgsqlSample.cs
  L Properties
   L launchSettings.json
  L wwwroot
   L ts
    L Page.ts
   L pages
    L Index.html
  L appsettings.Development.json
  L appsettings.json
  L EfCoreNpgsqlSample.csproj
  L package.json
  L package-lock.json
  L Program.cs
  L Startup.cs
  L tsconfig.json

Controllers や Models など今回登場していないものもありますが、このへん 参照ということで。
( HomeController.cs については後で触れます)

TypeScript のコンパイル

TypeScript をコンパイルして、 JavaScript ファイルを作ります。

ここでも tsc を使用します。

npx tsc -b

これで Wwwroot > js 以下に Page.js が出力されます。

Page.js

"use strict";
function greeting() {
    alert("hello");
}

【Rider】変更時に自動でコンパイルしてほしい

このコンパイル作業、毎回手動で実行するのは結構大変です。

Gulp や npm などを使って自動でコンパイルすることもできますが、今回は JetBrains Rider の力を借りることにします。
(内部的にはこれらのツールを使っているかもですが)

  1. File > Settings... > Languages & Frameworks > TypeScript を開く
  2. Node interpreter に Node.js のパスを、 TypeScript に プロジェクト > node_modules にある TypeScript のパスを指定する
  3. Recompile on changes にチェックを入れる

f:id:mslGt:20190411072957p:plain

これで TypeScript のファイルを変更すると、自動でコンパイルされるようになります。

注意点として、 tsc -b のようにフォルダ・ファイルの生成は実行してくれない、というものがあります。

そのため、 TypeScript のファイルを追加したときは tsc -b を実行し、ファイルを生成しておく、というのが良さそうです。

静的ファイルのルーティング

さて、 ASP.NET Core で静的ファイルを有効にすると、 wwwroot 以下のファイルは全て外部からアクセスできるようになります。

例えばログインユーザーのみに見せたいなど、 Controller クラスを使ってルーティングをしたい場合はどうすれば良いでしょうか。

ここで静的ファイルのパスを Route として指定しても効果はありませんでした。

HomeController.cs

        ~省略~
        [Route("/")]
        [Route("/Home")]
        [Route("/pages/Index.html")]
        public string Index()
        {
            return "hello";
        }
        ~省略~

これを実行しても、 wwwroot > pages > Index.html が表示されます。

まぁせっかくの ASP.NET Core なので、 Razor を使って View として返してやれば解決するのですが、今回は使わずに頑張ってみることにします。

UseStaticFiles でパス指定

Views の下に置いて。。。など試してみたのですが、結局 wwwroot 直下を公開するのではなく、その下の階層を公開し、ルーティングを行いたいファイル( HTML )を wwwroot 以下の別階層に置く、というのが良さそうという結果になりました。

Startup.cs

        ~省略~
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseStaticFiles(new StaticFileOptions
            {
                FileProvider = new PhysicalFileProvider(
                    Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "js")),
                RequestPath = "/js"
            });
            
            app.UseMvc();
        }
    }
}

これで localhost:5000/js/~ のファイルは表示されますが、 localhost:5000/pages/~ などのファイルは表示されなくなりました。

例えば wwwroot/css など他にも公開したい階層がある場合は、 app.UseStaticFiles をもう一つ追加します。

あとは、 Controller を使って表示されなくなった HTML ファイルを表示するようにします。

HomeController.cs

        ~省略~
        [Route("/")]
        [Route("/Home")]
        [Route("/pages/Index.html")]
        public IActionResult Index()
        {
            return File("pages/Index.html", "text/html");
        }
        ~省略~

これでルーティングが効くようになりました。

いったん切ります。

参照

Entity Framework Core < - > PostgreSQL 間の MAX/MIN 値のやりとり (数値データ編) 3

はじめに

さらっと終わるはずが、もう 3 回目。。。

まぁここは自分にすら役に立つかわからないことを興味の赴くままに調べていく場所。

今回もぼちぼち行きますよ。

trigger

テーブルに INSERT / UPDATE / DELETE が実行された時に特定の処理を実行する、 trigger を作ることができます。

処理自体は PL/pgSQL という言語を使います。

ここでは NumericValueText という text 型のカラムを追加し、 INSERT / UPDATE が実行された時に null か空白でなければ numeric に変換して NumericValue に入れる、という処理を作ってみたいと思います。

ALTER TABLE "NumberSample" ADD COLUMN "NumericValueText" text

手順としては、 function を作り、それを trigger として登録する、ということのようです。

CREATE OR REPLACE FUNCTION im_the_trigger() RETURNS TRIGGER AS $set_numericvalue$
    BEGIN
        IF ((TG_OP = 'INSERT' OR TG_OP = 'UPDATE') AND NEW."NumericValueText" is not null) THEN
            NEW."NumericValue" := CAST(NEW."NumericValueText" AS numeric);
        END IF;
        RETURN NEW;
    END;
$set_numericvalue$ LANGUAGE plpgsql;

大文字にしているからなのか、 IF ~ THEN のせいなのか、 VB っぽさを感じます。

注意点としては、最終行の 「$set_numericvalue$ LANGUAGE plpgsql;」。

これが無いと CREATE TRIGGER ~ の部分でエラーが発生します。

また trigger の名前で、 「$im_the_trigger$」のように設定しようとしたところ、「$」の部分でエラーとなりました。

調べてもよくわからなかったのですが、命名規則などがあるのでしょうか。

ともかく、 function が出来上がったらそれを元に trigger を作ります。

CREATE TRIGGER set_numericvalue BEFORE INSERT OR UPDATE ON "NumberSample"
FOR EACH ROW EXECUTE PROCEDURE im_the_trigger()

ここで trigger と対象となるテーブルの紐づけを行います。

ちなみに trigger を削除する場合は 「DROP TRIGGER set_numericvalue ON "NumberSample"」 のように紐づけたテーブル名も指定する必要があります。

これで INSERT 、 UPDATE 実行時に NumericValueText の値がキャストされて NumericValue に入ります。

どう使うか

今回のケースでは、 C# で頑張って書いていた BigInteger -> string -> 小数点を戻す -> SQL で numeric に変換 といった手順を省略するのに使えそうです。

ただ、あまり不用意に使いすぎると例えば別のアプリから NumericValue の値を直接更新した場合など、想定外の動きにつながることもあり、注意が必要そうです。

trigger については今回ここまでですが、後述の view に対して作ることもできる、ということもあったり、別途もう少し何ができるのかなど遊んでみたいと思います。

rule

DB に rule を設定しておくことで、 INSERT 、 UPDATE 、 DELETE 時に本来の操作の代替となる操作を実行することができる…そうです。
( ON SELECT で条件付きで SELECT に適用することも可能)

試しに INSERT で、 NumericValueText が null でなければキャストした値を NumericValue にセットするようなルールを作ってみます。

CREATE OR REPLACE RULE set_numericvalue_on_insert AS ON INSERT TO "NumberSample" 
WHERE NEW."NumericValueText" IS NOT null
DO UPDATE "NumberSample" SET "NumericValue" = CAST(NEW."NumericValueText" AS numeric)

最初 DO UPDATE ~ を DO INSERT としていたのですが、無限ループになってしまうためエラーが発生しました。

UPDATE についても同じように rule を作ることができます。

ON SELECT

SELECT * FROM "NumberSample" が実行された時に、 NumericValue を含まないような rule が作れるか?と思ってみたのですが、 DO INSTEAD SELECT ~ で指定できる SELECT では、全カラムを含んでいる必要があり、難しいようです。

これが必要な場合、次の view を使うのが良さそうです。

view

SELECT 文において、これまで WHERE によるフィルタリングや CAST による変換などを行った上でデータを取得していました。

これを毎回指定しなくても、物理的なテーブルを作成した時と同じように扱うことができるもの(と思っている) が view です。

例えば下記のように view を作ると…

CREATE VIEW "NumberSampleView" ("Id", "NumericValueText")
AS SELECT "Id", "NumericValueText" FROM "NumberSample"
WHERE "Id" % 2 = 0

下記を実行したときに、「 SELECT "Id", "NumericValueText" FROM "NumberSample" WHERE "Id" % 2 = 0 」と同じ結果が得られます。

SELECT * FROM "NumberSampleView"

なお CREATE VIEW の「("Id", "NumericValueText")」で NumberSampleView におけるカラム名を指定しているため(省略可)、特にテーブルを結合している場合など、必要に応じて指定するのが良さそうです。

また上記では使っていませんが、 INNER JOIN などによる結合、 CAST 、 COUNT などによる元のテーブルにはないカラムの追加を行うこともできます。

ただし、 ORDER BY は使えないとのこと。

あと、テーブルを作成したときと同じように、とは言うものの、実際のところは元の SELECT 文が実行されるので、元のテーブル( NumberSample )の最新のデータが結果として返る、ということと、 view を作っても実行速度が速くなるわけではないということは覚えておくべきかもしれません。

Entity Framework Core での扱い

この view ですが、 Entity Framework Core (以下 EF Core )ではどのように扱うのでしょうか。

。。。と思ったら、通常の物理テーブルと同様に扱うことができるようです。

NumberSampleView.cs

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace EfCoreDataTypeSample.Models
{
    [Table("NumberSampleView")]
    public class NumberSampleView
    {
        [Key]
        public int Id { get; set; }
        public string NumericValueText { get; set; }
    }
}

EfCoreDataTypeSampleContext.cs

    ~省略~
    public DbSet< NumberSampleView> NumberSampleViews { get; set; }
    }
}

HomeController.cs

public async Task< List< NumberSampleView>> Index()
{
    return await _context.NumberSampleViews.ToListAsync();
}

view の更新

さて、このようにテーブルと同じように扱える view ですが、 INSERT や UPDATE はできるのでしょうか。

答えは条件付きでできます、ということです。

PostgreSQL 9.3 から、複数テーブルを結合していないこと・関数を使っていないことなどの条件を満たす view であれば、特に trigger などを作らなくてもそのまま INSERT や UPDATE ができるようになったそうです。

そのため、このような処理を実行すると、 NumberSample にレコードが追加されます。

NumberSampleView newView = new NumberSampleView
{
        NumericValueText = "1234567"
};
_context.NumberSampleViews.Add(newView);
await _context.SaveChangesAsync();

※ NumericValueText 以外は serial などデフォルトで値が入るカラム以外は null になります。

view の作成条件によって更新できたりできなかったりというのは、規模が大きくなってくるとややこしくなりそうなので、どれも更新可能にしておくか、明示的に更新できる・できないがわかるように工夫する必要がありそうです。
( CREATE VIEW の時に information_schema を参照すると更新可能かわかる、とのことですが、特にアプリからだと見落としそうなので)

また、 view には再帰を使って結果を得る Recursive View やクエリの結果を実際のテーブルにキャッシュする Materialized View など種類があるので、これらも今後試してみたいところ。

money

だいぶ寄り道した感がありますが、一応今回ラストのデータ型、 money です。

名前の通りお金を扱うデータ型である、と。

取り得る範囲は -92233720368547758.08 ~ +92233720368547758.07 です。

UPDATE "NumberSample" SET "MoneyValue" = -92233720368547758.08 WHERE "Id" = 35;
UPDATE "NumberSample" SET "MoneyValue" = 92233720368547758.07 WHERE "Id" = 36;

0.08 や 0.07 など、中途半端な値に見えるのですが、これを超えてみるとどうなるのでしょうか。

小数点第4位以下を足してみる

-92233720368547758.081 のように、小数点第4位以下を足した場合。

小数点第4位以下 は切り捨てられ、特にエラーは発生しませんでした。

小数点第3位以上を足してみる

下記のエラーが発生しました。

ERROR: bigint out of range

bigint???

以前試した bigint の範囲 を見てみましょう。

-9,223,372,036,854,775,808 ~ 9,223,372,036,854,775,807

比較しやすいように、カンマ、ドットを省いて見てみます。

  • bigint: -9223372036854775808 ~ 9223372036854775807
  • money: -9223372036854775808 ~ 9223372036854775807

どうやら、 money 型は内部的には bigint で値を持っているようです。

なるほど~。

C# での扱い

小数点以下の値を持ち、正確に扱える、ということで decimal が良さそうです。

28 ~ 29 桁に収まりますし。

ただし、 decimal の方が扱うことのできる値の範囲が広いため、 C# 側から渡すときは注意が必要です。

NumberSample.cs

~省略~
namespace EfCoreDataTypeSample.Models
{
    [Table("NumberSample")]
    public class NumberSample
    {
        ~省略~
        private decimal _moneyValue;
        public decimal MoneyValue
        {
            get => _moneyValue;
            set
            {
                var moneyText = value.ToString(CultureInfo.CurrentCulture).Replace(".", "");
                if (long.TryParse(moneyText, out var longValue) == false)
                {
                    throw new ArgumentOutOfRangeException();
                }
                _moneyValue = value;
            }
        }   
    }
}

雑極まりない感じですが、まぁチェックを入れてあげると良いよねということで。

PostgreSQL の money 型としては、 DB のロケールに合わせて値の扱いも変わる、というのも気になるところですが、少なくとも C# に渡して扱う分には特に変わらないよね、ということでこんな扱いとなっております。

おわりに

特に numeric 型の扱いなど、やっぱりちゃんと調べておかないとなかなかツラい。。。

view など今回軽く触っただけのものも、別途しっかり調べて使えるようにしたいところ。

また、数値型以外の型も色々気になるので、これらも追って調べてみたいと思います。

が、次回は諸事情によりフロントエンドに移る予定。

多分。

参照

trigger

rule

view

Money