いがにんのぼやき

WEBエンジニアのブログ。IT、WEB、バンド、アニメ。

現在時刻を扱う処理をJavaScriptで書いてPCの時刻を変更して試すとき、ブラウザによって癖がある

現在時刻を扱う処理をJavaScriptで書くことはよくあると思う。ある日時を過ぎていたらリンクを非活性にしたり。
そんなときにブラウザで試そうと思うとPCの時刻を変えるのが手軽なわけだ。
もちろん現在時刻をモックして変えられる仕組みを導入していればいいのだが、WEBフロントエンドではそういう仕組みを導入しているところは少ないんじゃないかと。

じゃあ試してみようと思ってPCの時刻を変えてChromeで試してみると数十秒待たないとその時刻が反映されなかったりする。
Firefoxで試すと即時反映される。ブラウザごとに違うんだなーってことで一旦2ブラウザでどう変わってくるか試してみた。
検証したOSはWindowsMacとかだとまた違うかもしれない。

ブラウザ 現在時刻 タイムゾーン
Chrome タブを新しく開けば更新される、そのまま待っていても数十秒更新されない、ページを更新しても数十秒更新されない 即時反映
Firefox 特に更新もせず即時反映 タブを引き直しても反映されない、Firefox自体を落とさないといけない

Windowsとかだと「日付と時刻」というところから「日付を手動で設定する」のところから日時を変更することが出来る。
んで、ブラウザの開発者ツールからConsoleを開いて new Date と叩けば現在時刻が出てくるのでそれで確認。 f:id:igatea:20200308024107p:plain

現在時刻に加えてタイムゾーンの変更時の挙動も検証してみたらちょっと仕様が違っていて面倒だなと。
SafariやEdgeは検証していないが実際に試してみる場合はこのことを頭に入れておかないとハマりそう。

2019年振り返り

せっかくだし簡単に振り返っていく
総評としてはあまり変化のない1年だったなという感じ
忙しかったんだけど何か新しいこととかはあまりやっていないような

仕事

  • 延々とその会社特有のものを勉強する1年だった
  • 例えばその実装になっている背景や延々とレガシーコードを読むとか
  • 持っているスキルを使って最速でタスクをこなしていく、有識者から仕様を聞き回りレガシーコードを読み業務仕様を追って行った1年
  • 業務知識が増えたことによって視野が広がった
  • チームの人が入れ替わりまくって辛かった、また新しい方が入ってきてチームとして人が増えていろいろやっていけそうで嬉しい
  • 全くのエンジニア実務未経験という方を教えることになったので教える力がついた

プライベート

今年はアウトプットが弱かった年なのでいろいろやっていきたいな、仕事、プライベートどちらも

熱海・伊豆旅行記 2/3 望水編 絶景の海!日の出が見れる旅館!

熱海・伊豆旅行記 2つ目です。

伊豆北川駅

MOA美術館を見た後は熱海駅に戻りそこから伊豆北川駅まで電車移動。
1時間電車に揺られて到着。
そこは無人駅でした。

f:id:igatea:20191124005124j:plain

f:id:igatea:20191124005449j:plain

 駅からの眺め

f:id:igatea:20191124005302j:plain

駅では猫がお出迎え

f:id:igatea:20191124005106j:plain

ただしエサ厳禁

f:id:igatea:20191124005507j:plain

駅から旅館までは10分ほど歩きます。
こんな道を下って、海岸線を歩きます。

f:id:igatea:20191124005605j:plain

弧を描いた海岸線なので端からでも目的地の宿が見えてる

f:id:igatea:20191124005846j:plain

望水到着

やっとこさ到着。
この時点で4時くらいになってたと思う。

宿泊する宿は望水というお宿です。

www.bousui.com

f:id:igatea:20191124010521j:plain

f:id:igatea:20191124010657j:plain

でかい・・・思った以上に。
8階建てなので旅館?にしてはホテルっぽい感じもある。
海岸線の方が入り口かと思ったらこっちは裏口で、正面入り口はどうやら8階らしい。

部屋、ラウンジ

一度8階のラウンジに通された後チェックインを済ませたらお部屋に通されました。
今回は7階のお部屋。
確か一番お安いスタンダードなお部屋だったと思う。

f:id:igatea:20191124022318j:plain

f:id:igatea:20191124021514j:plain

お部屋に通された後はお茶と菓子

f:id:igatea:20191124020642j:plain

温泉

お部屋で少し休んだ後は温泉へ

f:id:igatea:20191124021718j:plain

f:id:igatea:20191124033818j:plain

f:id:igatea:20191124022458j:plain

温泉を上がると湯上りサロンというものがあり、そこでところてんが食べられます

f:id:igatea:20191124014703j:plain

めちゃくちゃ美味しかったところてん
行くことがあれば絶対食べてほしい
わさび味が絶妙にさっぱりしていて最高だった

f:id:igatea:20191124014502j:plain

夕食

温泉で休んだ後はお部屋食

f:id:igatea:20191124020827j:plain

f:id:igatea:20191124015814j:plain

f:id:igatea:20191124020011j:plain

 

f:id:igatea:20191124020030j:plain

f:id:igatea:20191124020942j:plain

f:id:igatea:20191124033645j:plain

f:id:igatea:20191124033704j:plain

f:id:igatea:20191124041521j:plain

どれもこれも美味しかった・・・!

食べた後はプライベートガゼボ

プライベートガゼボ

望水ではプライベートガゼボという特別なお風呂が用意されています。
宿泊者には1回50分が無料でついています。
追加料金払えば回数を増やせるみたいですね。
お部屋に着いた時にいつ入るか聞かれていたので夕食の後の時間でお願いしました。
準備ができたら部屋に電話がかかってくるので向かいます。

そんなお風呂があるんだ〜くらいで向かったら凄かった。

なんだろう・・・完成された空間だった。

f:id:igatea:20191124021256j:plain

f:id:igatea:20191124021337j:plain

f:id:igatea:20191124022018j:plain

f:id:igatea:20191124021402j:plain

f:id:igatea:20191124021323j:plain
この空間で裸になって風呂入ったり、バスローブに包まれてゆったりしたりを繰り返してすごく贅沢で幸せな時間だったw

夜の散策

ガゼボを堪能した後は夜の散策に。

8階の正面入り口の看板

f:id:igatea:20191124015207j:plain

夜の望水

f:id:igatea:20191124022204j:plain

ラウンジでチョコとドリンクが振舞われたり 

f:id:igatea:20191124015306j:plain

f:id:igatea:20191124021432j:plain

1日目満喫できました。
おやすみなさい〜。

f:id:igatea:20191124022108j:plain

2日目は日の出から

望水の大きな特徴として絶対あげたいものが日の出コールというものがあります。
スタッフに伝えておくと日の出が出る時間に部屋に電話して起こしてくれるというもの。
この電話をしてくれることで宿から日の出を眺めることができ最高です。
雲などで隠れて日の出が見れない日はそのままお電話しないとのこと。
今回は運よく日の出が出てお電話が!

日の出が最高に綺麗で良かった!
これが一番いい体験でしたね。

ラウンジからの日の出

f:id:igatea:20191124015033j:plain

部屋から

f:id:igatea:20191124021822j:plain

f:id:igatea:20191124021832j:plain

日の出を浴びる望水

f:id:igatea:20191124040011j:plain

日の出を浴びる伊豆北川温泉

f:id:igatea:20191124040028j:plain

f:id:igatea:20191124040727j:plain

台風の影響で入れなかった黒根岩風呂

f:id:igatea:20191124021047j:plain

朝風呂

朝風呂に入った後はまた湯上りサロンで軽食(温泉は人がいたので写真は撮れなかった)

f:id:igatea:20191124020210j:plain

f:id:igatea:20191124020408j:plain

朝食

朝食ももちろん美味。

海苔が食べ放題でめっちゃ食べてしまった

f:id:igatea:20191124020102j:plain

f:id:igatea:20191124020326j:plain

f:id:igatea:20191124020528j:plain

f:id:igatea:20191124033522j:plain

チェックアウト

日も上がり切ったので部屋からは綺麗な海が見えてまたいい景色

f:id:igatea:20191124041021j:plain

ラウンジからも綺麗

f:id:igatea:20191124040953j:plain

f:id:igatea:20191124021552j:plain

そんなこんなでチェックアウトを済ませて望水、伊豆北川温泉を後に。

f:id:igatea:20191124040905j:plain

f:id:igatea:20191124021119j:plain

 充実しすぎ、写真多すぎでめちゃくちゃ長くなってしまった・・・これは是非足を運んで欲しい、また行きたい宿でした。

次は城ヶ崎海岸へ。

熱海・伊豆旅行記 1/3 熱海探索編 MOA美術館を勧めたい!

つい先週に天皇即位が行われるための祝日とその次の日にお休みをとって熱海と伊豆に旅行に行ってきました。
めちゃくちゃ良かったのでいろんな方にお勧めしたいのと、自分の記録として残しておきます。

1日目

  1. 東京駅から熱海駅
  2. MOA美術館
  3. 伊豆北川温泉の宿、望水に宿泊

2日目

  1. 宿からの日の出
  2. 城ヶ崎海岸
  3. 熱海サンビーチ

ってな感じで行ってきました。
長くなりそうなので記事分けてます。

熱海駅しらす丼

 何はともあれ移動ですね。
新幹線で行ったのですが40〜50分くらいで東京から熱海まで行けてしまうんですよね、新幹線早い。片道も5000円弱。

f:id:igatea:20191104002136j:plain
ささっと駅着いてちょうどお昼くらいの時間だったので商店街に。
結構人もお店もいっぱいで栄えてる感じだった。
新幹線の移動中に何が美味しいかなーと思ってネットで調べていたらこのしらす丼のお店が出てきたのでここに。

tabelog.com

お洒落なカフェ?なんだけどしらす丼がとても美味しかった〜〜〜!

f:id:igatea:20191104001229j:plain

f:id:igatea:20191104001602j:plain

f:id:igatea:20191104001622j:plain

f:id:igatea:20191104001707j:plain

 自分が頼んだのは2食丼で1200円。

MOA美術館へ

熱海にはMOA美術館というものがあり、そこが山の上に建っていることもあり景色が最高らしいと。

www.moaart.or.jp

熱海駅からMOA美術館へはバスで行くことができます。
バスは20分間隔くらいなので乗り遅れないよう気をつけて停留所に。

バスに7分ほど揺られると到着。

f:id:igatea:20191104010729j:plain

チケットはネットで買っておいたのでチケット売り場に並ぶ方々を横目に入場。
入場するとオシャレ空間が始まります。

f:id:igatea:20191104011539j:plain

f:id:igatea:20191104011922j:plain

この階段が数個続いた先の虹色の空間を抜けると一度外に。
そこで初めて美術館の全体を見ることができます。

f:id:igatea:20191104012412j:plain

この景色を振り返ると海!これはたまらない!!

f:id:igatea:20191104012831j:plain

立ち位置によっては熱海城や熱海の海岸を眺めることができます。

展示品は歴史的なもの、美しいものと色々ありこの空間を味わうだけでも大満足。

展示の順路入口

f:id:igatea:20191104013150j:plain

 お洒落オブジェ

f:id:igatea:20191104013026j:plain

f:id:igatea:20191104013103j:plain

平安時代から江戸時代まで幅広い巻物や道具が展示されていたり

f:id:igatea:20191104013344j:plain

f:id:igatea:20191104013430j:plain

f:id:igatea:20191104013450j:plain

f:id:igatea:20191104013617j:plain

 この展示のガラスは映り込みが少ないように工夫されているらしく写真を撮っていても目の前で見ていてもガラスを忘れるようになっています。

展示を一通り見た後は美術資料のある建物の裏にある茶の庭に行ってみました。

f:id:igatea:20191104014430j:plain

綺麗な道を抜けると

f:id:igatea:20191104014511j:plain

お洒落な食事処があったり

f:id:igatea:20191104014549j:plain

f:id:igatea:20191104014600j:plain

当時の暮らしを再現した建物があったり

f:id:igatea:20191104014659j:plain

f:id:igatea:20191104014650j:plain

f:id:igatea:20191104014710j:plain

こんな感じでMOA美術館を満喫してきました。
2時間くらい見ていた気がする。
すっごい楽しかった。

そこからまた熱海駅に戻って旅館に。

次に続きます。

ParcelとFirebase Hostingで簡単サイト公開

ParcelとFirebase Hostingを使ってみて今はこんなに簡単にサイトが作れるんだと感動したので紹介。

先週こんなサイトを公開した。

reiwa-count-up.web.app

ただの悪ふざけで作ったサイト。
平成のカウントアップなのにreiwa-count-upというサブドメインにしたり。
ちょうど令和100日目にこんなツイートをして公開した。

このサイトはParcelとFirebase Hostingを使用して開発している。

parceljs.org

firebase.google.com

Parcelすっごく簡単

Parcelは簡単に言ってしまえばWebpackの簡易版。
指定のHTMLをエンドポイントにHTML、CSS、JSを良しなにまとめてくれる。
例えば以下のHTMLをparcelでビルドするとmain.jsを main.4217b2e0.jsみたいな感じのファイルとして生成してリンクを張り替えてくれる。

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

JS内での他ファイルのimportももちろん対応している。

import './style.css';
import hoge from './hoge';

hoge();

試していないがDynamic importsなんかも対応しているとのこと。

npm install --save-dev parcel-bundlerでインストールしnpm scriptでparcel index.html で開発用のサーバーが立ち上がる。
標準はdistというディレクトリに出力される。これは-dオプションで出力ディレクトリを変更可能。
開発ではなく本番用のビルドをするにはparcel build index.html

Parcelの気になったところ

Mochaとの組み合わせ。
テストをするときにParcelでビルドされたものをテストする方法が分からなかった。
なのでテストファイルに書いたimportがそのままでは解決できず、Parcelでは必要ない.babelrcを設定する必要が出た。
こんな感じのnpm scriptを用意して、.babelrcを用意してテストを走らせる必要があった。

"test": "cross-env NODE_ENV=test mocha --require babel-register src/js/*.spec.js",

{
  "presets": ["es2015"]
}

あとは特定の画像などをハッシュ付きにしないで出力ディレクトリに出力する方法が分からなかった。
コピープラグインなどあるようだが、Parcelだけだと出来なそう?
SNS用のOGP画像などは https://reiwa-count-up.web.app/hei.png といったメインでの絶対パス指定になるのでhashがついたパスで出力されるとそこがうまく解決できなかった。
なので同じ画像を出力先ディレクトリに保存しておく運用にしている。
buildした後はhei.pnghei.[hash].pngが存在する形になっている。 これはsitemap.xmlなども同じで出力ディレクトリに保存している。

Parcelについてもっと知りたかったらこの記事を見るといいかも。
よくまとまっている。

qiita.com

細かい設定が出来ないが、すごくシンプルなのでプロトタイプや小規模なものをサクッと作るのに良さそう。

Firebase Hosting色々便利

ドメインもらえる!

Firebase自体もちゃんと使うのは初めてで、Hostingを使ってみたがこれがすごい。
まずドメインが貰える。
正確には[プロジェクト名].web.app[プロジェクト名].firebase.comというドメインをもらうことが出来る。
しかもHTTPSにも対応しているのでそのまま使うことが出来る。

f:id:igatea:20190813015853p:plain

凄くいいなと思ったのはxxx.web.appというドメインが設定できること。
なんかアプリ感あるドメインぽくてよくない?
今までHerokuもそうだけどPaaSの製品名がドメインに入っているのが普通だったけど、ちゃんと普通のサイトっぽいドメインをそのまま使えるのはいい。
最初は独自ドメイン当てようかなと思ってたんだけどこんなちょっとしたサイトでドメイン買うのもなーと思っていたら、このドメインだったのでこれはいいと思ってそのまま使った。

CLIでデプロイ簡単、バージョン管理も

プロジェクトディレクトリでfirebase initコマンドを叩くとfirebaseプロジェクトとして設定を行うことができる。
どの機能を使うか(HostingやFirestoreなど)、プロジェクト名などの設定が簡単にできる。
設定後はfirebase deployで即デプロイが可能。
設定ファイルも以下のような数行で終わる程度。

{
  "hosting": {
    "public": "dist",
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ],
    "rewrites": [
      {
        "source": "**",
        "destination": "/index.html"
      }
    ]
  }
}

デプロイするとバージョニングがされ、指定日時にロールバックも可能。 f:id:igatea:20190813020857p:plain

いや~、本当に便利だ。

値段も相当バズったり動画や画像を置きまくらない限り無料プランで完全に賄える。

f:id:igatea:20190813021739p:plain

余談

Google Search Console反映遅すぎ。
3週間くらいかかった。
sitemap.xmlの反映がうまくいってなかったのが原因かもしれないけど。

JavaScriptで指定のスタイルが当たっている要素を探す

JavaScriptで指定の要素に適用されているスタイルや、特定のスタイルが適用されている要素を探したいときってあるよね。
そんなときに簡単にとろうと思ってJSでコードを書いてみると、

document.getElementById('hoge').style

こんなコード書けばとれるんじゃないかと。
で、オブジェクトが返ってくるので中身を見てみるとCSSに書いたスタイルが取れてないんですよ。
この記法だと要素に直接記述されたスタイルしか取れない。

結論、要素に書いたスタイル、それ以外のスタイルも含めて要素に適用されているスタイルをとるならgetComputedStyleを使おうぜって話。

developer.mozilla.org

const elm = document.getElementById('hoge');
const styles = getComputedStyle(elm);

これでCSSも含めてすべてのスタイルが適用された要素のスタイルがCSSStyleDeclarationという型のオブジェクトで返ってくる。
スタイルはそれぞれケバブケースからキャメルケースにした名前でその値が取得できる。

const elm = document.getElementById('hoge');
const style = getComputedStyle(element).backgroundColor; // rgb(0, 0, 0)

f:id:igatea:20190707004417p:plain

注意として色をblack#000としている場合にrgbに変換された値で出てくるので比較するならそれを配慮しなければいけない。

指定の要素から特定のスタイルが当たっている先祖要素を探す

再帰的に検索してあげればいい。
子孫要素から探すなら逆に再帰関数を書けばいける。

/**
 * @param {HTMLElement} element
 * @param {string} styleName
 * @param {string} styleValue
 */
function findHasStyleAncestorElement(element, styleName, styleValue) {
  if (!element) return null;
  const style = getComputedStyle(element)[styleName];
  if (style === styleValue) return element;
  return findHasStyleAncestorElement(
    element.parentElement,
    styleName,
    styleValue
  );
}

const elm = document.getElementById("c");
const blackBackgroundElm = findHasStyleAncestorElement(
  elm,
  "backgroundColor",
  "rgb(0, 0, 0)" // blackではない
);

Dapperでトランザクションを使用、トランザクション分離レベルを変更する方法のメモ

Dapperでトランザクションの分離レベルを変更するときにどういう挙動をするのかよく分かっていなかったので調べたメモ。
ついでにトランザクション周りのSQLも確認。

今回の環境

今回試しているのはSQLServerだが他のRDBでも方言の違いはあれど基本的に同じなはず。

SQL

まずSQLの確認から。

トランザクションの分離レベルの設定。 Serializableであれば下記。

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE

docs.microsoft.com

分離レベル オプションは一度に 1 つだけ設定でき、設定したオプションは明示的に変更されない限り、その接続で継続的に使用されます。 ステートメントの FROM 句内にあるテーブル ヒントで、テーブルに対して別のロック動作やバージョン管理動作が指定されない限り、トランザクションのすべての読み取り操作は、指定した分離レベルのルールに従って実行されます。

公式ドキュメントを見ると接続単位でトランザクション分離レベルが決まるらしい。

実際に今の分離レベルを確認するなら。

DBCC USEROPTIONS

docs.microsoft.com

このSQLを実行するとisolation levelの項目に今の分離レベルが表示される。

f:id:igatea:20190421204934p:plain

SSMSで確認すると切断、再接続をしてもずっと変更後の分離レベルになっているのでここらへんの挙動ははっきりわからない。
恐らくセッションが生きているようで、SSMS自体を再起動すると規定値に戻る。

MySQLで分離レベルを確認する場合はこんな感じらしい。(未検証)

SELECT @@GLOBAL.tx_isolation, @@tx_isolation;

実際に分離レベルを設定してトランザクションを張るにはBEGIN TRAN、コミットはCOMMIT TRAN。

BEGIN TRAN
    /****** 実行する処理  ******/
COMMIT TRAN

実際にはSQLで全部やるならTRYCATCHしてROLLBACKの処理が入るだろう。

Dapper

次にアプリケーション側でどう書くか。
今回はDapperを使って書いてみる。
まずはusingでSqlConnection.BeginTransactionを呼び出す。
これでトランザクションを張ることが出来る。
BeginTransactionは第一引数で分離レベルを設定することが出来る。

docs.microsoft.com

実際に実行するコードはこんな感じに。 このSQLだと分離レベルを変える意味はないがご愛嬌。

using (var tran = conn.BeginTransaction(IsolationLevel.Serializable))
{
    var sql1 = @"
INSERT INTO Samples ( Id, Name )
VALUES ( @id, @name )";
    var sql2 = @"
INSERT INTO Samples ( Id, Name )
VALUES ( @id, @name + 'duplicate' )";

    try
    {
        await conn.ExecuteAsync(sql1, new { id = sample.Id, name = sample.Name }, tran);
        await conn.ExecuteAsync(sql2, new { id = sample.Id, name = sample.Name }, tran);
        tran.Commit();
        return true;
    }
    catch (SqlException e)
    {
        // log出力とか
        return false;
    }
}

usingでBeginTransactionを囲っておけばこのコードが終わった時にコミットされていなければロールバックしてくれる。
ここで分離レベルを指定しなかった場合、Dapperで実行するとそのコネクションプールが保持されている限り、その接続で前に使用されたトランザクションレベルが使用されてしまう。
・・・と思ったけどDBCC USEROPTIONS の結果は固定化されているが using (var tran = conn.BeginTransaction()) したときは一番最初のトランザクションレベルとなっている。

公式ドキュメントにはこのように記載があった。

分離レベルを指定しない場合は、既定の分離レベルが使用されます。 分離レベルを指定する、BeginTransactionメソッドを受け取るオーバー ロードを使用して、isoパラメーター (BeginTransaction)。 トランザクションの分離レベルは、接続が終了または破棄されるまで、トランザクションが完了した後に永続化します。 分離のレベルを設定スナップショットsnapshot 分離レベルが有効になっていないデータベースではない例外をスローします。 既定の分離レベルを使用して、トランザクションが完了します。

日本語訳が変だけど、以下も併せて読んでみるとどうやらREAD COMMITTEDがSQLServerでのデフォルトでになっており、それが使用されるらしい。

stackoverflow.com

ので、分離レベルはこんな感じに切り替わる。

using (var conn = CreateConnection())
{
    var t1 = conn.Query("DBCC USEROPTIONS"); // 前のトランザクションレベルが表示される
 
     conn.Execute("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE"); // BeginTransactionでは意味なし
 
    using (var tran = conn.BeginTransaction())
    {
        var t2 = conn.Query("DBCC USEROPTIONS", new { }, tran); // READ COMMITTEDが表示、既定のトランザクションレベル

        var sql1 = @"
INSERT INTO Samples ( Id, Name )
VALUES ( @id, @name )";
        var sql2 = @"
INSERT INTO Samples ( Id, Name )
VALUES ( @id, @name + 'duplicate' )";

        try
        {
            await conn.ExecuteAsync(sql1, new { id = sample.Id, name = sample.Name }, tran); // READ COMMITTEDで実行される
            await conn.ExecuteAsync(sql2, new { id = sample.Id, name = sample.Name }, tran);
            tran.Commit();
            return true;
        }
        catch (SqlException e)
        {
            // log出力とか
            return false;
        }
    }
    }
}

trasactionの引数でちょっとした罠があり、パラメータがなくてもQueryAsyncの第二引数に匿名オブジェクトを入れて、第三引数にトランザクションを入れなければいけない。
そうしないとIDbTransactionをオブジェクトとして認識してただのクエリに入れるパラメータと判断し、以下のエラーが出る。

InvalidOperationException: ExecuteReader requires the command to have a transaction when the connection assigned to the command is in a pending local transaction. The Transaction property of the command has not been initialized.

分離レベルを変えて、Serializeレベルで行うならこんな感じになる。

using (var conn = CreateConnection())
{
    var t1 = conn.Query("DBCC USEROPTIONS"); // 前のトランザクションレベルが表示される

    using (var tran = conn.BeginTransaction(IsolationLevel.Serializable)) // 変わる箇所
    {
        var t2 = conn.Query("DBCC USEROPTIONS", new { }, tran); // Serializableが表示

        var sql1 = @"
INSERT INTO Samples ( Id, Name )
VALUES ( @id, @name )";
        var sql2 = @"
INSERT INTO Samples ( Id, Name )
VALUES ( @id, @name + 'duplicate' )";

        try
        {
            await conn.ExecuteAsync(sql1, new { id = sample.Id, name = sample.Name }, tran); // Serializableで実行される
            await conn.ExecuteAsync(sql2, new { id = sample.Id, name = sample.Name }, tran);
            tran.Commit();
            return true;
        }
        catch (SqlException e)
        {
            // log出力とか
            return false;
        }
    }
}

実行したソースはこちら。

https://github.com/igayamaguchi/OrmExample/tree/master/InsertDuplicate

調べている上で知ったこと

SQLServerへの接続を切る方法。

クエリ発行をするセッションは切れない。

docs.microsoft.com

切断。

KILL [セッションID]

セッション確認。

 SELECT conn.session_id, host_name, program_name,
     nt_domain, login_name, connect_time, last_request_end_time 
 FROM sys.dm_exec_sessions AS sess
 JOIN sys.dm_exec_connections AS conn
    ON sess.session_id = conn.session_id;

プライマリーキー、一意性違反の検知

SqlExceptionのNumberに2627という数字が入る。
これはSQLServerの重複違反のエラーコード。
使用するデータベースによって数字は異なる。

public enum CreateResult
{
    Success,
    Duplicate
}

try
{
    await conn.ExecuteAsync(sql, new { id = sample.Id, name = sample.Name }, tran);
    tran.Commit();
    return CreateResult.Success;
}
catch (SqlException e) when (e.Number == 2627)
{
    // log出力とか
    tran.Rollback();
    return CreateResult.Duplicate;
}