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接続する方法(ポートフォワーディング)


オフィス狛 技術部の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以外でも接続可能ですし、いろいろ応用できると思いますので、ぜひ参考にしてみてください。


, , , ,

2019年2月28日木曜日

SpringBoot 1.3 から 1.5 へのバージョンアップでハマったこと その2


オフィス狛 技術部のJoeです。
今回が初の投稿になります。どうぞよろしくお願いします。

以前、SpringBoot1.31から1.51にバージョンアップを記事にしましたが、その影響がさらに出てきました。
今回は、バージョンアップが原因で発生した事象を追加でご紹介します。

今回の事象が発生したのはファイルのアップロードです。 数MB程度のファイルをmultipart/form-data形式でアップロードしたところ、下記のエラーが出ました。

org.apache.tomcat.util.http.fileupload.FileUploadBase$FileSizeLimitExceededException: The field [xxxxx] exceeds its maximum permitted size of 1048576 bytes.

「最大サイズの1MBを超えています」だそうです。
エラーに出ているファイルサイズ(1MB)はSpringbootのデフォルトのサイズです。

最大サイズはapplication.ymlに定義しているはずなので確認しました。

multipart:
    maxFileSize: 100MB
    maxRequestSize: 100MB

確かに最大100MBと定義しています。
では、何故1MBになってしまうのか・・・・・・
調べてみたところ、バージョンアップによりプロパティーのキー名が変更されていました。

【SpringBoot 1.5】での指定方法
spring:
    http:
        multipart:
            max-file-size: 100MB
            max-request-size: 100MB

デフォルトでは、上記の設定はapplication.ymlには存在しないので、デフォルトのサイズ(1MB)になっていたのですね。
とりあえず、上記のキー名に修正し、無事にアップロードすることができました。

ちなみに、今回のアップロードは1ファイルなので同じ値を設定していますが、
複数ファイルの場合、「max-request-size」はファイル数に応じて変更です。
・max-file-size :1ファイルの最大バイト数
・max-request-size:1リクエストで複数ファイルの最大バイト数

さらに調べてみると、SpringBoot 2.0では、下記の設定方法に変更されていました。

【SpringBoot 2.0】での指定方法
spring:
    servlet:
        multipart:
            max-file-size: 100MB
            max-request-size: 100MB

今回ご紹介した以外にも、バージョンアップでキー名が変更されているものがあるようなので、対応の際は調査が必要ですね。


,

2019年2月27日水曜日

Angular angular.json の scripts は記載する順番が大事、と言う話(bootstrap、popper)


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

Angular の angular.json には、外部のJavaScriptの読み込む為に、「scripts」と言う項目が存在しますが、そこに記載する際は、順番が大事だよ、と言う話です。
まあ、Angularに限った事じゃ無いのですが、毎回ハマるので、備忘録で記載します。

とある新規プロジェクトで、デザイナーが作ったhtmlをせっせとAngularプロジェクトに移植していたのですが、ポップアップ系の表示がエラーになってしまいました。
zone.js:192 Uncaught TypeError: Bootstrap dropdown require Popper.js (https://popper.js.org)
    at c.t.toggle (bootstrap.min.js:6)
    at HTMLAnchorElement. (bootstrap.min.js:6)
    at Function.each (jquery.min.js:2)
    at w.fn.init.each (jquery.min.js:2)
    at w.fn.init.c._jQueryInterface [as dropdown] (bootstrap.min.js:6)
    at HTMLAnchorElement. (bootstrap.min.js:6)
    at HTMLDocument.dispatch (jquery.min.js:2)
    at HTMLDocument.y.handle (jquery.min.js:2)
    at ZoneDelegate.push../node_modules/zone.js/dist/zone.js.ZoneDelegate.invokeTask (zone.js:421)
    at Zone.push../node_modules/zone.js/dist/zone.js.Zone.runTask (zone.js:188)

「Bootstrap dropdown require Popper.js」なので、
ああ、「bootstrap.min.js」が「Popper.js」を必要としているのね、と言う事で、まずはインストールしました。
npm install popper.js --save

その後、angular.jsonに「Popper.js」追加しました。
    "scripts": [
        "node_modules/jquery/dist/jquery.min.js",
        "node_modules/bootstrap/dist/js/bootstrap.min.js",
        "node_modules/popper.js/dist/umd/popper.min.js"
    ]
既に、「bootstrap.min.js」は追加していあったので、その直後に追記しました。
(これが良くなかった・・・・)

その後も一向にエラーが消えることがありません。
少し考えた後、「これ、もしかして順番かな?」と思い、
    "scripts": [
        "node_modules/jquery/dist/jquery.min.js",
        "node_modules/popper.js/dist/umd/popper.min.js",
        "node_modules/bootstrap/dist/js/bootstrap.min.js"
    ]

順番を入れ替えたらエラーも消えました。
いやー、どハマりする前に気付けて良かったです。

何事も読み込む順番には気を付けようね、と言う話でした。
あまり、Angular関係無かったですね。
次回はもっとAngularっぽい機能を書いていこうと思います。

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


2019年2月26日火曜日

node.jsでインストールしたモジュール(knex、mssql)のバージョンによる問題


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

今回はnode.jsのpackage.jsonに記述したモジュールのインストールバージョンによってハマった事象を書こうと思います。 

環境構築時にDBのテーブルを作成したり、テストデータを登録したりすると思いますが、今回そのテーブル作成(migrate)とテストデータ登録(seed)作業をnode.jsとknex.jsというSQL Query Builderのモジュールで実行させます。
また開発の都合上DBはSQL Serverを利用するので、SQLServerに接続するmssqlというモジュールも利用します。 

■実装環境

・node.js(Express)
・knex、mssqlモジュールを利用 

■npmインストール

まずそれぞれのモジュールの最新バージョン(knex:0.16.3 、mssql:4.3.0)をインストールして使おうとしましたが、そもそもknexとmssqlの最新バージョン互換対応がしておらず、怒られます。
Error: This knex version does not support any other mssql version except 4.1.0 (knex patches bug in its implementation)
このバージョン(knex:0.15.2 、mssql:4.1.0)が最終的に対応しているとのことで、バージョンを指定しインストールします。
 

■テーブル作成(migrate)、テストデータ登録(seed)

次にテーブル作成(migrate)とテストデータ登録(seed)の処理を作成します。
テーブル作成(migrate)ではknexをつかったテーブルcreateを、テーブル毎のファイル別に作成し、その各ファイルを読み込んで実行するというような基本的な流れでテーブルを作成します。
データ登録(seed)も基本的には同じで、insert文が書かれた各ファイルを読み込む感じです。 

このmigrate処理を下記のような感じで記述して実行したのですが
// テーブルcreateファイルの読み込み
const createTableA= require('./createTableA');
const createTableB= require('./createTableB');
const createTableC= require('./createTableC');
const migrationOrder = [
  createTableA,
  createTableB,
  createTableC,
];

// 実行
exports.up = async knex => {
  const results = migrationOrder.map(migration => migration.up(knex));
  return Promise.all(results);
};

 なぜか「Can't acquire connection for the request. There is another request in progress.」というエラーがSQLServerから返ってきてしまいテーブルが作成できませんでした。
記述的には特におかしなところはなさそうなので、なぜうまくいかないのかかなりハマりました。
いろいろ調べていくとmssqlのバージョン4系では単一のトランザクション内で複数のクエリ実行はエラーとなるバグ?のようらしくこのときpromise.allは機能しないようです。
https://github.com/tediousjs/node-mssql/issues/491
https://github.com/Vincit/objection.js/issues/671 

■回避方法

1.mssql、knexのバージョンを下げる

mssqlのバージョンを単純に下げると今度はknexのバージョン互換の関係でつかえなくなるので、
  knex: 0.13.0
  mssql: 3.3.0
が現在の記述でうまくいくバージョンの組み合わせとなるようです。

ただ、やはり最新ではないですが新しいバージョンで開発したほうが今後のためにも良いとはおもうので、できるだけバージョンは下げたくないなーという思いで、個人的にはこの回避方法はやめました。

2.for文を使う

GitHubにも対応策として書かれていた下記のように記述しました。
exports.up = async knex => {
  const results = [];
  for (const migration of migrationOrder) {
    results.push(await migration.up(knex));
  }
  return results;
};

promise.allをつかわずに上記のようにfor文でテーブル作成を行うとうまくいきました。
ただ自分の開発環境ではこれにも1つ問題点がありました。 

ESlintをインストールしていて、for文の記述がこのESlintのチェックにひっかかってしまいました。
error   iterators/generators require regenerator-runtime, which is too heavyweight for this guide to allow them. Separately, loops should be avoided in favor of array iterations no-restricted-syntax

現在の開発環境ではgitのcommit時にESlintチェックしていて、もしチェックに引っかかる場合はcommitできないようにしているのでこのままではcommitできません。
うーんこれは困った。。。
このチェックだけESlintから外したりすることも可能ですが、それだと今後開発のときのチェックとしてはすべて外れてしまうしー、
ということで、一旦ここの記述だけをチェックしないようにする、というちょっと強引な感じでひとまず切り抜けることにしました。

記述はこんな感じです。
exports.up = async knex => {
/* eslint-disable */
  const results = [];
  for (const migration of migrationOrder) {
    results.push(await migration.up(knex));
  }
  return results;
/* eslint-enable */
};

れでこの記述だけはチェック除外となるので、ひとまずは切り抜けられました。
データ登録(seed)も同じようなながれで記述することで回避できます。

結局やってみるとちょっと強引な形なので、回避方法1にしてもいい気もしますが、ご参考までに。

ということで、まだ他にもこのようなモジュールのバージョンでなにか事象があるかと思いますが、ハマり次第記載していけたらと思います。

, ,

2019年2月22日金曜日

StackViewの優先度 Content Hugging PriorityとContent Compression Resistance Priority


こんにちは、オフィス狛 モバイル開発担当 Aika-yuy です。
今回の投稿は、 StackViewにつけるAutoLayoutの優先度について書いてみたいと思います。


サイズが曖昧だと怒られる

動的なサイズ変更や、デバイスサイズにも柔軟に対応してくれるStackViewですが、サイズが曖昧だと怒られることがあります。固定サイズを指定するという方法もありますが、iPad対応で文字のサイズを動的に変更したい場合は、あらかじめ優先度を設定しましょう。

まず、StackViewに2つのLabelを配置し、StackViewのサイド20、Y軸の中心になるように制約をつけます。











そうすると、こんなエラーが出ます。


Add missing constraints for "Stack View"? This will add enough constraints to resolve the ambiguity






『どっちをLabelサイズを優先すればいいかわからないよーー 』ということです。
こまちゃんのLabelの優先度を低くするようにアドバイスしてくれています。

優先度を指定する

優先度を変更するときはAutoLayoutで設定できます。
優先度は、高くしたい方のpriorityを高くするのではなく、低くしたい方のpriorityを低くするようです。

いろんな解釈の仕方があると思いますが、私が覚えやすい方法使い分けをで書いてみました。

・Content Hugging Priority・・・・・・・・・・  子Viewが親Viewより小さい時 

・Content Compression Resistance Priority・・・  子Viewが親Viewより大きい時 


子Viewが小さい時

2つのLabelどちらともの文章が短い時、どちらのサイズを大きくしてViewいっぱいに表示するかを指定しなければいけません。
その場合は、優先順位が低い方の(固有のサイズを保持しなくてもいい方、大きくなってもいい方)
Content Hugging Priorityを1小さく設定します。

下の画像では、nameは動的にサイズが変更する予定がないので元々のサイズを維持。こまちゃん部分は動的にサイズ変更したいというような場合です。
こまちゃんのLabelのContent Hugging Priorityを下げることにより、長くなってしまった場合省略表示され、nameのラベルを親Viewから押し出すこともなくなります。











子Viewのどちらかが大きい時

2つのLabel合わせたサイズが親Viewよりも大きくなってしまう場合、どちらを優先して表示するか指定します。
省略して表示してもいい方の優先度を1小さくします。










子Viewどちらとも親Viewより大きい時(どちらかを優先すると、どちらかが消えてしまう場合)

この場合は、設定次第ですがどちらも表示したい場合は親のStackViewの比率から固定サイズで指定した方がいいかもしれません。

AutoLayoutマスターになるべく、少しづつブログも更新していけるよう頑張ります。



,

2019年1月31日木曜日

gitマージツールにWinmergeを使う方法。

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

開発するとき、特に複数人で開発をされる場合は、gitを使われている方が多いと思います。その際に皆さんはマージツールやdiffツールに何を使っていますでしょうか?
私は開発はwindowsを使うことがほとんどなので、WinMergeというマージツールをよく使っています。なので、これをgitのマージツールとして設定していますが、今回はこのgitのマージツールの設定方法について書いてみたいと思います。

ちなみに自分のgitクライアントツールですが、自分はGitExtensionsを使っています。GitExtensionsはデフォルトでkdiffというマージツールが使えるようになっていますが、個人的には使いづらくて、マージツールの設定方法を変えられてうれしかった記憶があります。

設定はプロジェクトにある「.git」というディレクトリ内の「config」ファイルに下記の記述をそのまま追加して保存するだけでOKです。※.gitは隠しファイルとなっています

■config

[diff]
    tool = WinMerge
[difftool "WinMerge"]
    path = C:/Program Files/WinMerge/WinMergeU.exe
    cmd = \"C:/Program Files/WinMerge/WinMergeU.exe\" -f \"*.*\" -e -u -r \"$LOCAL\" \"$REMOTE\"
[merge]
    tool = WinMerge
[mergetool "WinMerge"]
    path = C:/Program Files/WinMerge/WinMergeU.exe
    cmd = \"C:/Program Files/WinMerge/WinMergeU.exe\" \"$MERGED\"

ひとまず上記で最低限の設定となりますが、この記述はいろいろとカスタマイズ可能なので、自分用に見やすくしたり、3画面構成にしたりとかcmdの記述部分でいろいろとオプション設定も可能なので、もっと細かく設定してとことん使いやすくするのもありだと思います。

もちろんクライアントツールで設定を変えることも可能ですが、ツール毎に設定画面も違いますし、この方法はconfigファイルに記述するだけなのでGitExtensions以外のツールでも同じやり方で使えるようになります。

とりあえず細かな設定は置いといてサクッとgitでWinMergeを使いたいときにはぜひご活用ください。


Angular Templateでリストのフィルタを行う。


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

今年最初のブログ、何とか1月中に出来ました。(ギリギリ)
今年最初はAngularネタで行きたいと思います。

AngularのTemplate(html)でリストのデータを表示する際、
<ng-container *ngFor="let item of testList>
  <div class="test_class">{{ item.name }}</div>
</ng-container>
こんな感じの書き方をすると思います。
この時、「リスト内のデータをフィルタしてから表示したいんだよなー」と思う時がありませんか?
ありません?ないかな・・・・まあ、私はあります。

というわけで、こんな時は「Pipe」を使います。
以前、『AngularのPipeを使ってhtml(View)の表示を共通化する。』をブログに書きましたが、あの「Pipe」です。

では、いつものようにAngular CLIで作成します。コマンドは、
ng g pipe [path/name]
です。今回は、「list-filter」という名前で作ります。
$ng g pipe shared/list-filter
CREATE src/app/shared/list-filter.pipe.spec.ts (204 bytes)
CREATE src/app/shared/list-filter.pipe.ts (209 bytes)
UPDATE src/app/shared/shared.module.ts (711 bytes)

すると、以下のようなファイルが生成されます。
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'listFilter'
})
export class ListFilterPipe implements PipeTransform {
  transform(value: string, args?: any): any {
    return null;
  }
}

これを以下のように変更します。
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'filter'
})
export class ListFilterPipe implements PipeTransform {
  transform(items: any[], col: string, value: string): any {
    if (col.length === 0 || value.length === 0) {
      return items;
    }
    return items.filter(x => x[col] === value);
  }
}
Pipeの名前は分かりやすく「filter」にしています。

そして、transformの引数を以下のように変更しました。
第1引数:items: any[] → 対象のリスト
第2引数:col: string → フィルタを行う対象の項目名
第3引数:value: string → フィルタする値

実際は色々作り込むのですが、とりあえず、今回は、
フィルタする項目・値が無かったら、リストをそのまま返すようにして、
if (col.length === 0 || value.length === 0) {
  return items;
}
最後にリストのフィルタした結果を返しています。
return items.filter(x => x[col] === value);
では、実際にこのPipeを使ってみましょう。

リストの中身はこんな感じだとします。(分かりやすくする為に、json形式で書いています。)
{"hoge_list":[
  {"id":"1","name":"テスト1"},
  {"id":"2","name":"テスト2"},
  {"id":"3","name":"テスト3"},
  {"id":"4","name":"テスト4"},
  {"id":"7","name":"テスト7"}
]}

実際にTemplateで使用する場合は以下のように記載します。
<ng-container *ngFor="let item of testList | filter:'id':'7'">
  <div class="test_class">{{ item.name }}</div>
</ng-container>
@Pipe の name をvertical lineでリストの後ろにつけて、Pipeの後ろは、「:(colon)」を繋げてPipeへの引数を記載します。
つまり、「testList | filter:'id':'7'」の部分で、リスト内の「id」という項目が「7」になっている値を絞り込んでいます。

出力結果は、
 テスト7
となります。

如何でしょうか。Pipe、色々な使い方がありますね。

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