狛ログ

2021年12月28日火曜日

C# List内の要素同士で一致するものがあるか確認方法。

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

プログラム実装時に、配列やList同士で一致するものがあるかどうかチェックする、というような場合が多々あると思います。
今回はC#でこのチェックをさっときれいに書く方法がないか調べてみて、良さそうな書き方を見つけたので、ご参考程度にちょっと共有したいと思います。

まず単純に、2つのList内に同一のものがあるかどうかチェックするする場合、下記のようなFindAllメソッド等を使えば簡単に確認ができます。
string[] fruitsAry1 = new string[] { "Apple", "Banana", "Orange", "Grape" };
string[] fruitsAry2 = new string[] { "Apple", "Grape", "Grapefruit", "Strawberry" };
List<string> list1 = new List<string>(fruitsAry1);
List<string> list2 = new List<string>(fruitsAry2);
List<string> fruitsList = list1.FindAll(list2.Contains);
結果上記のfruitsListは{ "Apple", "Grape" }になります。

単純なList同士であれば上記のような方法でチェックできますが、例えばListに複数要素が含まれている場合で、ある要素だけ、一致するかどうかをチェックしたい場合は、ちょっと工夫が必要になります。

例えば

■Fruits.cs

namespace Fruits
{
	public class FruitsData
	{
	    public string id { get; set; }
	    public string name { get; set; }
	}
}

■CheckFruits.cs

List<FruitsData> fruitsList1 = new List<FruitsData>()
{
    new FruitsData {Id = "1", Name = "Apple" },
    new FruitsData {Id = "2", Name = "Banana" }
    new FruitsData {Id = "3", Name = "Orange" }
    new FruitsData {Id = "4", Name = "Grape" }
};

List<FruitsData> fruitsList2 = new List<FruitsData>()
{
    new FruitsData {Id = "1", Name = "Apple" },
    new FruitsData {Id = "2", Name = "Grape" }
    new FruitsData {Id = "3", Name = "Grapefruit" }
    new FruitsData {Id = "4", Name = "Strawberry" }
};
上記のような場合でNameが一致するものがあるか、チェックする場合、素直にやると以下のような感じでしょうか
bool isSame = false;
foreach(var fruits1 in fruitsList1)
{
    if(isSame)
    {
        break;
    }
    foreach (var fruits2 in fruitsList2)
    {
        if(fruits2.Name.Equals(fruits1.Name))
        {
            isSame = true;
            break;
        }
    }
}
2つのリストを回して一致するものがあったら抜けるという、すごい単純なものですが、一致するものがあるかどうかのチェックだけのためにこれを書くのはちょっと長いですよね。

もうちょっとだけ簡略化すると、Existsメソッドを使えばこのようにも書けると思います。
bool isSame = false;
foreach(var fruits1 in fruitsList1)
{
    if(fruitsList2.Exists(fruitsItem2 => fruitsItem2.Name.Equals(fruits1.Name)))
    {
        isSame = true;
        break;
    }
}
結構すっきりしましたが、fruitsList1をとりあえず回さないといけないので、ここまできたらループさせずにいけそうな気がします。
で、上記を踏まえて、さらにFindメソッドを使って、最終的にはこんな感じに簡略化できました。
var machedFruits = fruitsList1.Find(fruitsItem1 => 
    fruitsList2.Exists(fruitsItem2 => fruitsItem2.Name.Equals(fruitsItem1.Name))
);
FindメソッドとExistsメソッドを使ってループを使わずに一致する要素の確認ができました。
ちなみにこのFindメソッドの返り値は、全体の中で最もインデックス番号の小さい要素が返ってきます。
一致するものが無い場合はnullが返ってきますので、単純に一致するかどうかのチェックとしては、nullが返ってこなかったら一致しているということになります。

この他にも、Linqを使ったり等いろいろと方法があると思いますが(もっと簡略化もできそう)、方法の1つとしてご参考いただければと思います。

【Laravel】条件によって、RateLimitterの回数制限のカウント処理を分ける方法。


オフィス狛 技術部のmmm(むー)です。

LaravelでAPIのリクエストをある回数以上行うと、「Too Many Attempts」のエラーが発生して一定時間経過するまでAPIを実行できなくなります。
このエラー文言の表示条件は、RateLimiterにて設定されています。
条件によって、この制限回数のカウント方法を変更する修正がしたかったのですが、対応方法がなかなか見つけられず時間がかかったため記事に残します。

やりたいこと
・ユーザー情報を取得できた場合:1分間に5回連続でAPIをリクエストすると、「Too Many Attempts」のエラーを発生させる
・ユーザー情報を取得できない場合:1時間に10回連続でAPIをリクエストすると、「Too Many Attempts」のエラーを発生させる

Laravelバージョン
$ php artisan --version
Laravel Framework 8.27.0

対処方法

早速ですが、下記のように記載すると実現できました。
RateLimiter::for('login', function (Request $request) {
return $request->user()
? Limit::perMinute(5)->by($request->user()->id) // ユーザー情報を取得できた場合のリクエスト回数制限
: Limit::perHour(10)->by($request->email.$request->ip()); // ユーザー情報を取得できない場合のリクエスト回数制限
}

・制限回数のチェックをする際は、by()でどの単位でカウントするかを指定します。
   ・ユーザー情報を取得できた場合:ユーザーID単位($request->user()->id)でカウント
   ・ユーザー情報を取得できない場合:リクエストのメールアドレス+IPアドレス($request->email.$request->ip())でカウント
・1分間での回数制限を設定する場合は、perMinute()を使用します。
・1時間での回数制限を設定する場合は、perHour()を使用します。

参考:https://laravel.com/docs/8.x/routing#segmenting-rate-limits (公式)

その他
確認する際は、キャッシュをクリアしてください。
$ php artisan cache:clear

またRateLimiterの修正が反映されているか、レスポンスヘッダーから確認することができます。
・X-RateLimit-Limit:全何回リクエストできるか
・X-RateLimit-Remaining:あと何回リクエストできるか
・Retry-After:何秒後にロックが解除されるか


以上となります、参考になりましたら幸いです。

,

2021年12月27日月曜日

【Python】loggingでログをカスタマイズする【Flask】


狛ログをご覧のみなさん、はじめまして。
2021年にオフィス狛に入社いたしました、nago(なご)です。
技術に関するブログ記事を書くのは初めてですが、有益な情報を投稿できるように頑張ります。
よろしくおねがいします☺

さて、今回はPython(Flask)で、お試し開発をしていた時に苦戦したログ出力についてまとめたいと思います。

作ろうとしていたのは、外部のREST APIにリクエストを投げて、何かしらの結果を受け取り、画面に表示させる機能です。
まず、先輩社員が投稿してくださっていたこちらの記事を参考にして、AWS API GatewayでAPIのMockを作成しました。

正しい合言葉をidに設定してリクエストすると、ステータスコードが200となり、とってもタメになる情報を受け取れるAPIにしました。
合言葉を間違えていたらステータスコードが401となりエラーが返ってきます。

  

  
↓ ↓ ↓
合言葉を間違えると401エラーとなり、以下のメッセージが返ってきます。
  
アプリケーションは以下のような構成で作成しました。
 	 
api-test/
static/
│ └ styles.css
├ templates/
│ └ api_test.html
├ views/
│ ├ __init__.py
│ └ api_test.py
├ __init__.py
├ app.py
get-pip.py
├ Pipfile
└ Pipfile.lock

コードはこちら。
・api_test.html
    
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>APIテスト</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
</head>
<body>
<form action="{{ url_for('api_test.call') }}" method="POST">
<input type="text" name="id">
<input type="submit" value="API CALL">
</form>
<input type="button" onclick="location.href=&#39;{{ url_for('api_test.show_api_test') }}&#39;" value="RESET">
<p>{{ message }}</p>
</body>

・views/app.py
    
from flask import Flask, redirect, url_for
from views.api_test import api_test
app = Flask(__name__)
app.register_blueprint(api_test)
# 最初の画面へリダイレクト
@app.route("/")
def redirect_to_api_test():
return redirect(url_for("api_test.show_api_test"))
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8080)

・api_test.py
 	 
import requests, json
from flask import Blueprint
from flask import render_template, request
api_test = Blueprint("api_test", __name__)
# 画面表示(初期 & リセット時)
@api_test.route("/")
def show_api_test():
message = ""
return render_template(
"api_test.html",
message=message
)
# APIリクエスト & レスポンス受け取り & 画面表示
@api_test.route("/call", methods=["POST"])
def call():
# APIGatewayでMock作成時に設定されたURL
url = "https://xxxx.amazonaws.com/api-test/my/info"
# クエリ文字列を作成
payload = { "id": request.form.get("id") }
# リクエストヘッダを作成
headers = { "Content-Type": "application/json" }
# リクエスト & レスポンス受け取り
response = requests.post(url, data=json.dumps(payload), headers=headers)
# レスポンスからメッセージを取り出す
 message = response.json()["message"]
# 画面遷移 & メッセージ表示
return render_template(
"api_test.html",
message=message
)

これで、APIから返ってきたメッセージをそのまま画面に表示することができました。
次に、APIがコールされるたびに、そのリクエスト内容と結果のログを出力する仕組みを作ろうと思います。

Pythonでログを出力するには、Pythonで用意されているloggingというモジュールを使います。
今回は設定ファイルを既存のファイルとは別に作成して、ログの設定をしてみます。
私はプロジェクトルートに「config」という名前のディレクトリを作り、その中にログの設定ファイルを作りました。
また、ログファイルを出力したかったので、ログファイルを置くための「log」ディレクトリも作成しました。

以下が作成したディレクトリと設定ファイルです。

 	 
api-test/
├ config/
│ ├ __init__.py
│ └ logging_config.py
├ log/
static/
│ └ styles.css
├ templates/
│ └ api_test.html
├ views/
│ ├ __init__.py
│ └ api_test.py
├ __init__.py
├ app.py
get-pip.py
├ Pipfile
└ Pipfile.lock
※赤字が追加分

・config/logging_config.py
    
from logging import DEBUG, INFO, config
import datetime
# ファイル名に入れる日付フォーマット
today = datetime.date.today().strftime("%Y-%m-%d");
config.dictConfig(
{
"version": 1,
# ログフォーマット設定
"formatters": {
"formatter": {
"format": "%(levelname)s %(asctime)s [%(module)s] %(message)s"
}
},
# ハンドラー設定
"handlers": {
# 標準出力ハンドラー
"streamHandler": {
"class": "logging.StreamHandler",
"formatter": "formatter",
"level": INFO
},
# ファイル出力ハンドラー
"fileHandler": {
"class": "logging.FileHandler",
"formatter": "formatter",
"level": INFO,
# ログファイルを出力したい場所のパスとファイル名を指定
"filename":f"xxxx/api-test/log/{today}_api-test.log",
"encoding": "utf-8"
}
},
# ロガー設定
"loggers": {
"basicLogger": {
"handlers": [
"streamHandler",
"fileHandler"
],
"level": INFO,
"propagate": 0
}
}
}
)

ログの設定は、logging.config.dictConfigに辞書(Dictionary型の設定値)を渡すか、
logging.config.fileConfigにconfigparser形式ファイルを渡すかで設定できますが、
私は今回はdictConfigで設定してみました。

ログを出力するには、まずLogger(ロガー)の取得が必要なのですが、そこにHandler(ハンドラー)を指定することで出力形式の指定をプラスできるようになっています。
また、出力フォーマットも設定したい場合は、ハンドラーに対してFormatter(フォーマッター)を指定します。

ロガー、ハンドラー、フォーマッターの名前は自由につけることが出来ます。
私はロガーは「basicLogger」を作り、そこに標準出力ハンドラーの「streamHandler」と、ファイル出力の「fileHandler」をくっつけました。フォーマッターは「formatter」にしました(そのまんま)

"varsion" : 1 ってなんぞやと思い調べてみたのですが、辞書スキーマのバージョンのことでした。
現在は1しかないので、1を入れておけばいいそうです。こちらはdictConfigに辞書を渡す場合は必須のため、入れないと怒られます。

さて、ようやくapi_test.pyで設定ファイルを読み込んで、ログ出力してみます。

・views/api_test.py
    
import requests, json
from logging import getLogger
from flask import Blueprint
from flask import render_template, request
from config import logging_config
api_test = Blueprint("api_test", __name__)
# ログ設定ファイル読込
logging_config
# ロガー取得
logger = getLogger("basicLogger")
# 画面初期表示
@api_test.route("/")
def show_api_test():
message = ""
return render_template(
"api_test.html",
message=message
)
# APIリクエスト & レスポンス受け取り & 画面表示
@api_test.route("/call", methods=["POST"])
def call():
# APIGatewayでMock作成時に設定されたURL
url = "https://xxxx.amazonaws.com/api-test/my/info"
# クエリ文字列を作成
payload = { "id": request.form.get("id") }
# リクエストヘッダを作成
headers = { "Content-Type": "application/json" }
# リクエストログ出力
logger.info("request : url = " + url + ", payload = " + str(payload))
# リクエスト & レスポンス受け取り
  response = requests.post(url, data=json.dumps(payload), headers=headers)
# レスポンスからステータスコードとメッセージを取り出す
  status_code = response.status_code
  message = response.json()["message"]
# レスポンスログ出力
if status_code == 200:
logger.info("response : status_code = " + str(status_code) + ", message = " + message)
else:
logger.error("response : status_code = " + str(status_code) + ", message = " + message)
# 画面遷移 & メッセージ表示
return render_template(
"api_test.html",
message=message
)
※赤字は追加分

・log/2021-12-15_api-test.log (2021/12/15にログが作成された場合のファイル名)
    
INFO 2021-12-15 14:23:53,701 [api_test] request : url = https://xxxx.amazonaws.com/api-test/my/info, payload = {'id': 'sushi'}
INFO 2021-12-15 14:23:53,778 [api_test] response : status_code = 200, message = お寿司はおいしい。猫はかわいい。ポメラニアンもかわいい。
INFO 2021-12-15 14:24:46,184 [api_test] request : url = https://xxxx.amazonaws.com/api-test/my/info, payload = {'id': ''}
ERROR 2021-12-15 14:24:46,254 [api_test] response : status_code = 401, message = 合言葉が違うようです・・・
正常パターンとエラーパターンを実行してみました。
フォーマッターで設定した「%(levelname)s %(asctime)s [%(module)s] %(message)s」の通りに表示されています。
出力時にapi_test.pyで設定したメッセージが「%(message)s」に代入されている形です。

調べてみると、loggingは思ったより奥が深くびっくりしました。
覚えておくと出したいログを見やすく出すことができて便利だと思います。

今回はこれでおしまいです。
ここまでお読みいただきありがとうございました。

参考リファレンス:
Logging HOWTO
logging.config --- ロギングの環境設定

, , ,

Angular・ngx-scannerでインカメラが使われるのを制御する方法。


こんにちは、オフィス狛 技術部のpinoです。


Angularで、バーコードやQRコード読み取りに使用するライブラリに、『ngx-scanner』があります。

テンプレートに<zxing-scanner></zxing-scanner>を埋め込むだけで、読み取り用のカメラを自動で起動してくれる便利なライブラリです。


当社のプロジェクトでも採用しているのですが、先日、通常なら背面のカメラが起動するところで、iOS12でのみインカメラが起動する事象が発生しました。


今回は、その時のトラブルシューティングとして、インカメラが使われるのを制御する方法について紹介します。



バージョン情報

  • Angular:12.0.3
  • ngx-scanner:3.0.1


なぜインカメラが起動する?

まず、インカメラが起動してしまう理由について見ていきます。


カメラが起動するまでの流れは、node_modules/@zxing/ngx-scanner/__ivy_ngcc__/fesm2015/zxing-ngx-scanner.jsで確認できます。

ngAfterViewInit()

  1. /**
  2. * Executed after the view initialization.
  3. */
  4. ngAfterViewInit() {
  5. // makes torch availability information available to user
  6. this.getCodeReader().isTorchAvailable.subscribe(x => this.torchCompatible.emit(x));
  7. if (!this.autostart) {
  8. console.warn('New feature \'autostart\' disabled, be careful. Permissions and devices recovery has to be run manually.');
  9. // does the necessary configuration without autostarting
  10. this.initAutostartOff();
  11. return;
  12. }
  13. // configurates the component and starts the scanner
  14. this.initAutostartOn();
  15. }

this.autostartはtrue固定なのでif (!this.autostart)の分岐に入りません。
this.initAutostartOn()に続きます。

initAutostartOn()

  1. /**
  2. * Initializes the component and starts the scanner.
  3. * Permissions are asked to accomplish that.
  4. */
  5. initAutostartOn() {
  6. return __awaiter(this, void 0, void 0, function* () {
  7. this.isAutostarting = true;
  8. let hasPermission;
  9. try {
  10. // Asks for permission before enumerating devices so it can get all the device's info
  11. hasPermission = yield this.askForPermission();
  12. }
  13. catch (e) {
  14. console.error('Exception occurred while asking for permission:', e);
  15. return;
  16. }
  17. // from this point, things gonna need permissions
  18. if (hasPermission) {
  19. const devices = yield this.updateVideoInputDevices();
  20. this.autostartScanner([...devices]);
  21. }
  22. });
  23. }

カメラアクセスの権限周りをチェックしています。

許可された状態だと、最終的にthis.autostartScanner([...devices])が実行されます。

autostartScanner()

  1. /**
  2. * Starts the scanner with the back camera otherwise take the last
  3. * available device.
  4. */
  5. autostartScanner(devices) {
  6. const matcher = ({ label }) => /back|trás|rear|traseira|environment|ambiente/gi.test(label);
  7. // select the rear camera by default, otherwise take the last camera.
  8. const device = devices.find(matcher) || devices.pop();
  9. if (!device) {
  10. throw new Error('Impossible to autostart, no input devices available.');
  11. }
  12. this.device = device;
  13. // @note when listening to this change, callback code will sometimes run before the previous line.
  14. this.deviceChange.emit(device);
  15. this.isAutostarting = false;
  16. this.autostarted.next();
  17. }

まず、カメラのlabelプロパティにback|trás|rear|traseira|environment|ambienteが含まれるかを見ていますね。

(調べてみたところ、ポルトガル語やスペイン語などで「後方」「環境」といった意味の単語が含まれているようです。)

devicesの中で上記に当てはまる最初のカメラか、当てはまるものがない場合はdevicesの最後尾のカメラが起動される仕組みになっています。


ここまでで、カメラが起動するまでの流れを追うことができました。


次は、iOS12と、比較用に背面カメラが起動するiOS15のカメラ情報を、デモページを使って確認してみます。


デモページでカメラアクセスを許可すると、使用できるカメラのリストが表示されます。

iOS12とiOS15で試した結果、以下のようになりました。

『〇〇カメラ』となっているのがlabelプロパティに当たり、並び順がそのままdevicesの要素の並びになっています。


どちらのOSも、labelback|trás|rear|traseira|environment|ambienteは含まれていません。

その場合、配列の最後尾のカメラが起動されるわけですが、iOS15は背面カメラなのに対し、iOS12は前面側カメラが最後尾になっています。

iOS12でのみインカメラが起動するのは、ここに原因があったんですね。

デモのコードは、公式のGithubか下記からも確認できます。

https://stackblitz.com/edit/zxing-ngx-scanner


ちなみに、iPhoneの設定から『iPhoneの使用言語』をEnglishに変更したところ、

  • 前面カメラ -> Front Camera

  • 背面カメラ -> Back Camera

という風に文言が変わりました。

labelは使用言語に基づくようなので、使用言語を変更することで回避できる、ということになりますが、あまり現実的ではなさそうです...。



インカメラが使われるのを避けるには?

自動起動されるカメラを変更するのは、先ほど確認したような処理の流れ自体を変更しないといけないので、難しいです。

ただ、デモページで確認できるように、カメラの切り替えは可能になっています。

だとしたら、自動起動が終わった後に、背面カメラを起動するよう設定し直すのはどうでしょうか?


ということで、今度は「自動起動が完了したことを検知できるか」を確認してみます。


ngx-scannerでは、いくつかのイベントが用意されています。

今回使用しているVer.3.0.1では、以下のイベントが定義されていました。

  • autostarted

  • autostarting

  • torchCompatible

  • scanSuccess

  • scanFailure

  • scanError

  • scanComplete

  • camerasFound

  • camerasNotFound

  • permissionResponse

  • hasDevices

  • deviceChange

名前からなんとなくどんなイベントか予想がつくかと思います。


例えば、「使用できるカメラが存在していない場合」と「読み取りに成功した場合」にイベントハンドラを設けたい時は、以下のようにします。

テンプレート側

  1. <zxing-scanner
  2. (camerasNotFound)="onCamerasNotFound()"
  3. (scanSuccess)="onScanSuccess($event)">
  4. </zxing-scanner>

ロジック側

  1. onCamerasNotFound(): void {
  2. console.log('cameras not found.')
  3. }
  4. onScanSuccess(result: string): void {
  5. console.log('scan success:', result)
  6. }

ここで、カメラが起動するまでの流れの最後の処理(autostartScanner())をもう一度見てみます。

  1. /**
  2. * Starts the scanner with the back camera otherwise take the last
  3. * available device.
  4. */
  5. autostartScanner(devices) {
  6. const matcher = ({ label }) => /back|trás|rear|traseira|environment|ambiente/gi.test(label);
  7. // select the rear camera by default, otherwise take the last camera.
  8. const device = devices.find(matcher) || devices.pop();
  9. if (!device) {
  10. throw new Error('Impossible to autostart, no input devices available.');
  11. }
  12. this.device = device;
  13. // @note when listening to this change, callback code will sometimes run before the previous line.
  14. this.deviceChange.emit(device);
  15. this.isAutostarting = false;
  16. this.autostarted.next(); // <- ここ!
  17. }

最後に、autostartedイベントが発火していますね。

これを、コンポーネント側で検知するようにしてあげればよさそうです。

テンプレート側

  1. <zxing-scanner
  2. [(device)]="currentDevice"
  3. (camerasFound)="onCamerasFound($event)"
  4. (autostarted)="onAutoStarted()">
  5. </zxing-scanner>

ロジック側

  1. availableDevices: MediaDeviceInfo[];
  2. currentDevice: MediaDeviceInfo = null;
  3. ...
  4. onCamerasFound(devices: MediaDeviceInfo[]): void {
  5. // 使用できるカメラを取得
  6. this.availableDevices = devices;
  7. }
  8. onAutoStarted(result: string): void {
  9. const userAgent = navigator.userAgent.toLowerCase();
  10. let iosVersionInfo = userAgent.match(/iphone os ([\d]+)_([\d]+)_([\d]+)/);
  11. if (!iosVersionInfo) {
  12. iosVersionInfo = userAgent.match(/iphone os ([\d]+)_([\d]+)/);
  13. }
  14. const iosVersion = iosVersionInfo?.[1];
  15. if (iosVersion === '12') {
  16. // iOS12の場合は、配列の最初のカメラを使用する
  17. this.currentDevice = this.availableDevices?.[0];
  18. }
  19. }

実際に、iOS12の場合は背面カメラを起動するよう設定し直すサンプルにしてみました。


まず、camerasFoundで使用できるカメラの情報が渡ってきますので、変数に控えます。

テンプレート側にある[(device)]プロパティに使用したいカメラを設定してあげるとカメラが切り替えられるので、UserAgentからiOS12と判断できる場合は配列の最初のカメラを設定するようにしました。


iOS12ではない場合はライブラリ側で自動起動したカメラが設定されますので、ピンポイントな修正が可能です。



INFO

最新版(Ver.3.2.0)では、this.autostarted.next()initAutostartOn()内に処理が移っていました。

紹介した処理の流れは大きく変わっていないと思うので、お使いのバージョンと比較してみてください。



以上が、今回のトラブルシューティングの内容でした。

どなたかの参考になれば嬉しいです🦓


iOS15版のSafariから目立つようになったステータスバーの背景色を指定したい。

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



iOS15版のSafariで以前私がHTMLなどを作成したWebシステムを確認したところ、ステータスバーの色が変わっていることに気づきました。

自動的に選ばれた色とヘッダーの色味が少し違う色でしたので、逆に浮いて見えて困っていました。

ステータスバーをWebシステムのメインカラーの色に合わせられたら違和感を感じないのにと考え、色を制御する方法を探していたところ、色を指定できるようなので忘れないよう備忘録も兼ねてブログに書いてみようと思います。


ステータスバーの背景色を指定する方法、それは「theme-color」というmetaタグを書くだけです。

headタグの中に下記を記述することで色を変えることができます。

  1. <meta name="theme-color" content="任意の色">

任意の色の部分にカラーコードを指定するだけで、ステータスバーやアドレスバーの背景色が変わります。

上記は
  1. <meta name="theme-color" content="#dcdcdc">
をheadタグの中に記述しました。

「theme-color」を指定した場合も、明度の高いペールカラーのような色を指定した場合にはダークモードでは色が変わらないこともあるようですが、それはダークモードの仕様上仕方ないかと思います。



余談ですが、iOS15版のSafariから画面下部にアドレスバーが表示される関係だと思うのですが、要素が少ないページの背景色がアドレスバーの下には回り込まないで下に「theme-color」で指定した色が表示されてしまったりします。

上記のような状態になった場合は、全体を囲んでいるタグにheight: 100vh;を指定すると背景が下まで表示されるようになりました。(余白が空いてしまった例のCSS指定は height: 100%; にしていました)

簡単なことなのですが、少しつまづいてしまったので、似たような現象に悩まれた方は試してみてはいかがでしょうか?

2022/11/01日 height:100vhを設定するよりも綺麗に背景色設定する方法ができた為、新しくブログ記事を書きました!(この方法だと、余分なスクロールができません)


iOS15版のSafariはレイアウト含めさまざまな変更点があり、技術者はもちろん普通に使っているユーザーも覚えることが多くて大変ですね……。

今後もiOS版Safariの仕様変更には気をつけていきたいです。

2021年4月30日金曜日

【AWS】API Gatewayで超手軽にMockを作成する。

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

Androidアプリの開発中に動作確認をしようとしたとき、データ取得先のAPIが未完成だったり、特定のレスポンスやエラー(メンテナンス画面にするなど)を返すように制御するのが難しい...
など、もやもやすることがありました。

で、、それならシミュレータとかMockとか呼ばれてるものを自分で用意しちゃえばいいじゃん!?
と、思い立ち、AWSのAPI Gatewayが提供する機能を使用して作成してみました。
これが意外と使える子だったので、今回は一番シンプルに使える「統合Mock」というサービスの使い方を紹介します!

Step1:API GatewayにAPIを登録する

何はともあれ、APIの外枠を作らないことには始まりません。
AWSの管理コンソールからAPI Gateway画面を開き[API作成]をクリックして、APIを作成していきます!

最初に、作成するAPIタイプの選択画面が表示されますので"REST API"の[構築]を選択してください。

次にAPIの作成画面が表示されますので適当にAPI名を指定してください。
この値はアクセスURLに反映はされませんが、変更が出来ないようなので、ご注意ください。
これでAPIの新規作成は完了です。

Step2:リソースとメソッドを作成する

APIの作成が完了するとリソース管理画面に遷移します。
この時点では、下図のとおり何も設定されていませんので、ここから設定を追加していきます。
作成開始前にイメージ共有しておいた方が分かりやすいと思うので、まずはMock化したいAPIの概要を決めておきます。
今回は以下のAPIレスポンスを代替する統合Mockを作ろうと思います。

 GET / https://www.yuckieee.com/book/search


上記APIの下線部がAWS Gatewayが自動発行するパスで置き換わる部分です。
そのため、book以降のパス部分を作成対象として進めていきます。

まずは[アクション]メニューから[リソースの作成]を選択し、上記に沿ってリソースを定義します。
ここで"リソース"と言っているのがアクセスするURLのパスとなります。
リソースは、ディレクトリ 構造での管理となっており、最上位を/(ルート)リソースと呼びます。
このルートリソースに子リソースを追加していく形でリソースを作成していきます。
今回は
/(ルート)リソースにbook子リソースを追加
bookリソースにsearch子リソースを追加
の順でリソースを作成します。

リソースの作成後、レスポンス返却させたいリソース(=今回はsearchリソース)を選択し、[アクション]メニューから[メソッドの作成]を選択します。
[メソッドの作成]クリック後、下図のように作成したいHTTPメソッドが選択可能になるため、今回は"GET"を選択し、右側の確定チェックボタンをクリックします。
確定チェッククリック後、メソッドのセットアップ画面が表示されます。
今回はAWSの統合Mock機能を使用するため、統合タイプから"Mock"を選択し[保存]をクリックします。
これでリソースとメソッドの作成は完了です。

Step3:マッピングテーブルを定義する

統合Mockが作成されると、下図のような定義画面が表示されます。

それぞれ、私視点での概要を軽く説明すると下記のとおりです。

タイプ 説明
メソッド
リクエスト
実際に受信したAPIリクエストに対して、入力値チェックやAPIキー指定などの設定が可能です。
統合
リクエスト
統合Mockエンドポイントに引き渡す値の設定を行います。
統合Mockの場合、ここで設定された値が統合レスポンスに引き渡されます。
統合
レスポンス
統合Mockエンドポイント(統合リクエスト)から受け取った値を元にレスポンスを定義します。ここでHTTPステータスやヘッダ、レスポンスBodyなどを設定します。
メソッド
レスポンス
統合Mockの場合、レスポンスに必要なHTTPステータスをここで設定しておく必要があるようです。

色々触ってみながら試していただくのが良いですが、 最もシンプルに使いたい場合は、統合レスポンスのマッピングテーブルを作成するだけ出来ちゃいます!

ということで、[統合レスポンス]を開きます。
統合レスポンスでは、統合リクエストから受信するHTTPステータスごとにマッピングテンプレートの設定が可能です。 新規作成段階では、統合レスポンスがデフォルトで1つ用意されており、統合リクエストで指定されたHTTPステータスに対応する統合レスポンスが存在しない場合、 この統合レスポンスのマッピングテンプレートがレスポンスされます。
※ただし、API Gateway自体がエラーとなった場合を除きます。

今回マッピングテンプレートに指定したい内容は以下のとおりです。
[
{
"id": "1001",
"title": "オフィス狛ものがたり",
"Author": "yuckieee"
},
{
"id": "1002",
"title": "狛ちゃん冒険譚",
"Author": "yuckieee"
}
]

デフォルトの統合レスポンスを順番に展開し、マッピングテンプレートのContent-Type:application/jsonにレスポンスしたい内容を入力します。
入力完了後、右下の[保存]をクリックして保存します。

 👉  統合リクエストと統合レスポンスのマッピングテンプレートの記述方法は
   「おまけ:リクエストによってレスポンス内容を切り替える」の章を参照してください

マッピングテンプレートの設定は以上で完了です。

Step4:動作確認(テスト)を行う

これまでの設定が問題ないか、デプロイまでにテストできる機能がありますので、これを使って動作確認してみます。 リソースの管理画面左にある[テスト⚡️]をクリックしてください。
メソッドのテスト画面が表示されますので、画面中央下部の[⚡️テスト]クリックで、先ほど定義した内容でAPIが呼び出されます。
[⚡️テスト]クリック後、結果が画面右側に表示されます。
この内容が実際にMockにアクセスした際に返却される値と同様の内容となっていますので、この段階でレスポンスに問題がないかを確認します。
もし、Internal Errorなどが返却されている場合は、設定をもう一度見直すか、ログ領域を参照しエラー内容を確認してください。

Step5:デプロイ(外部公開)する

テストで動作に問題がないことを確認したあとは、実際にデプロイしてみます。
[アクション]メニューの[APIのデプロイ]をクリックしてください。
APIのデプロイダイアログが表示されますので、デプロイするステージを新規作成します。
デプロイステージ名は、URLにも組み込まれますので分かりやすい名前にしておきましょう。
今回はdevとしておきます。
問題なくデプロイが完了すると、新規作成したステージのエディター画面に遷移します。 遷移先の画面に呼び出し用のURLが記載されていますので、このURLを使用してMockにアクセスが可能になります。

Step6:実行確認する

最後にPOSTMANを使用してリクエストが可能かを確認してみます。
POSTMANを起動し、該当のURLを指定して実行してみてください。下図のようにマッピングテンプレートに定義した値が想定通り返却されればOKです!
※APIクライアントツールが入っていない場合は、GETであればブラウザ直接指定でも確認可能です!
デプロイが出来ていない場合、下図のようにMissing Authentication Tokenエラーが返ってきますので再度デプロイしてみてください。
これでMockとして使用できるようになりました!
あとは参照元のAndroidアプリ側URLをMockに切り替えてLet's テスト〜♪♬♩

おまけ:リクエストによってレスポンス内容を切り替える

テストをしていると、HTTPステータスやレスポンスBODYの値をリクエストによって切り替えたくなりますよね。私もそうでした!!!!(最終的にはLambdaを使用しましたが)
簡単な切り替えであれば、統合Mock機能でも可能です。幾つか切り替え方法をご紹介しますので参考にしてみてください。

💡 マッピングテンプレート記述言語

マッピングテンプレートではApache Velocity Template Language (VTL)という言語が採用されているようです。 この言語仕様に沿って変数の定義やIF文、FOR文などの記述が可能です。
今回は、これらを使ってリクエストによるレスポンス内容の切り替えを行っていきたいと思います。
なお、ここでは言語自体の説明を省きますので、詳細は公式リファレンスを参照ください。

パターン1:HTTPステータスコードを切り替える

① 統合リクエストのマッピングテンプレート編集
HTTPステータスを切り替えたい場合は、統合リクエストのマッピングテンプレートを編集します。
デフォルトでは{statusCode: 200}が指定されていますが、リクエスト値によって切り替えたい場合は以下の要領で設定します。

👉 GETメソッド(クエリ文字使用)の場合
クエリ文字に"id"というパラメータを指定していたと仮定して記述しています。
例:/book/search?id=1001
#set($id = $input.params("id"))
#if($id == "1001")
{
"statusCode": 200
}
#elseif($id == "1002")
{
"statusCode": 400
}
#else
{
"statusCode": 500
}
#end

👉 POSTメソッド(リクエストBODY使用)の場合
リクエストBODYにidというパラメータを指定していたと仮定して記述しています。
例:{ "id": "1001" }
#set($id = $util.parseJson($input.body).id)
#if( $id == "1001")
{
"statusCode": 200
}
#elseif($id == "1002")
{
"statusCode": 400
}
#else
{
"statusCode": 500
}
#end

② メソッドレスポンスの追加
レスポンスのHTTPステータスにバリエーションを持たせたい場合、最初にメソッドレスポンスを追加する必要があります。メソッドレスポンス画面を開き[➕ レスポンスの追加]をクリックし、追加したいHTTPステータスコードを入力し、確定してください。
今回は400と500を追加してみました。
③ 統合レスポンスの追加
続いて、HTTPステータスごとの統合レスポンスを作成します。
デフォルトをHTTPステータス200で使用することにして、新しくHTTPステータス400と500用の統合レスポンスを作成します。統合レスポンス画面を開き[➕ 統合レスポンスの追加]をクリックして下記の要領で統合レスポンスを追加してください。

HTTPステータスの正規表現:
 統合リクエストから引き渡されるStatusCodeのうち、対象とする値を指定します。
 この値は正規表現での指定も可能なため、4\d{2}と書くことでステータスコード400番台全てを対象とすることも出来ます。

メソッドレスポンスのステータス:
 ②で作成したメソッドレスポンスを指定します。
 ※先にメソッドレスポンスを作成したのは、ここで指定させるためでした。

コンテンツの処理:
 デフォルト値であるパススルーのままで!
この段階で一度[保存]をして開き直すと、マッピングテンプレートの追加が可能になりますので、適当なレスポンス内容を設定してみてください。
その後、HTTPステータス500についても同様に設定します。
全ての設定が完了したあとに、再度STEP4~5で紹介した手順を実施いただけば完了です...!

【補足】動作確認時のクエリ文字/リクエストBODYの指定について
リソースに指定されたHTTPメソッドがGETなどクエリ文字指定可能なものである場合、メソッドテスト画面にクエリ文字の入力エリアが表示されます。
同様にPOSTなど、リクエストBODY指定可能なメソッドの場合は、リクエスト本文の入力エリアが表示されます。(注:リクエスト本文は、GETメソッドの場合は非表示となります)

パターン2:レスポンスBODYを切り替える

HTTPステータスは200のままで、リクエスト内容によってレスポンス内容を変えたい!!!
というパターンのご紹介です。
なお、クエリ文字を使用する場合は、パターン1のHTTPステータスでご紹介した$input.params("id")が、統合レスポンスでも使用できるため説明を省略します。

① 統合リクエストのマッピングテンプレート編集
この手順はリクエストBODYを統合レスポンスで使用したいときだけ使用します。

👉 POSTメソッド(リクエストBODY使用)の場合
デフォルトでは、リクエストBODYは統合レスポンスに渡されません。そのため、統合リクエストで明示的にオーバーライドしてあげる必要があります。
なお、statusCodeは必ず統合レスポンスに送る必要があるため、削除しないようご注意ください。
#set($context.requestOverride.path.body = $input.body)
{
"statusCode": 200
}


② 統合レスポンスのマッピングテンプレート編集
👉 POSTメソッド(リクエストBODY使用)の場合
リクエストBODYにidというパラメータを指定していたと仮定して記述しています。
例:{ "id": "1001" }
#set($body = $context.requestOverride.path.body)
#set($id = $util.parseJson($body).id)
#if($id == "1001")
{
"id": "1001",
"title": "オフィス狛ものがたり",
"Author": "yuckieee"
}
#elseif($id == "1002")
{
"id": "1002",
"title": "狛ちゃん冒険譚",
"Author": "yuckieee"
}
#else
{
"message":"該当の書籍情報が存在しません"
}
#end

なお、この辺りの話はAWS公式のデベロッパーガイドに記述されていますので、こちらも参考にしていただければと思います。
AWS公式:API Gateway マッピングテンプレートとアクセスのログ記録の変数リファレンス

おまけ:APIGatewayの料金

今回使用したREST APIでは、受信したAPIコールと、転送データ量に対してのみ料金が発生します。

APIコール料金:
2021/4/13時点で、月間のリクエスト数が3億3,300万件まで100万リクエストにつき4.25USD(アジアパシフィック(東京))のようです。
※なお、キャッシュを利用する場合は、更に料金がかかりますが、テスト・開発で使用する分には必要のない機能かと思います。

データ転送料金:
2021/4/13時点で、10TBまで1GBあたり0.09USDのようです。
なお、対象はAPIGatewayから、インターネット方向のデータ(レスポンスデータ)のみが対象となります。
どちらもリクエスト数、転送データ量が多い場合は、レンジによって割引されます。

ただ、ある程度使い倒したとしても、テスト利用の範囲であれば数百円程度でしょうか。
Mockやシミュレータってどうすりゃいいのか...と、小1時間頭を抱えている工数分で解決できるなら、試してみるのも良さげです!
AWS公式:Amazon API Gateway 料金

まとめ

APIGatewayの統合Mock機能で手軽にMockやシミュレータが作成できるのが分かりました!
次回は、APIGatewayをLambdaやS3と連携して一歩進んだMock作成をご紹介できればと思います!

2021年4月26日月曜日

express-requests-loggerでリクエストログの特定の項目をマスクする方法。


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

担当しているプロジェクトで、Node.js と Express でAPIを作成して、
リクエストのログを出力しているのですが、
セキュリティの観点から、一部リクエストの値をマスクする必要がありました。

express-requests-loggerで、マスクする方法をまとめました。

【バージョン情報】
・node:v12.14.0
・express-requests-logger:v3.0.3

■express-requests-logger のインストール

express-requests-loggerをインストールします。
  1. npm install express-requests-logger

■ログ出力設定

app.jsにexpress-requests-loggerの設定を追加します。

※loggerで指定している「httpLogger」ついては、こちらの記事を参考に設定いただければと思います
Node.js + Express + log4jsで、アクセスログとリクエストログを取る。

  1. const audit = require('express-requests-logger');
  2.  
  3. // ログ設定
  4. app.use(audit({
  5. logger: httpLogger,
  6. request: {
  7. maskBody: ['password'],
  8. },
  9. response: {
  10. maskBody: ['token'],
  11. },
  12. }));

上記のように設定しますと、リクエストとレスポンスの内容がログに出力されますが、
リクエストBodyの「password」と、レスポンスBodyの「token」の値が「XXXXX」にマスクされます。

【ログ出力結果】
  1. [2021-04-16T11:04:56.407] [INFO] http - {
  2. response: {
  3. status_code: 200,
  4.  (中略)
  5. body: '{"token":"XXXXX"}'
  6. },
  7. request: {
  8. method: 'POST',
  9.  (中略)
  10. body: '{"loginId":"test","password":"XXXXX"}'
  11. },
  12. 'millis-timestamp': 1618538696407,
  13. 'utc-timestamp': '2021-04-16T02:04:56.407Z',
  14. stage: 'end'
  15. } Inbound Transaction


他にも、項目自体を除外するオプションなどもありますので、一部ご紹介します。
詳細は「express-requests-logger」のホームページをご覧ください。
express-request-logger

【オプション】
■doubleAudit
・true:リクエストを受けたタイミングで出力(リクエスト)、
レスポンスを送信したタイミングで出力(リクエストとレスポンス)します。
・false:レスポンスを送信したタイミングで出力(リクエストとレスポンス)します。

■excludeURLs
配列で指定した文字列が、リクエストURLの一部と一致する場合は、リクエストもレスポンスも出力しません。

【オプション(リクエスト)】
■audit
・true:リクエストを出力します。
・false:リクエストは出力しません。

■excludeBody
配列で指定した文字列と一致する項目は、bodyに出力しません。
※「*」を指定すると、bodyの全ての項目を出力しません。

■maskQuery
配列で指定した文字列と一致する項目は、queryにマスクして出力します。

■excludeHeaders
配列で指定した文字列と一致する項目は、headersに出力しません。
※「*」を指定すると、headersの全ての項目を出力しません。

■maskHeaders
配列で指定した文字列と一致する項目は、headersにマスクして出力します。

■maxBodyLength
bodyに出力される最大文字数を指定できます。

【オプション(レスポンス)】
レスポンスもリクエストと同様のオプションで、
Body、Headersの項目について、マスクや除外することができます。


少ない設定でマスクすることができますので、機会があればお試しください。

,

SQL Serverのトランザクションログについて(記録される内容、領域の解放、切り捨て)


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

先日、担当していたシステムで、SQL Serverのトランザクションログが肥大化し、
ストレージが空き容量不足になる事象が発生しました。
その対応にあたり、SQL Serverのトランザクションログについて
調査する機会がありましたので、簡単ですがまとめてみました。

※下記の内容はSQL Server 2017で確認しています

■SQL Serverのトランザクションログには何が記録されるのか?

SQL Serverはデータベースに設定されている、復旧モデルによって異なります。

※復旧モデルは下記で確認できます。
  1. SELECT name, recovery_model_desc FROM sys.databases WHERE name = 'model';

・単純モデル
トランザクション開始からコミットまでが、トランザクションログに記録されます。

・完全モデル
全ての操作(DDL、DML)がトランザクションログに記録されます。
インデックスの操作など、大量のデータが読み込れると、大量のログが出力されます。

・一括ログモデル
完全モデルと似ていますが、一括操作(bcp、BULK INSERT など)の場合に、使用されるログ領域を縮小します。
インデックス操作(CREATE INDEX)も、最小ログ記録の対象(※)になるようです。
※下記の「最小ログ記録が可能な操作」を参照ください
トランザクション ログ (SQL Server)

■トランザクションログの領域は、いつ解放されるのか?領域を使い切るとどうなるのか?

こちらも「復旧モデル」によって異なります。

・単純モデル
チェックポイント(トランザクションログファイルに書き込み完了のタイミング)で解放されます。

トランザクションログは既存の領域を自動的に再利用しますので、ログ領域の管理は基本的に不要です。
領域を再利用するので、トランザクションログをバックアップすることはできません。

・完全モデル、一括ログモデル
トランザクションログのバックアップで解放されます。

トランザクションログファイルは領域を使い切ると、自動拡張の設定をしていれば、
トランザクションログファイルの上限サイズ(※)まで、物理サイズを自動拡張します。
(もちろんストレージの空きが無ければ拡張できません)
※最大、2TBまで設定できます

トランザクションログファイルは一度拡張されると、
自動縮小のオプションを有効にしていない限り、
自動で物理サイズは縮小されませんので、ストレージは解放されません。

ストレージを解放するには、下記でトランザクションログファイルを縮小し、
物理ファイルサイズを減らすことで、ストレージの空き容量を増やすことができます。
  1. use [データベース名];
  2.  
  3. -- トランザクションログ論理名を確認
  4. SELECT name FROM sys.database_files where type = 1;
  5.  
  6. -- トランザクションログファイルを縮小
  7. DBCC SHRINKFILE( [トランザクションログ論理名] , [縮小後の容量(MB)]);
※縮小後の容量の単位はMB(100を指定すると100MB)なので、ご注意ください


■トランザクションログを、手動で切り捨てる方法

ストレージの容量不足や、バックアップを取得する時間が無い場合などに、
手動で切り捨てることができます。

※トランザクションログの使用状況は下記コマンドで確認できます。
  1. DBCC SQLPERF('LOGSPACE');

【方法①】トランザクションログの削除
下記で削除できます。
注意点としては、削除前に一度、データベースの完全バックアップを取得している必要があります。
(取得してい場合はエラーになります)
  1. BACKUP LOG [データベース名] TO DISK = N'NUL';

【方法②】データベースの復旧モデルを「単純モデル」に変更する
下記で変更できます。
注意点ですが、例えば完全モデルから単純モデルに変更する場合、
完全モデルのみで使用できる機能を使用していると、単純モデルへ変更できないようです。
※以前、AWS RDSで、マルチAZ「あり (ミラーリング)」が設定されている完全モデルのデータベースは、単純モデルに変更することができませんでした
  1. ALTER DATABASE [データベース名] SET RECOVERY SIMPLE;


以上です。
トランザクションログで何か問題があった際に、お役に立てば幸いです。

2021年4月23日金曜日

2021年追加版、AdobeFontおすすめフォント三選。


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


2021年4月8日に、AdobeFontsに日本語フォントが191も増えたそうです。(4月10日がフォントの日なので、そこに合わせてきたんですね)

既存のものも合わせると、AdobeFontsに日本語のフォントは436フォントもあるらしいです!(使いきれない!)

Adobe Fontsが大幅拡充!日本語フォントラインナップが強化されました

ラインナップを見ていると見覚えがある文字もチラチラ…。

今回はAdobeFontsの個人的推しフォントを3つご紹介しようと思います。



1.ABキリギリス

ABキリギリスは雑誌やお菓子パッケージでよく見るフォントです!

個人的には「え?あのキリギリスがAdobeFontsに!?」と驚きました!嬉しい〜!

ほぼ打ちっぱなしでも、良い感じになりますね

クラフト感や異国感を出したい時に使いたいフォントです。

漢字の収録が少ないですが、それでも素敵なフォントです。

ちなみに「きりぎりす」という、こちらのフォントのバランスを修正+収録文字が追加されたフォントもありますが、AdobeFontには入っていません!「キリギリスじゃ漢字が少ない!」と感じた方はこちらを買ってみてはいかがでしょうか?



2.TA重ね丸ゴ

文字の重なりがレトロでノスタルジーなフォントですね。

Googleで調べた所、アニメの字幕に使われていたらしいですね。

憧れの「スーボ」に似た印象を受ける文字なので一目惚れしていまいました。

平成初期くらいの雑誌や、本の見出しを思い出しませんか?

使い所は難しいですが、かわいい・ポップな印象を出したい時に使えそうですよね!



3.AB好恵の良寛さん

さらさらと小筆で書いたような手書き風フォントです。

女性っぽい字だと勝手に思っていたのですが、良寛という男のお坊さんが書いた臨書を元にしたフォント……との事です。

シンプルで細身なフォントなので筆文字!という迫力が少なくて色々な手書きフォントが必要な時に使えそうですね。

夏のお中元の広告や、ナチュラルなイメージにしたい時に使えそうだと思いました。




番外編.VDL メガ丸

今年追加されたものではないですが、このフォントも紹介させてください(みなさん知ってますかね?)

丸くて抜けてる印象を受ける可愛いフォントなのにきちんと読みやすい!

萌え〜なアニメゲーム、などのかわいい系のサブカルチャーとの相性が良さそうですよね。

SQL Server のデータ移行方法と特徴について。


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

担当したプロジェクトで、SQL Serverのデータを移行する要件がありました。
データの移行方法を調べてみると色々な方法がありましたので、
いくつかの移行方法と、その特徴をまとめてみました。

1.デタッチとアタッチ

【主な手順】
移行元でデタッチ
-> データファイルとトランザクションログファイルを移行先にコピー
-> 移行先でデータファイルをアタッチ

【特徴】
・データファイルとトランザクションログファイル自体を移行します。
 (実質、データベースの移行になります)
・デタッチすると、DBが参照できなくなります。
 (アタッチし直せば、また参照可能になります)
・SQL Serverのバージョンアップにも対応しています。
・デタッチには、いくつかの制限事項があります。(下記の「データベースのデタッチ」を参照)
 https://docs.microsoft.com/ja-jp/sql/relational-databases/databases/database-detach-and-attach-sql-server?view=sql-server-ver15
・後述する移行方法と比較すると、移行に掛かる時間は最も短いです。

2.バックアップとリストア

【主な手順】
移行元でバックアップ
-> バックアップファイルを移行先にコピー
-> 移行先でリストア

【特徴】
・バックアップファイルを作成して移行します。
 (実質、データベースの移行になります)
・DBを停止しなくても移行可能です。
・SQL Serverのバージョンアップにも対応しています。
・リストアの際に、DBとデータファイル名を変更することで、DB名の変更が可能です。
 同じインスタンス内であれば、DB名を変更することで、複製することもできます。

3.ExportとImport

【主な手順】
Data Source設定 -> Destination設定
 ※SQL Server Management Studio の「Import and Export Wizard」を利用した場合です

【特徴】
・移行先から直接移行先へデータを移行可能です。
・DBを停止しなくても移行可能です。
・Destinationに「Flat File Destination」を指定すれば、移行元のデータをファイル化することができます。
 ただし、テーブル単位でしかファイルを作成できませんでした。
・「Import and Export Wizard」は機能が多く便利なのですが、逆に使いこなすことが難しいと感じました。
一度、「Import and Export Wizard」でデータ移行を試してみたところ、下記エラーが発生しました。
  1. Error 0xc02020f4: Data Flow Task 1: The column "カラム名" cannot be processed because more than one code page (932 and 1252) are specified for it.
こちらは、Import と Export するテーブルの照合順序(Collation)が異なっていたことが原因でしたが、
メッセージから原因が予測できず、解決するのに少し時間が掛かってしましました。

4.スクリプト作成と実行

【主な手順】
移行元でスクリプト作成
-> スクリプトコピー
-> 移行先でスクリプト実行
 ※SQL Server Management Studio の「Generate and Publish Scripts」を利用した場合です

【特徴】
・スクリプトファイルを作成します。
・DBを停止しなくても移行可能です。
・スクリプトファイルは、スキーマのみ(CREATE DATABASE、CREATE TABLE 等)、
 データのみ(INSERT)や、スキーマとデータ両方など、用途によって作成できます。
・SQLが直接書かれたファイルなので、大量データの場合はファイル容量が大きくなり、
 スクリプトを実行する際も、時間が掛かってしまいます。
・逆に、SQLを直接修正(スキーマの変更や、データの編集)できるので、
 データ量が少ない場合は扱い易いです。


データ移行は、移行時のシステムの稼働状態やデータ量などによって、
適切な移行方法を選定する必要がありますので、上記の特徴が何かお役に立てれば幸いです。

2021年3月30日火曜日

Swift・UIBarButtonItemのtitleを変更する。


こんにちは、オフィス狛 技術部のpinoです。


この度、初めてiOSアプリの開発に携わることができました。

まだまだ勉強中の身ですが、今回は「UIBarbuttonItemのtitleを変更する方法」について紹介したいと思います!


方法

早速ですが、UIBarbuttonItemのtitle変更方法です。

  1. private func setBarButtonItem() {
  2. // UIBarbuttonItemのactionを設定
  3. let button = UIBarButtonItem(title: "タイトル", style: .plain, target: self, action: #selector(self.buttonTapped(_:)))
  4. }
  5.  
  6. @objc func buttonTapped(_ sender: UIBarButtonItem) {
  7. // 変更後のタイトルを設定
  8. sender.title = "変更後のタイトル"
  9. }

タップ時の処理でUIBarButtonItemを引数にとって、
(仮引数名がsenderの場合)sender.title = "hoge"としてあげることでtitleの変更ができます。


...思いのほか、すごく簡単でしたね!
せっかくなので、ここからはこのtitle変更を活かしたものを作ってみたいと思います。


作るもの

以下のようなものを作っていきます。

  1. ・画面真ん中にTextFieldを配置
  2. ・TextField押下でキーボード表示。このキーボードの上にUIToolbarを配置
  3. ・UIToolbar上に並んでいるUIBarButtonItemタップで、キーボードタイプを切り替え

UIBarbuttonItemのtitle変更は、「UIBarButtonItemタップ」で発火するようにしてみます。


準備

Interfaceに「Storyboard」を選択して、プロジェクトを作成します。


画面真ん中にTextFieldを置いて、Keyboard Typeを「Number Pad」にしておきます。


ViewControllerで、TextFieldにUIToolbarを追加します。
ViewController.swift
  1. import UIKit
  2.  
  3. class ViewController: UIViewController {
  4.  
  5. @IBOutlet weak var sampleTextField: UITextField!
  6.  
  7. override func viewDidLoad() {
  8. super.viewDidLoad()
  9. self.setToolbar()
  10. }
  11. private func setToolbar() {
  12.  
  13. let toolbar = UIToolbar()
  14. toolbar.sizeToFit()
  15.  
  16. let inputSwitchButton = UIBarButtonItem(title: "英数字入力に切り替え", style: .plain, target: self, action: nil)
  17. let space = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
  18.  
  19. toolbar.items = [space, inputSwitchButton]
  20.  
  21. self.sampleTextField.inputAccessoryView = toolbar
  22. }
  23. }


ここまでで、「英数字入力に切り替え」というtitleのUIBarButtonItemが表示されるようになりました。


UIBarbuttonItemのtitle変更

UIBarButtonItemタップ時の処理を定義します。

ViewController.swift
  1. import UIKit
  2.  
  3. class ViewController: UIViewController {
  4.  
  5. @IBOutlet weak var sampleTextField: UITextField!
  6. // 中略
  7. private func setToolbar() {
  8.  
  9. let toolbar = UIToolbar()
  10. toolbar.sizeToFit()
  11.  
  12. let inputSwitchButton = UIBarButtonItem(title: "英数字入力に切り替え", style: .plain, target: self, action: #selector(self.inputSwitchButtonTapped(_:))) // 🍀変更
  13. let space = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
  14.  
  15. toolbar.items = [space, inputSwitchButton]
  16.  
  17. self.sampleTextField.inputAccessoryView = toolbar
  18. }
  19.  
  20. // 🍀追加
  21. @objc func inputSwitchButtonTapped(_ sender: UIBarButtonItem) {
  22. sender.title = "数字入力に切り替え"
  23. }
  24. }

inputSwitchButtonTapped()を定義して、その中でtitleの変更を行うようにしました。

さらに、inputSwitchButtonの#selectorに inputSwitchButtonTapped()を設定することで、
ボタンタップ時にtitleが「数字入力に切り替え」へと変更されるようになります。

ここまでで、実際の動きは以下のようになります。

1回タップした後は、特に変化がありませんね。

今回はtitleと一緒にKeyboard Typeも変更したいので、inputSwitchButtonTapped()を以下のように修正します。

  1. @objc func inputSwitchButtonTapped(_ sender: UIBarButtonItem) {
  2. // キーボードが表示されたままだとKeyboard Typeの変更が反映されないのでフォーカスを外す
  3. self.sampleTextField.resignFirstResponder()
  4. // 🍀以下の分岐を追加
  5. if self.sampleTextField.keyboardType == .numberPad {
  6. sender.title = "数字入力に切り替え"
  7. self.sampleTextField.keyboardType = .namePhonePad
  8. } else if self.sampleTextField.keyboardType == .namePhonePad {
  9. sender.title = "英数字入力に切り替え"
  10. self.sampleTextField.keyboardType = .numberPad
  11. }
  12. // Keyboard Typeを切り替えた後に再度フォーカスする
  13. self.sampleTextField.becomeFirstResponder()
  14. }

(フォーカスのつけ外しについては、コメントの通りです)
現在のKeyboard Typeを判定して、titleとKeyboard Typeが切り替わるようにしました。

完成!

完成形は、以下のようになります。




以上、UIBarbuttonItemのtitleを変更する方法について紹介しました。
参考になるとうれしいです。

,