2019年8月16日金曜日

node.jsでの日付時刻処理(moment.jsを使ってみる)


オフィス狛 技術部のJoeです。

node.js(JavaScript)では日付時刻の扱いが難しく、特に文字列の時刻などを比較や計算するときはいつも調べているので、備忘録を兼ねてブログに書いておこうと思います。
また、日付時刻操作ではmoment.jsという便利なライブラリがあるので、同じ処理をmoment.jsでも書いてみます。

① 時刻(文字列)の比較と差分

【ライブラリを使用しない場合】
// Dateオブジェクトを生成
const dateTime1 = new Date('2019-08-01 09:00');
const dateTime2 = new Date('2019-08-01 14:00');

// 大小確認
if (dateTime1 < dateTime2) {
    処理;
}

// 一致確認の場合は「getTime()」でミリ秒形式に変換する
if (dateTime1.getTime() === dateTime2.getTime()) {
    処理;
}

// 差分算出の場合も「getTime()」でミリ秒形式に変換して計算、結果のミリ秒を欲しい単位に計算する
const diffTime = dateTime2.getTime() - dateTime1.getTime(); // ミリ秒で差分を算出
const diffSecond = Math.floor(diffTime / 1000 / 60); // 結果のミリ秒を分に変換
console.log(diffSecond); // 300(分)


【moment.jsを使用した場合】
const moment = require('moment');

// momentオブジェクトを生成
const dateTime1 = moment('2019-08-01 09:00');
const dateTime2 = moment('2019-08-01 14:00');

// 比較(<:isBefore  >:isAfter  <=:isSameOrBefore  >=:isSameOrAfter)
if (moment(dateTime1).isBefore(dateTime2)) {
    処理;
}

// 一致(第2パラメータで年から比較したい単位までを指定可能)
if (moment(dateTime1).isSame(dateTime2, 'minute')) { // 年~分までの一致確認
    処理;
}

// 差分(第2パラメータで取得したい差分の単位を指定可能)
const diffTime = dateTime2.diff(dateTime1, 'minutes'); // 差分を分で取得
console.log(diffTime); // 300(分)


② UTCからJSTに変換

【ライブラリを使用しない場合】
文字列の日時はUTCの時間と仮定しています。
// Dateオブジェクトを生成
const dateTime1 = new Date('2019-08-01 09:00'); // UTCの日時と仮定しています
// ミリ秒にして9時間(32,400,000ミリ秒)足す
dateTime1.setTime(dateTime1.getTime() + 1000 * 60 * 60 * 9);
console.log(dateTime1);  // Thu Aug 01 2019 18:00:00 GMT+0900 (日本標準時)


タイムゾーンを扱う場合は、「moment-timezone」を使います。
【moment.jsを使用した場合】
const momentTimezone = require('moment-timezone');

// UTCのmomentオブジェクトを生成
const dateTimeUtc = momentTimezone.tz('2019-08-01 09:00:11', 'UTC');
// JSTに変換(フォーマットも併せて変換)
const dateTimeJst = momentTimezone(dateTimeUtc).tz('Asia/Tokyo').format('YYYY/MM/DD HH:mm:ss');
console.log(dateTimeJst); // 2019/08/01 18:00:11



ご紹介したmoment.jsの機能はほんの一部ですので、日付時刻操作で悩んだらぜひ使ってみてください。


, ,

2019年8月3日土曜日

テンプレート内のclass属性を動的に変える(Angular)


オフィス狛 技術部のKoma(Twitterアカウントの中の人&CEO)です。

今回はAngularの軽い小ネタを紹介します。

テンプレート(html)内のclass属性を動的に変えたい、という事はよくあると思います。
特に、入力項目にエラーがあった場合のエラーメッセージ、などがそうですね。

(1)ngIfを使った方法

まずは、ngIfを使った方法から。

<div *ngIf="loginIdInvalid; else loginIdValidBlock" class="form-label alert alert-danger">
使用出来ないログインIDです。
</div>
<ng-template #loginIdValidBlock>
 <div class="form-label correct correct-info">
 使用可能なログインIDです。
 </div>
</ng-template>

「loginIdInvalid」がコンポーネント側(TypeScript側)で定義している変数で、エラーの場合、trueになると思ってください。
これをclass属性を動的に変えているか、と言われると微妙ですが、結果的にclass属性を使い分けている事になります。
困った時の「ngIf」ってやつですね。

(2)[class.xxxxx]を使った場合

次は、[class.xxxxx]を使った場合です。

<div class= "form-label"
       [class.alert]="loginIdInvalid"
       [class.alert-danger]="loginIdInvalid" 
       [class.correct]="!loginIdInvalid"
       [class.correct-info]="!loginIdInvalid">
{{ message }}
</div>

[class.xxxxx]の「xxxxx」にクラス名を指定し、そのクラスを使用するかどうかが、イコールの後ろの条件式(trueかfalseが返却されるもの)となります。
より簡潔にする為、メッセージは変数にして、コンポーネント側(TypeScript側)で設定するようにしました。
「form-label」は正常でもエラーでも共通して使うので、htmlのclass属性として切り出しました。
ちょっと冗長な気もしますが、良く使用されている手法かと思います。

(3)[ngClass]を使った場合

続いて、[ngClass]を使った場合です。

<div class= "form-label"
       [ngClass]="{'alert alert-danger' : loginIdInvalid, correct correct-info' : !loginIdInvalid}">
{{ message }}
</div>

[ngClass]は、クラスを複数一気に指定できます。連想配列形式で「{'xxxx' : '1111', 'yyyy' : '2222'}」のように設定します。
個人的には一番良く使うのですが、正直、こうして他と比べると、ちょっと見難いと言うか、テンプレート側が複雑になるのは、あまり良くないなぁ、と感じています。

(4)[class]を使った場合

最後に、[class]を使った場合です。
[class]を使う場合、コンポーネント側(TypeScript側)にも少しロジックを書く必要があります。
まずはテンプレート側の設定です。

<div [class]="loginIdClass">
{{ message }}
</div>

続いて、コンポーネント側(TypeScript側)の設定です。
get loginIdClass() {
  return this.loginIdInvalid
    ? 'form-label alert alert-danger'
    : 'form-label correct correct-info';
}

テンプレート側はだいぶスッキリしました。実はこの方式は個人的にはあまり使っていなくて、
今回、ブログにする為に使ってみたのですが、割と良いですね。
ただ、htmlのclassタグを同時に使用出来ないので、共通的のクラス(form-label)の記述を敢えて複数回記載することの面倒さと、本来はhtml、つまり画面表示に関わるclass属性内の文字列をコンポーネント側(TypeScript側)に記載している、と言う気持ち悪さは少し残ります。

以上となります。

それでは、良いAngularライフを!


2019年8月2日金曜日

Illustratorのトレース機能で出来るだけ楽にロゴをトレスする。


こんにちは、オフィス狛 デザイン部のSatoです。

今回は、Illustratorを使ってjpgやpngのロゴなどの簡単な画像を簡単にトレースする方法をご紹介したいと思います。(Illustrator使ってる方はみんな知ってる気でいたんですが意外と知らない方も多いようなので)
調節は必要ですが、ペンツールでトレースするよりも早くトレースできるので時短になりますよ!


今回使用する画像です。(架空のサービスのロゴマークです。)

まずこの画像をイラレで読み込みます。
画像を選択した後、コントロールパネルの「画像トレース」をクリックします。


トレース処理が終了後、コントロールパネルの「プリセット」を変えたりしていい感じのプリセットを探します。
一番元画像に近いプリセットを選択後、コントロールパネルの「拡張」ボタンをクリックすればパスにしてくれます!

単純な画像ならそのまま調整せずに綺麗にトレースしてくれます。


複雑な画像で「画像トレース」機能を使う場合は「ウインドウ」>「画像トレース」を選択して画像トレースパネルで調整しましょう!仕上がりを見ながら作業できるため便利です。


昔フォトショップで描いた鶏の絵をトレースしてみました!
絶対ペンツールでトレースしたくないような無駄にテクスチャなど使った絵を細かく再現してくれましたー!ありがたすぎる。


この方法を使うとアナログのラフスケッチを簡単にパス化したり自分で撮影した写真をトレースしたものを元にアイコンを作れたりして便利です!ぜひこの機能を使ってみてください!

2019年7月30日火曜日

踏み台サーバー(EC2)経由でSQLServer(RDS)にSSH接続する方法(ポートフォワーディング)・MacOS編


オフィス狛 技術部のKoma(Twitterアカウントの中の人&CEO)です。

以前、弊社のメンバーが、踏み台サーバー(EC2)経由でSQLServer(RDS)にSSH接続する方法(ポートフォワーディング)・Windows編という記事を書きました。

今回は、MacOSでのやり方について記載しようと思います。
(「ポートフォワーディングとは?」の説明については、Windows版の記事をご覧ください。)
今回の接続イメージは下図の通りです。


前回のWindows版は、踏み台サーバーから直接データベースへ接続していましたが、今回は、踏み台からさらにWebサーバーを経由してデータベースへ接続する事にします。
手順は以下となります。
(1)Webサーバー(EC2)へポートフォワーディングする設定で、踏み台サーバー(EC2)へSSH接続
(2)データベース(RDS)へポートフォワーディングする設定で、(1)のポートフォワーディング状態でSSH接続
(3)上記までのポートフォワーディングした状態でSQLServerにログインしDB操作
※各SSH接続で使う鍵はローカルに持っている事を前提とします。

手順(文章)だけだと、分かり難いですね・・・・ということで、具体的な手順を書いていきます。


(1)Webサーバー(EC2)へポートフォワーディングする設定で、踏み台サーバー(EC2)へSSH接続

まずは、踏み台経由でWebサーバー(EC2)へ接続します。
イメージ図で言うと、下記の赤字の「(1)」の部分になります。

まず、mac標準ツールのターミナルを開き、以下のコマンドを入力し、実行します。
ssh -L 2200:10.0.1.1:22 -i key-hoge-bastion.pem bastionuser@xx.yy.zz.001

それぞれ、説明をしていきます。

「ssh -L 2200:10.0.1.1:22」:
ローカルとリモートを繋ぐ設定になります。この場合、最終的に「localhost:2200」が「10.0.1.1:22」へと繋がる事になります。
localhost側のポートは、使用していないものであれば、何でも構いません。
最終的に接続する先のIP(今回は「10.0.1.1」)はプライベートIPである事に気を付けてください。

「-i key-hoge-bastion.pem bastionuser@xx.yy.zz.001」:
これは踏み台への接続設定です。「-i」の後に、踏み台の鍵を指定し、その後ろに、「接続ユーザー名@パブリックIP or ドメイン」を指定します。

コマンド実行後、ターミナルは踏み台に繋がった状態になっています。
ですので、さらにポートフォワードする場合には、ターミナルを新たに起動する必要があります。

※「ssh -fNL」とする事で、バックグラウンドでコマンドが実行されるので、そのまま同じターミナルを使う事が可能です。ですが、今、どこにポートフォワードしているのか分からなく可能性がありますし、タスクをkillするのも面倒だったりするので、最初の内は、ポートフォワードする単位でターミナルを開くことをお勧めします。


(2)データベース(RDS)へポートフォワーディングする設定で、(1)のポートフォワーディング状態でSSH接続

続いて、Webサーバー(EC2)経由でデータベース(RDS)へ接続します。
イメージ図で言うと、下記の青字の「(2)」の部分になります。


ssh -L 1433:hoge.z6vbrfgt5st.ap-northeast-1.rds.amazonaws.com:1433 -i key-hoge-web.pem webuser@localhost -p 2200

それぞれ、説明をしていきます。

「ssh -L 1433:hoge.z6vbrfgt5st.ap-northeast-1.rds.amazonaws.com:1433」:
ローカルとリモートを繋ぐ設定になります。この場合、最終的に「localhost:1433」がRDSのエンドポイント「hoge.z6vbrfgt5st.ap-northeast-1.rds.amazonaws.com:1433」へと繋がる事になります。
localhost側のポートは、使用していないものであれば、何でも構いませんが、後のことを考えて、1433にしました。

「-i key-hoge-web.pem webuser@localhost -p 2200」:
これは、Webサーバー(EC2)への接続設定です。「-i」の後に、Webサーバー(EC2)の鍵を指定し、その後ろに、「接続ユーザー名@localhost -p (1)で設定したポート番号」を指定します。
ポイントは、接続するのはあくまで「ポートフォワード設定をしたlocalhost」と言うことです。

コマンド実行後、ターミナルはWebサーバー(EC2)に繋がった状態になっています。
今回はこれ以上はポートフォワード設定を追加しないので、ターミナルはこのままにしておきます。


(3)上記までのポートフォワーディングした状態でSQLServerにログインしDB操作

ここまで来たら後はデータベースへの接続のみです。
今回は、Azure Data Studioを使って、接続します。

すでに、「localhost:1433」は、「踏み台→Webサーバー」を経由して、「endpoint:hoge.z6vbrfgt5st.ap-northeast-1.rds.amazonaws.com」に繋がっている状態なので・・・


上記のように、接続先を「localhost」(Azure Data Studioはデフォルト1433ポートを使用するので、1433は省略可能)として、ユーザー名とパスワードを入力すれば、無事に接続出来ると思います。

以上となります。今回はSQLServerに限定しましたが、他のDBサーバーでも、基本的な流れは変わらないと思います。


, , , , ,

2019年7月29日月曜日

C#でListの多次元化をやってみる。


オフィス狛 技術部のHammarです。

C#で開発中に、ある処理で値を多次元で持たせる必要があり、最初は配列で多次元化を考えたのですが、処理内容的にListを使っているので、Listで多次元化を行ってみました。
php等で配列の多次元はよく行いますが、C#でかつListで行ったことがなかったので、いろいろ調べつつ、自分への備忘録も兼ねて今回記載しようと思います。

まず基本的なListでの動きですが、通常Listに値をセットする場合等は以下のように記述すると思います。
var list = new List<string>();
list.Add("みかん");
list.Add("りんご");
list.Add("バナナ");

上記は1次元Listなので、そのまま
list[0]=>"みかん"
list[1]=>"りんご"
list[2]=>"バナナ"
というような感じで値が入ります。

で、多次元Listですが、基本的には2次元Listができればあとは応用という感じなので、一旦わかりやすく2次元Listを作ってみたいと思います。
例えば以下のようなphpの多次元配列のような感じでデータを持ちたい場合があるとします。
    [0]=>{
        [0]=>"みかん"
        [1]=>"orange"
    }
    [1]=>{
        [0]=>"りんご"
        [1]=> "apple"
    }
    [2]=>{
        [0]=>"バナナ"
        [1]=> "banana"
    }

上記を2次元Listにするには以下のように記述します。
var list = new List<List<string>>()
list.Add(new List<string>());
list[0].Add("みかん");
list[0].Add("orange");
list[1].Add("りんご");
list[1].Add("apple");
list[2].Add("バナナ");
list[2].Add("banana");
結果上記でいくと例えば
list[0][1]=>"orange"
list[2][0]=>"バナナ"
がデータとしてセットされていることになります。

Listの<>内にはクラスも入れる事ができるということなので、多次元化ができるということらしいですね。
なので、3次元、4次元としたい場合(そんなにないと思いますが)、さらにListの<>を入れ子にしていけば可能になります。

あと1次元Listと違って少し注意したいのが、Listに追加するときにはListの中もnewしてインスタンスする必要があります。自分はここで少しハマりました。。

以上のような感じで、Listを多次元化して処理を行う場合に参考になればと思います。

2019年7月26日金曜日

ローカルにあるhtmlファイルでChromeの拡張機能を使う設定。

こんにちは、オフィス狛 デザイン部のSatoです。

今回は、ほんの小技なのですが、ローカルにあるhtmlファイルをChromeで開いた際に、拡張機能を使う方法(設定)です。

何の設定もしていない状態だと、Chromeでローカルファイルを開いた時には拡張機能が使えないので、少々不便です。
私はコーディング中のページ全体を拡張機能でスクリーンショットを撮りたい時があるので、結構困っていました。

コーディング中でも拡張機能が使いたいと思う方は、私以外にもいらっしゃると思いますので設定方法をご紹介します。

拡張機能のアイコンバーを右クリックして、「拡張機能を管理」をクリックし、詳細設定画面に遷移します。

「ファイルの URL へのアクセスを許可する」という項目があるのでONにするとローカルファイルをChromeで開いた際も拡張機能を使えるようになります。

ただ、セキュリティ的に不安のある拡張機能では使わないようにした方が良いかと思います。
(ですので「ファイルの URL へのアクセスを許可する」をONする際は、拡張機能のセキュリティ要件を確認し、慎重に判断してください)

2019年7月5日金曜日

Amazon ConnectとLambdaで電話をかける ③Lambda関数の作成と、④実行


こんにちは、オフィス狛 モバイル開発担当 Aika-yuy です。
今回の投稿は、 AmazonConnectとLambdaで任意の番号に電話をかける方法をご紹介します。









AmazonConnectはコールセンターをメインとして、簡易に作成できるというものなので、
受電する記事をよく見かけますが、架電方法の記事は少なかったので記事にしました。




難しそうに見えますが、簡単で短時間で作成できますので試してみてください。
こちらの4ステップで紹介していきます。

①Amazon Connectで電話番号、お問い合わせフローの作成
②IAMでロールを作成
③Lambda関数の作成← 今回はこちら
④実行!!← 今回はこちら

 

③Lambda関数の作成

今回はいよいよ、Lambda関数からスマフォに電話をかけてみます! とっても簡単なので、あと少し頑張りましょう。

〜必要なもの〜

1、①で作成した電話番号
2、①で作成したお問い合わせフローのインスタンスID、リージョン名、コンタクトフローID
3、②で作成した、ロール
4、任意の電話番号


AWSにログイン後、Lambdaを選択します。
ページ遷移後、関数の作成ボタンを押してください。
下の画像のように入力選択していきます。
1.関数名に任意の関数名をつけてください。
2.実行ロールを既存のロールを使用するを選択してください。
3.②IAMでロールを作成で、作成したロールを選択してください。
※作成したロールがすぐに反映されないことがあるので、作成したロールが見つからない場合は少し時間をおいてからもう一度試してみてください。
4.関数を作成ボタンを押す

それではいよいよ関数を作っていきましょう!

exports.handler = (event) => {
     
    // import entire SDK
    var AWS = require('aws-sdk');
 
    var connect;
    // create amazon connect object
    // ①で作成したお問い合わせフローのリージョン名を入力してください。
    connect = new AWS.Connect({apiVersion: '2017-08-08', region:"ap-northeast-1"});
 
    // create API request parameter
    var params = {
      // ①で作成したお問い合わせフローのコンタクトフローID
      ContactFlowId: "XXXXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
      // 今回かける先の電話番号
      DestinationPhoneNumber: "XXXXXXXXXXXXXX",
      // ①で作成したお問い合わせフローのインスタンスID
      InstanceId: "XXXXXXXXXXXXXXXXXXXXXX",
      // かける元の電話番号(作成した電話番号)
      SourcePhoneNumber: "+8XXXXXXXXXXX"
    };
  
  // call API with parameter
  console.log("開始!!");
  var calling = connect.startOutboundVoiceContact(params, function(err, data) {
    if (err) {
      console.log(err);
    } else {
      console.log("終了!!電話を切る");
    }
  });
  return calling;
};


これで完成です!

④実行!!

それではいよいよ電話をかけてみます。
Lambdaのページの一番上、右にあるテストボタンを押してください。

任意の電話番号に、電話がかかれば成功です。

意外と簡単に作成できましたね。
アレクサから電話したり色々できそうで、夢が広がりますね。
次回はアプリ(ios)から電話をかけるをやっていきます。

, , ,

2019年6月30日日曜日

AngularのdetectChanges()で「ViewDestroyedError: Attempt to use a destroyed」が発生した時の対応方法。


オフィス狛 技術部のKoma(Twitterアカウントの中の人&CEO)です。

私の担当がいつの間にかAngular専任になっていますが・・・・今回もやっぱりAngularネタです。

Angularで、NgRxなどでAPIから値を取得した場合、そのままView(template・html)側にデータを流し込んであげれば、取得したデータは問題なく表示されます。
ところが、component(TypeScript)側で取得したデータをstore.selectで取得すると、View(template・html)側に表示データが反映されない場合があります。

うーん、言葉だけで説明するのが難しい。
この辺はいつかNgRxの使い方とかで詳しく説明したいですが、前者の場合、ストリーム(川の流れ)は繋ぎっぱなしですが、
後者は、一度ストリーム(川の流れ)から、データ取っているので、流れが止まっている、という事ですね。

強制的に画面に反映させる為には、「changeDetectorRef の detectChanges() 」メソッドを使います。

では、ここで使い方の例を一つ。
import { Component, ChangeDetectorRef, OnInit } from '@angular/core';
// (中略)
export class HogeKomaComponent implements OnInit {
// (中略)
  constructor(
    public store: Store,
    private changeDetectorRef: ChangeDetectorRef
  ) {}
 
  ngOnInit() {
    this.hogeKomaSubscription.push(
      this.store
        .select(fromHoge.getKomaList)
        .pipe(skip(1))
        .subscribe(result => {
            //
            // ここで取得したデータを編集し、Template側の変数へ再設定(これだけだと、画面は更新されない)
            //
 
            // 画面の表示を更新する(これを実施する事で画面が更新される)
            this.changeDetectorRef.detectChanges();
    }));
 
    // データ取得
    this.store.dispatch(new KomaListActions.GetKomaList());
  }

で、ここからが本題なのですが、「detectChanges()」を使っていると、以下のようなエラーが発生する事があります。
    common.96cfb6ba445916612966.js:1 ERROR Error: ViewDestroyedError: Attempt to use a destroyed view: detectChanges
    at pm (main.35927d7a5d9b4fa62e08.js:1)
    at Object.m_ [as updateDirectives] (main.35927d7a5d9b4fa62e08.js:1)
    at jv (main.35927d7a5d9b4fa62e08.js:1)
    at D_ (main.35927d7a5d9b4fa62e08.js:1)
    at Object.i_ [as checkAndUpdateView] (main.35927d7a5d9b4fa62e08.js:1)
    at n.detectChanges (main.35927d7a5d9b4fa62e08.js:1)
    at e._next (10.ab643854ddb1ed074304.js:1)
    at e.__tryOrUnsub (main.35927d7a5d9b4fa62e08.js:1)
    at e.next (main.35927d7a5d9b4fa62e08.js:1)
    at e._next (main.35927d7a5d9b4fa62e08.js:1)

エラーメッセージは、
ViewDestroyedError: Attempt to use a destroyed view: detectChanges
という事ですが、要は、「画面表示を行おうとしたけど、もう対象のViewが存在しない」という事ですね。

こういう場合は、
  if (!this.changeDetectorRef['destroyed']) {
    this.changeDetectorRef.detectChanges();
  }

上記のように、既にViewが破棄されていないか確認する事で回避出来ます。

今回も内容の割には長くなってしまいました・・・・
(エラーメッセージと対応内容書けば、ある意味終了なんですけどね。)

こんな内容でも、誰かの役に立つ事を願って・・・・

では、より良いAngularライフを!


Dockerの開発環境作成で発生した問題(BIOS is mandatory、bashエラー、no such file or directory 等)と、その対処方法。


オフィス狛 技術部のJoeです。

先日、Dockerによる開発環境を作成しました。
とはいっても、Docker初心者なので一から構築した訳でなく、既にymlファイルなどの定義は作成されていたので、必要なパッケージをインストールして起動するだけ。。。
のはずだったのですが、なぜか私の端末でのみいくつか問題が発生したので、対処方法と併せてご紹介します。

【環境】
・Windows10 Home
・Docker Toolbox 18.03.0-ce

問題1.仮想化を有効にできない。

まず、Docker用の仮想マシンを構築するため、「Docker Quickstart Terminal」を実行すると以下のエラーが発生しました。
Running pre-create checks...
Error with pre-create check: "This computer doesn't have VT-X/AMD-v enabled. Enabling it in the BIOS is mandatory"

すぐにBIOSで仮想化の有効化を忘れていたと思い、BIOSを起動しましたが、「VirtualizationTechnology」など、仮想化に関する項目が見当たりません。

これが意外と事例が無く、解決までに結構時間が掛かってしまったのですが、結局、BIOSをアップデートすることで仮想化の項目が現れ、有効化することができました。

参考までに、今回アップデートしたBIOS情報を載せておきます。
・BIOS:VC65-C1
・バージョン:更新前)0501 更新後)0801
・ダウンロード先:https://www.asus.com/jp/Mini-PCs/VivoMini-VC65-C1/HelpDesk_BIOS/


問題2.bashが実行できない。

仮想化を有効できたので、再度「Docker Quickstart Terminal」を実行すると、今度は以下のエラーが発生しました。
このショートカットは、リンク先の'bash.exe'が変更または移動されているので、正しく機能しません。

「Docker Quickstart Terminal」で実行される「start.sh」はbashなのですが、Docker Toolboxと一緒にインストールするGitの「bash.exe」を使用します。

こちらは、Docker Toolboxをインストールする前に、Gitを既にDドライブにインストールしていたことが原因でした。
「Docker Quickstart Terminal」のショートカットのリンク先、「bash.exe」のパスをCからDドライブに変更することで、エラーを解消できました。

問題3.コンテナが起動しない。

スタートからつまずきましたが、やっとコンテナの起動ということで、「docker-compose up」コマンドを実行したところ、以下のエラーで起動に失敗しました。

【コンテナのログ】
npm ERR! path /usr/src/app/package.json
npm ERR! code ENOENT
npm ERR! errno -2
npm ERR! syscall open
npm ERR! enoent ENOENT: no such file or directory, open '/usr/src/app/package.json'
npm ERR! enoent This is related to npm not being able to find a file.
npm ERR! enoent

「package.json」が見つからないと言ってますが、コンテナにコピーした時にエラーは出てなく、npmのインストールも正常に実行できていました。

docker psコマンドでSTATUSを見ると、Restartingが繰り返されている状態になっています。
CONTAINER ID  IMAGE  COMMAND                CREATED         STATUS                          PORTS  NAMES
6438e1e2df74  test   "bash -c 'npm run de…" 29 seconds ago  Restarting (254) 2 seconds ago         test

エラーになったコマンド「docker-compose up」では、ymlファイルを2ファイル(共通、環境別用に分けている)指定していたのですが、定義内容を1ファイルにまとめて指定することで起動できました。

2ファイルで起動できなかった原因がどうしても見つからなかったので、こちらは引き続き調べてみたいと思います。

2019年5月31日金曜日

Node.jsでknex.jsを使ったSQL書き方Tips


オフィス狛 技術部のHammarです。

皆さんNode.jsとRDBを使って開発をされるときに、クエリビルダは何をお使いでしょうか?

以前にもブログで何度か書きましたが、弊社の一部プロジェクトではknex.jsというクエリビルダを使っています。

結構クセのある書き方ではじめは手こずりましたが、やはり人間慣れは重要で、今では意外と使いやすいなと思う部分も結構あります。
幅広いDBの種類に対応しているので、DBの種類によってインストールするモジュールを変える必要はなく、これ1つで同じ書き方ができるのでお勧めです。

ただあまり日本語の解説記事がないので、SQLの特殊な書き方をするときにどう書けばいいかわからなくて、ちょっとだけハマった事象について、自分への備忘録も兼ねて書こうと思います。

■基本的なSQL

例えば出身地、名前、年齢が登録されているテーブルがあったとして、下記のようなSQLでとあるデータを取得するとします。
    select
        name
    from users
    where
        birth_place = '東京都'
        and age >= '20';
これをnodeでknexを使って書くと(knexはrequire済みとします)
knex
    .select('name')
    .from('users')
    .where('birth_place', '東京都')
    .andWhere('age', '>=', '20')

という感じで基本的にはSQLの書き方に沿った感じで書くことが可能です。

基本的な書き方は公式リファレンスをみていただければほとんど大丈夫なのですが、公式にも詳しくのっていなかったりするものもあります。

■数値をプレースホルダにいれる場合

以前ブログにも書きました数値をプレースホルダにいれる場合、下記のように書きます。
狛ログ:knexライブラリのIN句でプレースホルダを使うときの注意点
const array = [1, 2, 3];
knex
    .select('name')
    .from('users')
    .whereRaw('id in (?)', [array]);

■from句でエイリアスを使いたい場合

knex
    .select('name')
    .from(users.clone().as('u'));
これはたとえばサブクエリでいろいろ結合した結果をセレクトしたいとき等で、別名にしたいときに利用できます。
const subQuery = サブクエリの内容
knex
    .select('sub_name')
    .from(subQuery.clone().as('sub'));

■order byで複数設定したい場合

通常のorder byの記述の方法で複数ソートしたいとき、公式の書き方だとちょっと面倒です。
    select *
    from users
    order by email asc, age desc
これをknexで書くと
knex
    .('users')
    .orderBy(['email', { column: 'age', order: 'desc' }])

上記でも全然問題ないんですが、ちょっとぱっと見わかりにくいのと、ソートが沢山あるとさらにわかりにくくなるので、下記のような書き方のほうがいいと思っています。
knex
    .('users')
    .orderByRaw('email asc, age desc');

このRawというのは基本的にそのままSQL文を記述が可能で、join句にもjoinRawというのがあったり、where句にもwhereRawというのあります。
select句にはselectRawみたいな書き方はないのですが、knex.raw()を使うことができ、例えば下記のようにselectにcase文を使いたい時など、select句で特殊なことをするときに使えます。
knex
    .select(
        'name',
        knex.raw('case when age >= 10 then categoryA else categoryB end as category'),
    )
    .from('users')
    .where('birth_place', '東京都')

で、knex.raw()はfrom句だろうが、where句だろうがどこにでも使えて、これを使うともはやknexの書き方に従わなくともSQLがそのまま書けますw
そこは臨機応変にだと思いますが、このように結構自由度も高いのでknexは個人的にはお勧めだと思います。

またtips的な事象が出てきたら書きたいと思います。

,

2019年5月30日木曜日

AppStoreアプリ審査の際にスクリーンショットでリジェクトされた!リジェクトされポイント


こんにちは、オフィス狛 デザイン部のSatoです。

今回はAppStoreアプリ審査の際にスクリーンショットでリジェクトされた話です。
私は確認不足で色々とAppStoreアプリ審査の際にやらかしがちなのでスクリーンショットを理由にリジェクトされたりなことがあります。
スクリーンショットでリジェクトされるとやっぱりエンジニアさんに申し訳なくなる……ので、リジェクトされないように自分のミスを共有していこうと思います。


①AppStoreアプリ審査する際に必要なスクリーショットのサイズが変更になった。

リジェクト以前にアプリ審査が出せません!
AppStoreアプリ申請について必要なものを分かりやすくまとめているブログサイトは便利でわかりやすいのですが、「数日前に増えた申請必須項目」は当然載ってないことが多いので、アプリのアップデートや新規申請の際はApp Store Connectのニュースページを確認しましょう。(このページ探すのが地味に大変でした)
アプリ申請の少し前に① に気づくとちょっと辛いです。

②スクリーンショットの端末対応サイズとスクリーショットに使用している端末が違う。

例えばiPhoneXなどの端末で表示するためのスクリーンショットに写っている端末がiPhone8な時はリジェクトされます。
悲しいですね。
直前に①に気づくと端末枠用意していないからスクリーンショットのサイズだけ変えて申請!としたくなりますがリジェクトされます。
(スクリーショットをはめこんでいる端末っぽい枠がイラストっぽくても色が違くても大丈夫みたいです。今後変わる可能性がありますが……)


今後、またスクリーンショットでリジェクトされたらその話題でブログを書こうと思います。
自分のミスでリジェクトされないのが一番いいですが!


(こぼれ話なのですが、今回のブログの最初に表示される画像の背景みたいな柄が可愛いくて大好きなのですが、なんていう名前なのか分からないので困っています。
分からないままこういう感じの幾何学模様ごちゃ混ぜみたいな柄を真似して作りました。
柄名を知ってる方!なんていう柄なのか教えてください!)


2019年5月29日水曜日

Amazon ConnectとLambdaで電話をかける ②IAMでロールを作成


こんにちは、オフィス狛 モバイル開発担当 Aika-yuy です。
今回の投稿は、 AmazonConnectとLambdaで任意の番号に電話をかける方法をご紹介します。









AmazonConnectはコールセンターをメインとして、簡易に作成できるというものなので、
受電する記事をよく見かけますが、架電方法の記事は少なかったので記事にしました。




難しそうに見えますが、簡単で短時間で作成できますので試してみてください。
こちらの4ステップで紹介していきます。

①Amazon Connectで電話番号、お問い合わせフローの作成
②IAMでロールを作成← 今回はこちら
③Lambda関数の作成
④実行!!


②IAMでロールを作成

Lambda関数から、Connectを呼ぶ際に、権限を設定する必要があります。
設定するのは、下記の2つです。
・StartOutboundVoiceContact(電話をかける権限)
・StopContact(電話を切る権限)
IAM→ロール→ロール作成→
Lambdaを選択→次のステップにアクセス→
ポリシーの作成→サービスでConnect選択→StartOutboundVoiceContact、StopContact選択→
全てのリソースにチェック→ポリシーの確認→
名前を入力→ポリシーの作成→完成!!!


意外と簡単に作成できましたね。

次回は③Lambda関数の作成を書いていきます。


,

2019年5月1日水曜日

AngularのEventEmitter(Output、emit) で複数の値を送りたい。


オフィス狛 技術部のKoma(Twitterアカウントの中の人&CEO)です。

令和最初のブログはAngularです。

今回は、普段は意識しないけど、「そう言えばどうやるんだ?」的な小ネタです。

Angularでは、親コンポーネントから子コンポートに値を渡す時は「@Input()」を使用します。
イメージ的には変数経由で値を渡す、と言う感じですかね。
逆に子コンポーネントから親コンポートに値を渡す時は「@Output()」を使用します。
こちらは、メソッドに経由で値を渡す、と言う感じです。

この「@Output()」ですが、簡単な使い方として、
親コンポーネント側(hoge-parent.component.ts)で、以下のように記載します。
分かりやすくする為にView(html)はファイルを分けています。(hoge-parent.component.html)

[hoge-parent.component.ts]
@Component({
  selector: 'koma-hoge-parent',
  templateUrl: './hoge-parent.component.html'
})
export class HogeParentComponent {

// (中略)

  onSubmit(childValue: string) {
    console.log(childValue);
  }
}

[hoge-parent.component.html]
<koma-hoge-child
  (formSubmit)="onSubmit($event)">
</koma-hoge-child>

「onSubmit($event)」が子コンポーネントからの指示(formSubmit)を待ち構えている、と言う表現がしっくりきますね。

子コンポーネント側(hoge-child.component.ts)は、以下のように記載します。
[hoge-child.component.ts]
@Component({
  selector: 'koma-hoge-child',
  templateUrl: './hoge-child.component.html'
})
export class HogeChildComponent {
  @Output() formSubmit = new EventEmitter<string>();

// (中略)

  onClickButton(textValue: string) {
    this.formSubmit.emit(textValue);
  }
}

onClickButton のメソッドの中で、textValue を引数に、「emit」を使って、親コンポーネント側の処理を実行します。

と、前置きが長くなったのですが、上記までが「@Output()」の使い方の基本です。

ある時、ふと思ったんですよね。
あれ?これ、複数の引数を送りたい場合どうするんだ?
と。

モデル(クラス)を利用して複数の値を送る場合

モデル(クラス)を利用すれば、引数としては1つですが、複数の値を送る事が実現できます。
[hoge.ts]
export class HogeHogeModel {
  hogeId: string;
  hoge1: string;
  hoge2: number;
}

[hoge-child.component.ts]
@Component({
  selector: 'koma-hoge-child',
  templateUrl: './hoge-child.component.html'
})
export class HogeChildComponent {
  @Output() formSubmit = new EventEmitter<HogeHogeModel>();

// (中略)

  onClickButton(textValue: string) {
   const hogeObj = new HogeHogeModel();
    hogeObj.hogeId = textValue;
    hogeObj.hoge1 = '名前';
    hogeObj.hoge2 = 1234;
    this.formSubmit.emit(hogeObj);
  }
}

親コンポーネント側(hoge-parent.component.ts)は下記のようになります。
※View(hoge-parent.component.html)は変更不要です。
[hoge-parent.component.ts]
@Component({
  selector: 'koma-hoge-parent',
  templateUrl: './hoge-parent.component.html'
})
export class HogeParentComponent {

// (中略)

  onSubmit(childObj: HogeHogeModel) {
    console.log(childObj.hogeId);
    console.log(childObj.hoge1);
    console.log(childObj.hoge2);
  }
}

記載は省略していますが、モデル(クラス)のimportは必須です。

「モデル(クラス)を送れば解決」でも良いんですが、
わざわざモデル(クラス)を作るのもなぁ・・・・と思う事があるかもしれません。
(個人的にはそれでもモデル(クラス)作るべきだと思いますが)

連想配列をその場で定義し、複数の値を送る場合

ちょっと特殊ですが、もう一つの書き方を紹介します。
子コンポーネント側(hoge-child.component.ts)を以下のように記載します。
[hoge-child.component.ts]
@Component({
  selector: 'koma-hoge-child',
  templateUrl: './hoge-child.component.html'
})
export class HogeChildComponent {
  @Output() formSubmit = new EventEmitter<{ hogeId: string; hoge1: string; hoge2: number }>();

// (中略)

  onClickButton(textValue: string) {
    this.formSubmit.emit({
      hogeId: textValue,
      hoge1: '名前';,
      hoge2: 1234
    });
  }
}

まあ、分かってしまえば、「そりゃそうだよな」と言う書き方なのですが。

続いて、親コンポーネント側(hoge-parent.component.ts)は下記のようになります。
※View(hoge-parent.component.html)は変更不要です。
[hoge-parent.component.ts]
@Component({
  selector: 'koma-hoge-parent',
  templateUrl: './hoge-parent.component.html'
})
export class HogeParentComponent {

// (中略)

  onSubmit(childObj: any) {
    console.log(childObj.hogeId);
    console.log(childObj.hoge1);
    console.log(childObj.hoge2);
  }
}

気を付ける事は引数の型を「any」にする事ぐらいですかね。
型を特定出来ない(any)と言う事は、実行時にエラーになる可能性が高いので、やはりオススメしません。

以上です。今回も内容の割には長くなってしまいました・・・・

では、令和も良いAngularライフを!


2019年4月30日火曜日

Node.js + Express + log4jsで、アクセスログとリクエストログを取る。


オフィス狛 技術部のKoma(Twitterアカウントの中の人&CEO)です。

平成最後の投稿です。

弊社では「Node.js + Express」でAPIを作成する事が多々あるのですが、
その際、アクセスログは別レイヤーで出力して、リクエストログは各業務処理内で出力しています。

今回、それを共通化してまとめたいと思います。
各バージョンは以下の通りです。

node 10.13.0
log4js 3.0.6

※それぞれ、ちょっと前のバージョンになるので、参考にされる方はご注意下さい。

log4js のインストール

まずは log4js のインストールです
npm install log4js

Configファイルの作成

ログの出力レベルなどの設定関連は、別ファイルにしたいので、
「[プロジェクトルート]/src/config」に「log4js.config.json」ファイルを作成します。
ファイルの中身は今回は以下のようにしました。
{
  "appenders": {
    "ConsoleLogAppender": {
      "type": "console"
    },
    "SystemLogAppender": {
      "type": "file",
      "filename": "./log/system.log",
      "maxLogSize": 5000000,
      "backups": 3
    },
    "HttpLogAppender": {
      "type": "dateFile",
      "filename": "./log/http.log",
      "pattern": ".yyyy-MM-dd",
      "daysToKeep": 7
    },
    "AccessLogAppender": {
      "type": "dateFile",
      "filename": "./log/access.log",
      "pattern": ".yyyy-MM-dd",
      "daysToKeep": 7
    }
  },
  "categories": {
    "default": {
      "appenders": ["ConsoleLogAppender"],
      "level": "all"
    },
    "system": {
      "appenders": ["SystemLogAppender"],
      "level": "info"
    },
    "http": {
      "appenders": ["HttpLogAppender"],
      "level": "info"
    },
    "access": {
      "appenders": ["AccessLogAppender"],
      "level": "info"
    }
  }
}

各設定とも「filename」でログの保存場所&ファイル名を指定しています。
「type」によってローテーションが違いますが、
「"type": "file"」の場合は、「maxLogSize」で最大ファイルサイズ(byte)を指定し、
それを超えた場合、「backups」に記載している世代分は保存されます。

「"type": "dateFile"」の場合は、「pattern」に記載した形式(今回は「yyyy-MM-dd」)、つまり日毎にローテーションされ、「daysToKeep」に記載している日付分は保存されます。
詳しくは本家のドキュメントを参照ください。

出力ロジックの記載

続いて、app.jsにログ出力について記載していきます。
const log4js = require('log4js');
const express = require('express');
const cors = require('cors');
const cookieParser = require('cookie-parser');
const bodyParser = require('body-parser');
require('dotenv').config();

// (中略)

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(cors(corsOptions));

// health check
app.use('/health', check);

// ログ設定
log4js.configure('./src/config/log4js.config.json');
const systemLogger = log4js.getLogger('system'); 
const httpLogger = log4js.getLogger('http'); 
const accessLogger = log4js.getLogger('access');
app.use(log4js.connectLogger(accessLogger));
app.use((req, res, next) => {
  if (typeof req === 'undefined' || req === null ||
        typeof req.method === 'undefined' || req.method === null ||
        typeof req.header === 'undefined' || req.header === null) {
    next();
    return;
  }
  if (req.method === 'GET' || req.method === 'DELETE') {
    httpLogger.info(req.query);
  } else {
    httpLogger.info(req.body);
  }
  next();
});
systemLogger.info("App start");

// API Version
const apiVersion = '/api/v2.1.0';

// 各業務API
app.use(`${apiVersion}/hoge/auth`, hogeAuth);
app.use(`${apiVersion}/hoge/master/user`, userMaster);

// (後略)

ポイントは、ログ出力設定をhealth checkの後、そして、業務APIの前に記載している事です。
(今回は、health checkからの接続をログに出力したくなかったので)

ただこれは、health checkのURLへの接続を前もって制限出来ている事が前提となります。
health checkのURLには、特定の信頼している接続元からのみ接続出来る事が分かっているので、ログは不要です、と。

では、ログ設定の部分を見ていきます。

まずは、先程作成した外部の設定ファイルを読み込みます。
log4js.configure('./src/config/log4js.config.json');

続いて、各設定を読み込みます。(configファイルの「categories」に記載した名前を使用します。)
const systemLogger = log4js.getLogger('system'); 
const httpLogger = log4js.getLogger('http'); 
const accessLogger = log4js.getLogger('access');

続いて、下記の記載で、Expressへのアクセスをログに出力することが可能になります。
app.use(log4js.connectLogger(accessLogger));

続いて、下記の記載で、接続時のリクエスト値をログに出力することが可能になります。
app.use((req, res, next) => {
  if (typeof req === 'undefined' || req === null ||
        typeof req.method === 'undefined' || req.method === null ||
        typeof req.header === 'undefined' || req.header === null) {
    next();
    return;
  }
  if (req.method === 'GET' || req.method === 'DELETE') {
    httpLogger.info(req.query);
  } else {
    httpLogger.info(req.body);
  }
  next();
});

当APIプロジェクトはRESTになっているので、
GETとDELETEはqueryで、それ以外(POST、PUT)は、bodyで送られてくる、という想定になっています。

※「next()」を忘れないようにして下さい。
これが無いと、そこで処理が終了してしまい、以降の業務API側に値が行き渡りません。


そして最後に
systemLogger.info("App start");
これは、単純にExpressを起動した時に一度だけ出力されます。

これで、Expressを起動するとログが出力されます。

今回は、設定ファイル(log4js.config.json)で「"filename": "./log/xxxx.log",」と記載しているので、 app.jsが格納しているディレクトリにlogディレクトリが作成され、その中に、ログが格納されます。
こんなイメージです。

ちなみに、今回は説明を省きましたが、
業務側で500エラーになった場合も、app.js内で最終的にcatchし、systemLoggerでエラー内容(スタックトレース)をシステムログに出力しています。

以上です。

平成最後の投稿、内容の割に長くなってしまいました。

それでは、また令和でお会いしましょう!


, ,

2019年4月26日金曜日

Amazon ConnectとLambdaで電話をかける ①電話番号取得、お問い合わせフローの作成


こんにちは、オフィス狛 モバイル開発担当 Aika-yuy です。
今回の投稿は、 AmazonConnectとLambdaで任意の番号に電話をかける方法をご紹介します。









AmazonConnectはコールセンターをメインとして、簡易に作成できるというものなので、
受電する記事をよく見かけますが、架電方法の記事は少なかったので記事にしました。




難しそうに見えますが、簡単で短時間で作成できますので試してみてください。
こちらの4ステップで紹介していきます。

①Amazon Connectで電話番号、お問い合わせフローの作成
②IAMでロールを作成← 今回はこちら
③Lambda関数の作成
④実行!!


①Amazon Connectでお問い合わせフローの作成

AmazonConnect登録後、ダッシュボードまでたどり着くことができたら、下の動画のようにお問い合わせフローを作成してください。



左のバーからもう一度ダッシュボードまで戻り、電話番号の取得、お問い合わせフローの選択をしてください。




意外と簡単に作成できましたね。

次回は②IAMでロールを作成を書いていきます。


, ,

2019年4月25日木曜日

ASP.NET MVCでファイルダウンロード後にViewが表示できないときの対処方法。


オフィス狛 技術部のJoeです。

最近、ASP.NET MVCで開発されたWebシステムの改修プロジェクトをお手伝いしたのですが、ASP.NET MVCでの開発が初めてだったので、色々と戸惑いが多かったです。
その中でも、あるエラーについての対処方法をご紹介したいと思います。

今回のプロジェクトでは、ControllerからViewを表示したり、別のアクションを起こす場合は、主に下記のActionResultオブジェクトを使っていました。

① 呼び出し元のViewを表示する

return View();

② 指定したViewを表示する

return View("~/Views/Test/index.cshtml");

③ 指定したControllerのアクションを実行する

return RedirectToAction(MethodName.Index, ControllerName.Test);

ところが、ファイルをダウンロードした後に上記のActionResultオブジェクトを使用すると、Exceptionが発生してしまいました。

[ C#(Response.WriteFileメソッドを使用してサーバ上のExcelファイルをダウンロード) ]
// Excelファイルをダウンロード
var DownloadFile = Server.MapPath("~/Reports/Download.xlsx");
long offset = 0;
long size = new FileInfo(DownloadFile).Length;
Response.Clear();
Response.ContentEncoding = Encoding.GetEncoding("UTF-8");
Response.ContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";  // これはExcel
Response.AppendHeader("content-disposition", "attachment; filename=" + Server.UrlEncode("ダウンロード.xlsx"));
Response.WriteFile(DownloadFile, offset, size);
Response.End();

// 呼び出し元のViewを表示する(①のパターン)
return View();

[ エラー内容 ]
HTTP ヘッダーの送信後にサーバーでヘッダーを追加できません。

②でも同じエラーになります。

実は今回、CSRF対策としてView側で「Html.AntiForgeryToken」ヘルパーを呼び出していました。
ファイルをダウンロードしているので、「Response.End()」の時点でクライアントにHTTPヘッダーが送信されています。
「return View(…)」でViewを返した際に、View側で「Html.AntiForgeryToken」ヘルパーがCookieを設定しようとして、エラーになってしまいました。

では③「RedirectToAction」を試してみると

[ エラー内容 ]
HTTP ヘッダーを送信後はリダイレクトできません。

こちらはエラーのとおり、リダイレクトができないようです。


【対処方法1】何も返さない
何も返さなければいいということで、以下に修正してExceptionを回避できました。
return new EmptyResult();

ちなみに以下でも同じ動きです。MVCが判断して「return new EmptyResult();」を返してくれます。
return null;

【対処方法2】ダウンロードするファイルを返す
こちらは「return File」でファイルを返します。(「Response.WriteFileメソッド」を使用しません)
なんだかスッキリしてますし、ダウンロード後に何か処理が無ければこちらのほうが良いですね。
[ C# ]
var DownloadFile = Server.MapPath("~/Reports/ManuallyRecord.xlsx");
var contentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";  // Excel
return File(DownloadFile, contentType, Server.UrlEncode("ダウンロード.xlsx"));

ActionResultオブジェクトだけでもいろいろなパターンがあって、まだまだ覚えることがたくさんありそうです。


,

2019年4月24日水曜日

C#(ADO.NET)でIN句にSqlParameterをわたす方法


オフィス狛 技術部のHammarです。

SQLでIN句を使ってデータを取得するというのはよくある取得条件ですが、実際のプログラムで実装する場合には、SQLインジェクション対策といったセキュリティの観点から、IN句の中はプレースホルダを使って動的に値を受け渡したいという場面があると思います。

基本的にはIN句の中を作成し、そのままパラメータとして渡すという方法でいけるのですが、C#(ADO.NET)を使って同様の実装をすると、意図したデータが取得できずにうまくいかずハマってしまいましたので、今回はその対応方法を書きたいと思います。

DB環境としてSQLServerを対象にしています。
まず通常のIN句をそのままパラメータで渡す方法として、下記のように記述します。
// 配列データ
string[] array = new string[3];
array[0] = "name1";
array[1] = "name2";
array[2] = "name3";

// DB接続
string connect = 【DB接続内容】

// 取得SQL
string query =
   "SELECT * FROM TestTable "
       + "WHERE Name IN (@param) ";
// IN句作成
string paramValue = string.Join(",", array);

using (SqlConnection connection = new SqlConnection(connect))
{
   SqlCommand command = new SqlCommand(query, connection);
   // パラメータセット
   command.Parameters.Add("@param", SqlDbType.VarChar).Value = paramValue; 
   connection.Open();
   // SQL実行
   SqlDataReader reader = command.ExecuteReader();
   while (reader.Read())
   {
       Console.WriteLine("\t{0}\t{1}\t{2}", reader[0], reader[1], reader[2]);
   }
   connection.Close();
}

上記を実行すると、結果は何も取得できません。 どうやらSqlParameterでわたす値は1つの値と見なされるなしく、IN句のカンマ区切りで渡した内容も1つの値となるため、検索条件がおかしくなり意図しない結果として返ってくるようです。

これを知らなかったので、いくらIN句の値をいろいろ変えてやってみたんですが、全く値が取れずにかなりハマりました。

結果「SqlParameterでわたす値は1つの値と見なされる」ということなので、IN句の中で渡すパラメータを1つずつ設定してあげないといけないようです。 SQLはこんな感じになります。

string query =
   "SELECT * FROM TestTable "
       + "WHERE Name IN (@param1, @param2, @param3) ";

当然IN句の中は動的に変える必要があるので、@param自体も動的に作成する必要があるということになります。
で、最終的にはこのような感じになりました。

// DB接続
string connect = 【DB接続内容】
// 取得SQL作成
string query =
   "SELECT * FROM TestTable WHERE Name IN ( ";
for (int i = 0; i < array.Count(); i++)
{
   if (i < (array.Count() - 1))
   {
       query += "@param" + i + ", ";
   }
   else
   {
       query += "@param" + i;
   }
}
query += ")";

using (SqlConnection connection = new SqlConnection(connect))
{
   SqlCommand command = new SqlCommand(query, connection);
   string[] paramNames = new string[array.Count()];
   // パラメータ作成
   for (int j = 0; j < array.Count(); j++)
   {
       paramNames[j] = "@param" + j;
       command.Parameters.Add(paramNames[j], SqlDbType.VarChar).Value = array[j];
   }
   connection.Open();
   // SQL実行
   SqlDataReader reader = command.ExecuteReader();
   while (reader.Read())
   {
       Console.WriteLine("\t{0}\t{1}\t{2}", reader[0], reader[1], reader[2]);
   }
   connection.Close();
}

上記のようにSQLとパラメータのIN句部分をそれぞれ動的に生成することで対応します。

結局スマートな書き方というか、そういう書き方ではなく、どちらかというとまあ当然の書き方というか、IN句をループして動的処理を自作しなければいけないので、ちょっときれいな感じではないですが、IN句はよく使われると思うのでご参考になれば。

ちなみにいろいろ調べてみると、LIKE句をうまく使ってやることもできるようで自分も試してみたのですが、なぜか最初の1件だけしか取得できずうまくいかなかったので、今回のようなスタンダードな書き方に落ち着きました。

2019年3月30日土曜日

Storybordで制約を付け直す(Xcode、iOS)


こんにちは、オフィス狛 モバイル開発担当 Aika-yuy です。


viewを動的に消したり、表示したり、大きさを変更したいときなどに制約も修正しなければいけないと思います。

そんな時にコードで修正するのが面倒。。なんてことはないでしょうか。
そこでstorybordで簡単に制約を付け直す方法をご紹介します。
コードでの修正はとても少なく、使いやすいです。

まず、3つのviewを用意し、間を均等に60空ける制約をつけます。

今回は、動的にyellowViewを消す場合をやってみます。

黄色のviewを消してみました。
黄色のviewがなくなっても、赤と青のviewの位置はそのままです。

それでは、変更したい制約をstorybordで作ります。


redViewのbottomとblueViewのtopの間を60空ける制約をつけました。
この時制約同士が、ぶつかってしまうので後からつけた制約を無効にしなければなりません。
写真では、無効にしてあるので、色が薄くなっています。こうなっていればOKです。


上の写真のように、installedのチェックを外すだけです!!

それができたら、viewControllerに接続します。


後は消したいタイミングで .isActiveをtrueにするだけです。

と言いたいところですが、、、、、
制約の変更はスレッドセーフではない為、メインスレッドで実行しないと反映されません。
DispatchQueue.main.async {}で囲みましょう。

























うまくできました!!!

class ViewController: UIViewController {

    @IBOutlet weak var redView: UIView!
    @IBOutlet weak var yellowView: UIView!
    @IBOutlet weak var blueView: UIView!
    @IBOutlet weak var changeConstraint: NSLayoutConstraint!
    @IBOutlet weak var blueViewConstraint: NSLayoutConstraint!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.yellowView.isHidden = true
        
        DispatchQueue.main.async {
            self.blueViewConstraint.isActive = false
            self.changeConstraint.isActive = true
        }
    }
}



2019年3月29日金曜日

Illustratorのワークスペースでの色と書き出した時の色が違う時はカラー設定のせいかもしれません。


こんにちは、オフィス狛 デザイン部のSatoです。

Illustratorで画像を書き出した時に「あれ?きちんとドキュメントのカラーモードをRGBに設定していたのにIllustrator上での色と書き出した時の色が違う……?」と思った経験がありませんか?
そんな時に見直すべきだなと思うのがカラー設定です。

Illustratorを使っている方ならだいたい知っている基礎的な話なのですが、新規ファイルを作成するとカラー設定が戻ってしまうバグで私自身焦ったので今後似たようなバグがあった時用に記載しておこうと思います。
https://forums.adobe.com/docs/DOC-9568←私の環境で起こった現象はこのバグに近いですがちょっと違うようでした)


Illustratorカラー設定を確認するにはまず、メニューバーの「編集」のカラー設定をクリックして出てくるカラー設定の設定セレクトボックスを「Web ・インターネット用-日本」に変更すればweb用に書き出した際の色で作業できるようになります!簡単!

初期設定である「Adobe Illustrator5.5をエミュート」に設定されてる場合のワークスペースと「Web ・インターネット用-日本」に設定されてる際のワークスペースの色の違いはこんな感じです。
「Adobe Illustrator5.5をエミュート」での見え方はなんだか薄いです。

ちなみにですが、私がカラー設定が「Adobe Illustrator5.5をエミュート」になっていると気づいたのは作業中のワークスペースの色がなんだかおかしいので色の校正を間違って設定したのかと思いメニューを確認したところ、「色の校正」が使えない状態になっており、調べたところカラー設定が「Adobe Illustrator5.5をエミュート」になっていると使えないという情報が出てきたからです。

調べたところ、数ヶ月前から起こっているバグなのに現在もまだ修正されていないようですね……。
もし色がおかしいな?と思ったら「カラー設定」を見直してみてください。


2019年3月28日木曜日

MySQLでAUTO INCREMENTの値を取得したい。


オフィス狛 技術部のJoeです。

INSERT後にAUTO INCREMENTで自動採番された値を使用したいことがあるかと思いますが、
MySQLでは前回INSERTのAUTO INCREMENT値を「LAST_INSERT_ID()」関数で取得できます。

テーブルを作成して、1件INSERTすると「1」が取れました。
mysql> CREATE TABLE test (
    -> id   INT         NOT NULL PRIMARY KEY AUTO_INCREMENT,
    -> name VARCHAR(10) NOT NULL );
mysql> INSERT INTO test ( name ) VALUES ( '111' );
mysql> SELECT LAST_INSERT_ID();
+------------------+
| LAST_INSERT_ID() |
+------------------+
|                1 |
+------------------+

AUTO INCREMENTは一意であることを保証してくれますので、各セッション内で最後にINSERTした値が取れました。
【セッションA】INSERT INTO test ( name ) VALUES ( '111' );
【セッションB】INSERT INTO test ( name ) VALUES ( '222' );
【セッションB】SELECT LAST_INSERT_ID();
+------------------+
| LAST_INSERT_ID() |
+------------------+
|                2 |
+------------------+
【セッションA】SELECT LAST_INSERT_ID();
+------------------+
| LAST_INSERT_ID() |
+------------------+
|                1 |
+------------------+

と、便利な関数ですが、想定する値が取れないパターンも併せてをご紹介します。

●AUTO INCREMENTのカラムに値を指定してINSERTした場合
値を指定してしまうと取得できません。同一セッションで前回自動採番された値が取れるようです。
mysql> INSERT INTO test ( name ) VALUES ( '111' ); -- 値を指定しない(自動採番)
mysql> INSERT INTO test ( id, name ) VALUES ( 10, '222' ); -- 値を指定する
mysql> SELECT LAST_INSERT_ID();
+------------------+
| LAST_INSERT_ID() |
+------------------+
|                1 |
+------------------+

●単一のINSERT文で複数データをINSERTした場合
3件INSERTしたので「3」が欲しいのですが、「1」が取れました。
mysql> INSERT INTO test ( name ) VALUES ( '111' ), ( '222' ), ( '333' );
mysql> SELECT LAST_INSERT_ID();
+------------------+
| LAST_INSERT_ID() |
+------------------+
|                1 |
+------------------+

●別セッションの場合
「当然!」と言われそうですが、別セッションでは取得できません。
下記のようなDB接続から開始するAUTO INCREMENT値の取得処理みたいなものを作ってしまうと、想定した値が取れませんでした。
(MySQLへの接続は「MySql.Data.MySqlClient」を使っています)

[ C#でMySQL接続してAUTO INCREMENT値を取得(ダメな例) ]
string conString= "Database=test;Data Source=localhost;User Id=test;Password=test";
public void Index()
{
    using (MySqlConnection con = new MySqlConnection(conString))
    {
        // DB接続→INSERT
        con.Open();
        MySqlCommand cmd = con.CreateCommand();
        MySqlTransaction tran;
        tran = con.BeginTransaction();
        cmd.Connection = con;
        cmd.Transaction = tran;
        try
        {
            cmd.CommandText = "INSERT INTO test (name) VALUES ('1111')";
            cmd.ExecuteNonQuery();
            tran.Commit();
            ShowAutoIncrementId();  // AutoIncrementの値を表示
        }
        catch (Exception)
        {
            tran.Rollback();
        }
    }
}

public void ShowAutoIncrementId()
{
    using (MySqlConnection con = new MySqlConnection(conString))
    {
        // DB接続→AutoIncrementIdを取得して表示する
        con.Open();
        MySqlCommand cmd = con.CreateCommand();
        cmd.Connection = con;
        cmd.CommandText = "SELECT LAST_INSERT_ID()";
        object id = cmd.ExecuteScalar();
        System.Diagnostics.Debug.WriteLine("***** ID : " + id);
        return;
    }
}

【1回目の実行】
INSERTとSELECTが別セッション(428と429)になってしまったので取得できませんでした。
■結果
***** ID : 0
■MySQLログ
2019-03-06T07:39:31.579862Z 428 Query INSERT INTO test (name) VALUES ('111')
2019-03-06T07:39:31.582475Z 428 Query COMMIT
2019-03-06T07:39:31.660854Z 429 Connect test@localhost on test using SSL/TLS
2019-03-06T07:39:31.668533Z 429 Init DB test
2019-03-06T07:39:31.668751Z 429 Query SELECT LAST_INSERT_ID()

【2回目の実行】
なぜか「1」が取れました!?
コネクションプールで、1回目のINSERTと2回目のSELECTのセッションIDが同じ(428)になると、前回INSERTのIDが取得できてしまうようです。
■結果
***** ID : 1
■MySQLログ
2019-03-06T07:39:40.390591Z 429 Query INSERT INTO test (name) VALUES ('111')
2019-03-06T07:39:40.393070Z 429 Query COMMIT
2019-03-06T07:39:40.469436Z 428 Init DB test
2019-03-06T07:39:40.469689Z 428 Query SELECT LAST_INSERT_ID()


AUTO INCREMENTの値は、主キーや外部キーなどで使用することがあるかと思いますので、
「LAST_INSERT_ID()」関数を使用される際はご注意ください。

2019年3月27日水曜日

踏み台サーバー(EC2)経由でSQLServer(RDS)にSSH接続する方法(ポートフォワーディング)・Windows編


オフィス狛 技術部のHammarです。

最近はAWSで環境を作って開発する機会が多いですが、ここでセキュリティ的な部分や運用的な観点としてよく踏み台サーバー経由で目的のサーバーに接続するということがあると思います。
今回はその「踏み台サーバー(EC2)経由でSQLServer(RDS)に接続する」という目的で、接続したSQLServerをSQL Server Management Studio (SSMS) で操作したいと思います。

■利用環境

Windows(ローカルPC)
AWS ES2(踏み台)
AWS RDS(SQL Server)

■利用ツール

SQL Server Management Studio
teraterm

今回の目的を達成するには、「SSHポートフォワーディング」というテクニックを利用します。
ポートフォワーディングとは、SSHによって確立した通信経路を利用して、クライアントのポートを、クライアントが直接アクセスできないサーバのポートに転送してくれる仕組みを指します。

これを利用して今回の手順は簡単に書くと下記のようになります。
   ①踏み台サーバーにSSH接続
   ②踏み台サーバーからSQL Serverにポート転送(ポートフォワーディング)
   ③ポートフォワーディングした状態でSQLServerにログインしDB操作


では上記手順を踏まえて、具体的な手順を書いていきます。

1.teratermでポートフォワード設定する

よくSSH接続につかわれるツールで有名なteratermには、デフォルトでポートフォワード設定機能がついていて、これを利用します。

①teratermを起動し、メニューバーの「接続」→「SSH転送」を選択
②ポート転送画面で「追加」ボタンを押下

③ポート転送を行う向きの選択で「ローカルのポート」にチェックを入れ下記のように設定します
   ローカルのポート:11433(何でもよい)
   リモート側ホスト:接続するRDSのエンドポイント
   ポート:1143

④OKを選択する


2.teratermで踏み台にSSH接続する

次に踏み台にSSH接続するのですが、このとき1で設定したポートフォワード機能が有効になり、踏み台に接続したと同時に目的のサーバーにも接続されることになります。(トンネルを掘った状態にするともいいます)

①teratermメニューバーの「ファイル」→「新しい接続」で下記のように設定します
   ホスト:EC2のエンドポイント
   TCPポート:22
   サービス:SSHにチェック

②SSH認証画面でEC2へ接続するための認証情報を設定する
  ※基本ユーザー名と鍵設定で行けると思います

③OKを選択してSSH接続する


3.SSMSでログインする

1、2まででポートフォワーディングは完了しているので、この接続をつなげたままであとはツール(今回はSSMS)で直接SQLServerにつなげるだけです。
接続設定は下記のように設定します。
   サーバー名:127.0.0.1,11433
   認証:SQL Server認証
   ログイン:ルート権限ユーザー名
   パスワード:設定したパスワード

サーバー名は[127.0.0.1]のローカルホストを指定し、カンマでポート指定できますので、先ほど1で設定したローカルのポート(11433)をここに設定することで、 ローカルから踏み台サーバー経由、ポートフォワーディングでSQL Server接続が可能になります。

あとはSSMSで操作が可能なので、ツール内でデータベース作ったりテーブル作ったりが簡単にできます。


今回のポートフォワーディングを利用することで、もちろんSQL Server以外でも接続可能ですし、いろいろ応用できると思いますので、ぜひ参考にしてみてください。


, , , , ,