Firebase Analytics for Flutter

Firebaseコンソールにて

  • アプリ登録
  • パッケージとハッシュを入力。ハッシュ必須
  • googleさんから頂いたgoogle-services.jsonファイルをプロジェクトにイン!
    • google-services.json >> [project > android > app > google-services.json]

gradleに追加

  • 指示がでたとーりにproject > android以下のbuild.gradleを修正
...
buildscript {
    repositories {
        google()
        jcenter()
    }

    dependencies {
        classpath 'com.android.tools.build:gradle:3.0.1'
        classpath 'com.google.gms:google-services:3.1.2'// add
    }
}
...
...
//add to last of build.gradle
apply plugin: 'com.google.gms.google-services'

pubspec.yamlにプラグインを追加

...
dependencies:
  flutter:
    sdk: flutter

  firebase_analytics: ^0.3.0
...

importは analyticsとanalytics observer

  • あとFutureが帰ってくるので、import 'dart:async';も必要になる
...
    import 'dart:async';
	import 'package:firebase_analytics/firebase_analytics.dart';
	import 'package:firebase_analytics/observer.dart';
...

初期化処理

FirebaseAnalytics analytics = new FirebaseAnalytics();// add

void main() => runApp(new MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Flutter Demo',
      theme: new ThemeData(
        primarySwatch: Colors.blue,
      ),
      navigatorObservers: <NavigatorObserver>[
        new FirebaseAnalyticsObserver(analytics: analytics)// add
      ],
      home: new MyHomePage(title: 'Flutter Demo Home Page', analytics: analytics),
    );
  }
}

使い方(すごく適当)

//for example to log
	//自由なイベント名でログを送信
    analytics.logEvent(name: "click_float");

	//パラメータにMapを追加してログを送信
	analytics.logEvent(name: "login_account", parameters: {"account": _googleSignIn.currentUser.email});

	//もともと用意されているイベント名を利用
	//例えばloginイベント
	analytics.logLogin();

動かないとき

  • package名!
  • ハッシュ!
  • build.gradle!2つとも!

Firebase > Analytics > DebugViewで見たいなら

adb -s [device] shell setprop debug.firebase.analytics.app [com.yourdomain.yourpackagename]


この記事のトラックバック用URL - http://mashi.exciton.jp/archives/329/trackback



Android開発時のAPKも実はしっかりと署名されてたりする

開発中のアプリも実は署名されてる

  • apkを端末やエミュレータで動かすにはたとえ AndroidStudioなんかで開発途中だったとしても署名が必要。
  • アプリの署名 | Android Studio

Android では、すべての APK はインストール前に証明書を使用してデジタル署名されている必要があります。

 

そんなのしたこと無い

  • 勝手に作られてるkeyfileで勝手に署名してくれてます。
  • リリース時は[Generate Signed APK]で明示的に署名をするが、それと同じことを勝手に裏でやってくれている

署名に使われてる証明書の場所

  • Windowsでも、Macでも、Linuxでも↓に相当する場所にあります。
  • $HOME/.android/debug.keystore

passwordとaliasは?

“”SDK ツールは事前に指定された名前とパスワードを使用してデバッグ キーストア/キーを作成します。
キーストア名 – 「debug.keystore」
キーストアのパスワード – 「android」
キーのエイリアス – 「androiddebugkey」
キーのパスワード – 「android」
CN – 「CN=Android Debug,O=Android,C=US」””

 

ハッシュ値が必要なんだけど

keytool -exportcert -list -v \
 -alias androiddebugkey -keystore ~/.android/debug.keystore
keytool -exportcert -list -v \
-alias androiddebugkey -keystore %USERPROFILE%\.android\debug.keystore
  • パスワードは[android]
  • MD5, SHA1, SHA256,を含めいろいろ情報が表示されます。

 

複数の環境で同じ署名にしたい

 


この記事のトラックバック用URL - http://mashi.exciton.jp/archives/294/trackback


FlutterのState, StatefulWidget, setState()について

StatefulWidget

  • StatefulWidgetがStateを内包する。
  • 状態を表す変数(ここではhogehoge)はStatefulWidgetではなくてStateが持つ
  • ここではbuild()もかかない
class MyWidget extends StatefulWidget {
  @override
  MyState createState() => new MyState();
}

 

 

State

  • StateはStatefulWidget(のサブクラス)ジェネリクスを持つStateを継承する
  • 状態を表す変数を持つ
  • レイアウトを構築するbuild関数をもつ。StatefulWidgetじゃなくてStateがレイアウトを構成する!
class MyState extends State<MyWidget> {
  int _hogehoge = 0; //状態を表す変数

  Widget build(BuildContext context) {
    return new Text("hogehoge is ${_hogehoge}");
  }
}

 

  • なんとなくStatefulWidgetがbuildをもっててレイアウトを構成していたほうが自然な感じはするが、オフィシャルではこういうやり方。

 

 

状態が変化した場合

  • StateのなかでsetStateを呼ぶ
  • 変数を変更してからsetState();しちゃいかんよ!
  • setStateに変数を変更するfunctionを渡すんだ!
class MyState extends State<MyWidget> {
  int _hogehoge = 0; //状態を表す変数

  void functionCalledByAnyone(){
    setState(() {
      _hogehoge += 1
    });
  }

  Widget build(BuildContext context) {
    return new Text("hogehoge is ${_hogehoge}");
  }
}

 

  • setStateについては変数を変更する前後でチェックが入ったり、適切なタイミングで変数の変更が行われるために、このような呼び方になっているみたい。
  • とはいっても、現状でこの程度のコードであれば何もしないfunction渡しちゃっても、別にフツーに動く。
  void functionCalledByAnyone(){
    _hogehoge += 1
    setState((){});
  }

 


どこのだれが変数を変更するか?


この記事のトラックバック用URL - http://mashi.exciton.jp/archives/268/trackback


SSH秘密鍵のSSH-AGENTをつかった管理ベストプラクティスを考えてみた

暗号化されたSSH秘密鍵のパスフレーズ入力を出来る限り省略、なおかつ安全に運用する

  • SSH秘密鍵のパスフレーズは必要
  • しかも11文字以上の長いものが推奨される
  • sshやgitのたびにパスフレーズをいちいち入力するのは大変
  • ssh-agentのプロセスを管理する
  • 必要なときにだけパスフレーズを入力(シェル起動時に入力とかしたくない)
  • ssh-addされた鍵を一定時間で自動消去

SSH秘密鍵にちゃんとパスフレーズをつけよう。

  • SSH接続に公開鍵認証を取り入れるのは当然ですが、秘密鍵にちゃんとパスフレーズつけてますか?
  • (※もちろんパスワードログインはちゃんとOFFにしてね!)
  • パスフレーズをめんどくさくて付けていない場合はPCやUSBメモリなんかに入った秘密鍵が漏れたら即アウトです。

パスフレーズちゃんとつけてます!

  • 何文字のパスフレーズを付けていますか?
  • 短いパスフレーズは↓くらいの時間で解除できるという実験結果もでてます。

SSH秘密鍵のパスフレーズは(つけるなら)11文字以上にしましょうねという話

CPU Intel Core i7 2.67GHz
(実験では1コアしか使っていない)

パスフレーズ文字数 解析にかかった時間
5文字 8秒
6文字 480秒
7文字 8時間
8文字 20日
9文字 3.3年
10文字 197年

ssh-agent

ssh-agentの問題点

  • 登録するときssh-addコマンドで秘密鍵ファイルを登録しなければいけない
  • 登録するとずっと生の秘密鍵が残ってしまい明示削除しなければずっと使える状態のまま
  • ssh-agentを一度起動すると、ログアウトしてもプロセスが残ったままになる
  • 解答された鍵ファイルはプロセスごとに残っているのでプロセスを再利用する必要がある

ssh-agentをプロセス管理して自動立ち上げ

  • .bashrcに書き込んで、シェル立ち上げ時にプロセスがなければ立ち上げる
  • 立ち上げたプロセスの情報を ~/.ssh-agent ファイルに保存
  • プロセスがあればそれを再利用
  • ファイルの有効時間を設定
#有効時間(好きな秒数に設定)
SSH_KEY_LIFE_TIME_SEC=3600
SSH_AGENT_FILE=$HOME/.ssh-agent
test -f $SSH_AGENT_FILE && source $SSH_AGENT_FILE > /dev/null 2>&1
if [ $( ps -ef | grep ssh-agent | grep -v grep | wc -l ) -eq 0 ]; then
    ssh-agent -t $SSH_KEY_LIFE_TIME_SEC > $SSH_AGENT_FILE
    source $SSH_AGENT_FILE > /dev/null 2>&1
fi

実際に使われるときにパスフレーズを入力させる設定

  • ~/.ssh/configのサーバー設定にAddKeysToAgent yes
  • .bashrcにてssh-agent -t secをしたのは.ssh/configには有効時間が設定できなかったため
    Host my-server
        HostName myhost.mydomain.jp
        User taro
        IdentityFile ~/.ssh/taro_secret_key
        AddKeysToAgent yes
    

ログオフの際にssh-agentのプロセスを終了させる。

このままだとログオフしても鍵が保存されたままになってしまうので、
ログオフ時にプロセスを自動で終了させる。

    ssh-agent -k

有効時間の前にssh-agentに登録されている鍵を削除する

  • 現在、登録されている鍵を一覧
    • ssh-add -l
  • 特定の鍵を削除
    • ssh-add -d [key_file_path]
  • 全削除
    • ssh-add -D


この記事のトラックバック用URL - http://mashi.exciton.jp/archives/250/trackback


AWSとGAEを比較してみた。

やりたいこと

アプリのバックエンドとしてAPIを提供するサーバーを構築したい。

ウェブアプリケーションへの拡張の可能性も高いと思うので、Scala & Play Frameworkで構築しようと思う。
データベースにApache等HTTPサーバー、Javaコンテナにローテーションバックアップ、動作監視等のスクリプトあたりが必要?
VPSに環境構築するのが安く済みそうではあるが、スタートアップなのでどのくらいの負荷がかかるかは分からない。
理想的には少しずつユーザーが増えて増えてスケールアウトしていく、、、、というイメージなので、VPSでは環境の載せ替えや、サーバーウェアのチューニングなどの業務にかなり手間を割かれそうな予感がする。
ということで少なくとも最初はPaaSでの運用をしたほうが地雷が少なさそう。
候補としてはAWSとGoogleCloudPlatform(GCP)が使いたいなーーということで比較してみた。

 

それぞれ必要な構成

AWS: ElasticBeanstalk(EB) & Amazon Relational Database Service (RDS) & S3
GCP: AppEngine(GAE) & Google Cloud SQL & Cloud Storage

AWSではEBを使ってJavaのアプリケーションをホストして、staticなファイルはS3においておく感じ。
GCPではGAEにホスト、StaticファイルはCloudStorage。
データベースはどっちでも機能的に不足はなさそう。

他の様々な”便利”なサービスを利用しないんであれば、どちらでもできることはほとんどかわらなさそう。

 

PaaSの比較(AWS EB vs GCP GAE)

EBはEC2の上で動くサービスなのでVM上にマネジメントされた環境を構築して、スケールするときはVMを新しく起動する形。
EBはEC2を使ったマネジメントシステムということなので、EC2でできることなら柔軟に色々できそう。
GAEではVMはつかわずアプリケーションコンテナのプロセスを起動する形なので、スケールアウトの時間がミリ秒レベルの速さになるよう。
ただし、VMがないので、SSH接続もファイルの書き込みもできないなどの制限がある。(ファイルはCloudStorageに保存すれば良さそうだけど)

料金は
GAE:0ドル (1インスタンス) : 30ドル(2インスタンス) : 322ドル(10インスタンス)
AWS:20ドル弱 (1インスタンス) : 38ドル(2インスタンス) : 190ドル(10インスタンス)

GAEのほうは無料枠がついてるのでインスタンス一つで設定すると無料になる!
その場合GAE送信転送量のみに課金される

インスタンスが少ない場合はGAEのほうが安いがインスタンスが多くなるとAWSのほうが安くなるようだ
AWSでインスタンスを10にするような場合はインスタンスの種類を買えるほうが良いかもしれないが、、、、

 

DBの比較(AWS RDS vs GCP Cloud SQL)

レスポンスはそんなに大きな差はなさそう。使い勝手に差も対してないだろうと思う。
つかえるRDBMSもそれぞれ色々使えるので問題はなさそう
料金は最小構成と容量10GBで見積もってみたところ
RDS: 20ドル強
CloudSQL10ドル弱

こちらもお値段ではGCPの方に軍配。

 

Storageの比較(AWS S3 vs GCP Cloud Storage)

https://thinkit.co.jp/story/2015/03/30/5801?page=0%2C1
速度計測をしたレビューがありました。日本にデータセンターが無いにも関わらずCloudStorageがダントツに早いようです。
料金は1TB確保して、割りと大きなファルを配信する設定にしました。
S3:34ドル程度
CloudStorage: 27ドル程度

データ転送量に対する課金の比較

AWSではAWS全体からの転送量
GCPではGAEの転送量とCloudStorageの転送量にたいしてそれぞれレートで料金がかかるようです。
GAEの転送量に関してはGAEの料金に含めてしまったのでここでは

どちらも受信に関しては料金が発生しない。

それぞれS3とCloudStorageに関しては1TB/月で
GAEの転送量に関しては重いデータはStorageに逃がすと考え50GB/月

AWS:150ドル(S3-1TB)
GCP: 61ドル(GCS-1TB) + 5.9ドル(GAE-50GB)

 

無料枠について

AWSではこのプランの場合、新規契約から12ヶ月は月44.8ドル割引が適用されるらしい。
GAEでは1日毎にそれぞれ無料枠があるので、アクセスが少ない場合などは0円運用なども視野に入ってくる。

 

まとめと特筆事項

この構成で、ほとんど両者に機能や性能の差はなさそう。
特にそれぞれのPaaSにロックインされてしまうのが困るので特殊な機能はあまり使いたくない気持ちもある。
上記の設定、それほど負荷がかからないがstaticなデータ転送が多い想定での構成で料金はそれぞれ以下のようになった。

AWS:初年度の月額182.06ドル 次年度から 226.86
GCP: 月額103.31ドル

あとは料金の比較になるが、スタートアップや小さいサービスであればGoogleに完全に軍配があがる。

リスクとしては、AWSが規模で大きくリードしているため、GCPがサービスを終了させる可能性などもすこし頭をよぎる。
またGCPに関して今までは国内にデータセンターがなかったため、米国のデーターセンターへのアクセスとなり、ターンアラウンドタイムが少し遅いというデメリットがあったが、2016年中には日本にGoogle Cloud Platformのデータセンターが作られるようだ。国内でイマイチ盛り上がりにかけるところもあるので、これも期待したい。米国内でもデーターセンターの追加もあるようなのでGCP全体で勢いがつけばいいなというところ。

とりあえず、なるべくサービスにロックインされないような仕様のシステムを構成し、GCPを選択しようかなと思っています。
実際の運用データがでたらそれをもとにまた比較をしてもいいかも。

 



この記事のトラックバック用URL - http://mashi.exciton.jp/archives/241/trackback


洗濯表示アプリがR25スマホ情報局に紹介されました!

Android用のアプリ「洗濯表示」がR25スマホ情報局に紹介されました!

 

洗濯表示アプリ

 

衣類を傷めず自分で洗濯をする際にチェックしておく必要がある、タグに記載されたJISの「洗濯表示」。よく調べずに洗濯してしまい、衣服が縮んだり伸びたりしてしまった経験がある人も多いでしょう。そんな悲劇を繰り返さないために役立ててほしいのが、この「洗濯表示 -新洗濯マーク対応-」です。

 

2016年12月から採用される、新しい洗濯表示全41種類に対応している点が大きな特徴。アイコンをタップすると画面下段に意味が表示されるので、洗濯方法を間違えてしまうリスクが回避できます。特に、新しい洗濯表示は見慣れないアイコンが多いので、このアプリが頼りになるはずです。ちなみに、新しい洗濯表示は国際基準に則ったものなので、海外製衣類の洗濯法もわかるようになりますよ。

 

もちろん現行の洗濯表示(全22種)にも対応しているので、これまでに購入した衣類の洗濯法を調べることも可能。機能自体はシンプルですが、実用度はかなり高いこのアプリ。大切な衣類を洗濯する際は、ぜひ活用してください。

アプリでわかる!衣類の正しい洗濯法 | R25スマホ情報局
http://smartphone.r25.jp/app-use/141752

 

 

 

ハウツーアプリの方にも紹介いただきました。

R25スマホ情報局

冬の衣類や毛布などを自宅で洗濯しようと思った時、気になるのが洗濯方法。もしタグにわからない「洗濯表示」のアイコンがあったら「洗濯表示 -新洗濯マーク対応-」でチェックしてみましょう。アイコンをタップするだけで洗濯法を教えてくれるので、衣類を傷めてしまう心配ともおさらば。2016年から採用される新しい洗濯表示にも対応しています。

小さな疑問を解決!暮らしハウツーアプリ | R25スマホ情報局
http://smartphone.r25.jp/app-use/theme-141754

 

どうもご紹介ありがとうございます!

 

 

【洗濯表示 -新洗濯マーク対応-】アプリのダウンロードはGoogle Play Storeからもどぞーー


この記事のトラックバック用URL - http://mashi.exciton.jp/archives/225/trackback


Slick3のSQLクエリを複数結合する

Slickにて、クエリを連続的に結合する方法についてのまとめ
忘備録的なものなので、用語に関してなど、間違っているところがあるかもしれないです。
ツッコミしていただけたらありがたいです。

環境

  • play2.4
  • Slick3.0
  • mysql


Slickでクエリを発行する際に、同一のWeb Requestの中でdb.runを何回も発行するとトランザクションが別になってしまうので、片方のトランザクションだけが失敗してしまって原子性が保たれないなどの場合がある。
同一トランザクションにするためにはすべてのクエリ(DBIOAction)を一つにまとめる必要がある。


データベースに対する操作はDBIOActionと呼ばれる

  • accountTable.schema.create
  • accountTable.map(_.userId === 1).result
  • accountTable += Account(1,”name”,”email)
  • (users returning users.map(_.id)) += User(None, “Stefan”, “Zeiger”)


これらはすべてDBIOActionであり、db.run( DBIOAction ) と実行できる類のものの事になります。

これらを同一のトランザクションとして処理したい場合、単純な方法としてはマニュアルに乗っているようにすればよい。
DBIO.seqの中に全部放り込めば良いので、上記のDBIOActionを例にすると以下のようになる。

Slick3.0.3 Manual
http://slick.typesafe.com/doc/3.0.0/gettingstarted.html#populating-the-database

val setup = DBIO.seq(
  	accountTable.schema.create,
	accountTable.map(_.id == 1).result,
	accountTable += Account(1,"name","email),
	(accountTable returning accountTable .map(_.id)) += Account(1,"name","email)
)
val setupFuture = db.run(setup.transactionally)

これですべてのクエリが実行されるわけだが、クエリの結果は取得することができない。ひとつとして。

さらに実際はSelectした結果をInsertしたい場合もあるだろうし、AutoincrementされたIdを元に他のレコードをUpdateしたい場合など、
クエリの結果を取得したうえで次のクエリを行うには次のように考えれば良い。


参考にしたいのがマニュアルにあるこちらのサンプル
for yield 文を使ってうまくクエリの結合をしている。

Slick3.0.3 Manual
http://slick.typesafe.com/doc/3.0.0/dbio.html#transactions-and-pinned-sessions

val a = (for {
  ns <- coffees.filter(_.name.startsWith("ESPRESSO")).map(_.name).result
  _ <- DBIO.seq(ns.map(n => coffees.filter(_.name === n).delete): _*)
} yield ()).transactionally

val f: Future[Unit] = db.run(a)

もう少し単純でわかりやすい形のサンプルにしてみる(これは実際に役に立つ例ではないが)

val coffees = TableQuery[CoffeeTable]
val a = (
  for {
    gotIds <- coffees.filter(_.type === "ESPRESSO").map(_.id).result
    gotCoffees <- coffees.filter(_.id === gotId.head).result
} yield (gotCoffees))

val f: Future[Seq[Coffee]] = db.run(a.transactionally)

このサンプルではまず3行目でtypeが“ESPRESSO”のレコードのIDをすべて取得する。そして取得されたすべてのidが左に示したgotIdsにSeqとして代入される。
次4行目でgotIdsと一致するレコードをCoffeeテーブルから取得する。それが左に示したgotCoffeesに代入される。(ここではheadを使い取得したgotIdの最初の1つと一致するものを取得している)
そして最後 yield に指定されたgotCoffeesが最終的にFutureから取得できる結果となり、Seq[Coffee]である。
もしyield(godIds)とすれば最後に取得できる値はSeq[Int] になる。
両方欲しければ yield((gotCoffese, gotIds)) とすれば両方の値が(Seq[Coffee], Seq[Int])とタプルで取得できるようになる。


まとめると
forの中の各行にクエリを記述し、取得される値を左辺の変数に代入、
次の行では同様にクエリを記述でき、前の行で取得した変数のリストを利用することもできる。
これをいくつも繰り返すことができるような形になっている。
そして、取得したい結果を左辺の変数より、yield に記述する。

そして最後にdb.runするまえにtransactionallyを指定しておけばコミット処理やロールバックを自動で行ってくれるため、原子性が守られることになる。


もう一つ少し実用的な応用のサンプルも一つ挙げてきます。
ESPRESSOの種類のCoffeのIDを取得し、時間とともにオーダーに加えるというサンプルです。
取得したIDを元にOrderインスタンスを作成しinsertするようなことも一つのトランザクションで可能です。

val coffees = TableQuery[CoffeeTable]
val orders= TableQuery[OrdersTable]
val a = (
  for {
    gotIds <- coffees.filter(_.type=== "ESPRESSO").map(_.id).result
    _ <- orders += Order(gotIds.head, "order time")
} yield ())

val f: Future[Seq[Unit]] = db.run(a.transactionally)

ところで、なぜこれらのクエリーがfor yield文で記述されるのか気になった方もいると思います。
for yieldはscalaではflatMap関数に変換されて処理されるところがポイントになります。

このfor yield文をflatMap関数で表していくと次のように表すことができます。

coffees.filter(_.type=== "ESPRESSO").map(_.id).result.flatMap( gotIds:Seq[Int] =>
    orders += Order(gotIds.head, "order time").map( _ => ... )

いままでのクエリがどのように組み立てられているか、なんとなくわかったのではないでしょうか。


DBIOAction.flatMap(result1:Any =>
  DBIOAction.flatMap(result2:Any => 
    DBIOAction.flatMap(result3:Any =>
      DBIOAction.flatMap(result4:Any => 

こんなかんじでクエリが作られていくようです。
この辺の仕組みがわかると、ようやくSlick3と仲良くなれたような気がしました。



この記事のトラックバック用URL - http://mashi.exciton.jp/archives/189/trackback


SlickにてAutoincrementされたIDを取得して別テーブルにInsert

SlickでAutoincrementされたIDを追加時に取得し、そのIDを使用して他のテーブルにもレコードを追加する方法
例えばAccountにユーザーを追加、IDはautoincrementされるが、そのIDを利用して他のテーブルにもIDに結びついたレコードを追加したいような場合。
の方法。


環境は
  • play2.4
  • Slick3.0
  • mysql


普通にレコード追加して、そのIDを取得する方法としてはオフィシャルドキュメントに載ってるとおり

Slick3.0.3 Manual
http://slick.typesafe.com/doc/3.0.3/queries.html#inserting

val userWithId =
  (users returning users.map(_.id)
         into ((user,id) => user.copy(id=Some(id)))
  ) += User(None, "Stefan", "Zeiger")

db.action(userWithId.transactionally)

問題ははこの取得したIDを使って他のレコードを追加すること。
トランザクションが別になっても良いのであれば難しいことは何もないのだが、
同一のトランザクションで行うには次のようにすればよい。

def(account: models.Account, another: models.Another){
  val action = (
    for {
      newId <- (accountTable returning accountTable.map(_.userId) )+=account
      _ <- anotherTable += another.copy(userId = newId)
  } yield newId )
  db.action(action.transactionally)
}

4行目でレコードを追加して、autoincrementされたnewIdを確保
5行目でanotherモデルのuserIdプロパティにnewIdをセット(関数型プログラミングならコピーで。)してInsert

これで同一トランザクションで処理できる
ほかの処理を追加したい場合はforの中に追加していく形でできる



この記事のトラックバック用URL - http://mashi.exciton.jp/archives/182/trackback


Slickで削除済みテーブルを使った削除を実装

Slickにて、レコード削除の際、削除したレコードを削除済みテーブルに追加する方法と
それを復活させる方法、
また管理の際に、通常テーブルと削除済みデーブルを結合して取得する方法について。

環境は

  • play2.4
  • Slick3.0
  • mysql

SQLのレコードを削除したいけど、削除済みのデータも残しておきたい。
そんなことも度々あると思うんですが、論理削除として削除フラグをテーブルに作るのも後々管理がしにくい、、ということで
通常テーブルと削除済みテーブルを2つ作ってしまえばいいじゃない!
そして、通常の処理では通常テーブルだけを参照して、
管理画面なんかで見る時だ通常テーブルと削除済みテーブルをUnionするという方法をとっています。
SQLで書くなら

通常の処理

  SELECT * From Account;

管理画面での処理

 (select *, false as Exist from Account) 
   union
 ( select *, true as Exist from AccountDelete)
   order by userId Desc;

こんな感じにしたいわけです。


ここから本題。
まず通常テーブルと削除済みテーブルの定義は以下のとおりで、通常テーブルについているO.AutoInc以外はほとんど同じです。

//通常テーブル
  val table = TableQuery[AccountTable]
  class AccountTable(tag: Tag) extends Table[Account](tag, "Account") {
    def userId = column[Int]("userId", O.PrimaryKey, O.AutoInc)
    def email = column[String]("email")
    def password = column[String]("password")

    def * = (userId.?, email, password) <>(Account.tupled, Account.unapply _)
  }

//削除済みテーブル
  val deleteTable = TableQuery[AccountDeleteTable]
  class AccountDeleteTable(tag: Tag) extends Table[Account](tag, "AccountDelete") {
    def userId = column[Int]("userId", O.PrimaryKey)
    def email = column[String]("email")
    def password = column[String]("password")

    def * = (userId.?, username, email, password) <>(Account.tupled, Account.unapply _)
  }

まず削除処理のSlick3での書き方

  def delete(userId: Int): Future[Unit] = {
    val act = {
      for {
        rows <- table.filter(_.userId === userId).take(1).result
        _ <- DBIO.seq(deleteTable += rows.head,
                      table.filter(_.userId === userId).delete)
      } yield()
    }
    db.run(act.transactionally)
  }

userIdに相当するレコードを探してきて、それを削除済みテーブルに追加して、通常テーブルから削除しています。
単純に3つのクリエをそれぞれに実行してもできますが、
一連の処理を一つのトランザクションで行いたいのでこういう形になります。


レコード復活の場合もほぼ同様ですが、UserIdをそのまま戻したいので、insertOrUpdateを利用しています。

  def restore(userId: Int): Future[Unit] = {
    val act = {
      for {
        rows <- deleteTable.filter(_.userId === userId).take(1).result
        _ <-DBIO.seq( table.insertOrUpdate(rows.head),
                         deleteTable.filter(_.userId === userId).delete)
      } yield ()
    }
    db.run(act.transactionally)
  }

通常の処理ではAccountTableのみを見ればよいですが、管理画面などでは削除されたものとそうでないものをまとめて、しかもID順に並び替えて閲覧したかったので、次のようにしました。
まず二つのテーブルを存在フラグのカラムを追加したうえで結合して、userId順に並び替えています。

  def selectAllWithDelete(): Future[Seq[AccountWithDeleteFrag]] ={
    val tbl = table.map(x => (x.userId.?, x.username, x.email, x.password, LiteralColumn[Boolean](true)) )
    val tblDelete = deleteTable.map(x => (x.userId.?, x.username, x.email, x.password, LiteralColumn[Boolean](false)))
    val union = (tbl union tblDelete).sortBy(_._1.desc)
    db.run(union.result).map(_.map(AccountWithDeleteFrag.tupled(_)))
  }
LiteralColumn[Boolean](true))

を使って、テーブルの定義に対し値を設定したカラムを1つ追加したレコードを取得しています。
それぞれののテーブルからの取得クエリを union して sortby しています。名前がないので、1番目を指定。
取得できるレコードは元のAccountレコードとは構造が変わってきますのでタプルのまま取得してもいいですし、
上記のように新しくbooleanのプロパティを増やしたcase class を作っても良いと思います



この記事のトラックバック用URL - http://mashi.exciton.jp/archives/177/trackback