woshidan's loose leaf

ぼんやり勉強しています

Impossible WHERE noticed after reading const tables

Explainの結果の中に見かけない出力を見つけたので調べました。

d.hatena.ne.jp

↑の記事によると、「ユニークキーを使って絞り込んだ後、データが見つからなかった場合に発生する」Extraのメッセージらしいです。

MySQLのドキュメントによりますと、

MySQL :: MySQL 5.6 リファレンスマニュアル :: 8.8.2 EXPLAIN 出力フォーマット

仕事で使ってるのが5.7でなくてすみません...。さて、上記のリファレンスマニュアルによりますと、

Impossible WHERE noticed after reading const tables MySQL はすべての const (および system) テーブルを読み取り、WHERE 句が常に false であることを通知します。

ということらしい。ところで、constテーブルって何ということで、MySQLのリファレンスマニュアルをまためくると、

const

テーブルには、一致するレコードが最大で 1 つあり、クエリーの開始時に読み取られます。行が 1 つしかないため、この行のカラムの値は、オプティマイザの残りによって定数とみなされることがあります。const テーブルは、1 回しか読み取られないため、非常に高速です。

const は PRIMARY KEY または UNIQUE インデックスのすべてのパートを定数値と比較する場合に使用されます。次のクエリーでは、tbl_name は const テーブルとして使用できます。

SELECT * FROM tbl_name WHERE primary_key=1;

SELECT * FROM tbl_name
  WHERE primary_key_part1=1 AND primary_key_part2=2;

ということで、PRYMARY_KEYやUNIQUEインデックスに指定した列からできているテーブルみたいなもの(それをindexと...(ry)なんだと思います。

それで、くだんのメッセージは、インデックスの列について、全部スキャンしたけど、インデックスの列の時点で全部falseだったので、これ以上WHEREを走らせることはできないよ、と言われているみたいです。

SwipeRefreshLayoutの中にあるRecyclerViewにLayoutManagerをセットする前にタッチすると落ちる

SwipeRefreshLayoutの中にRecyclerViewのあるFragmentがあって、 データの読み込みを待ってから、Adapterなどをセットしようと考えていたら、 データが読み込まれる前にRecyclerViewの部分をタッチすると落ちてしまっていた。

E/AndroidRuntime: FATAL EXCEPTION: main
    java.lang.NullPointerException
    at android.support.v7.widget.RecyclerView.computeVerticalScrollOffset(RecyclerView.java:1613)
    at android.view.View.canScrollVertically(View.java:11214)
    at android.support.v4.view.ViewCompatICS.canScrollVertically(ViewCompatICS.java:35)
    at android.support.v4.view.ViewCompat$ICSViewCompatImpl.canScrollVertically(ViewCompat.java:1253)
    at android.support.v4.view.ViewCompat.canScrollVertically(ViewCompat.java:1695)
    at android.support.v4.widget.SwipeRefreshLayout.canChildScrollUp(SwipeRefreshLayout.java:646)
    at android.support.v4.widget.SwipeRefreshLayout.onInterceptTouchEvent(SwipeRefreshLayout.java:660)
    at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:1822)

http://stackoverflow.com/questions/27416834/app-crashing-when-trying-to-use-recyclerview-on-android-5-0

これによると、RecyclerViewはLayoutManagerを設定していない状態でタッチすると落ちるみたいです。

ReyclerViewだけが置いてあっても特にデータやAdapterがなければレイアウト上に配置もされない*1ので、触っても落ちることはなく、ちょっと切り分けができてませんでした!!

単体だけあると配置されないのに、SwipeRefreshLayoutなどスクロールするViewGroupの中に置くと、Adapterなどを設定する前に配置されてしまうようです。

なので、いつもそうなんですが、SwipeRefreshLayoutなどのViewGroupの中に設置する場合は特に、 データの取得より前にとりあえずonActivityCreated()あたりで、LayoutManagerをセットしておいたほうがよさそうです。

// 触ると落ちるコードの例
package com.example.woshidan.donttouchrecyclerview;

import android.os.Bundle;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.Snackbar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.Toolbar;
import android.view.View;
import android.view.Menu;
import android.view.MenuItem;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // RecylerViewにLayoutManagerも何もセットしない
    }
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"
    tools:showIn="@layout/activity_main"
    tools:context="com.example.woshidan.donttouchrecyclerview.MainActivity">
    <android.support.v4.widget.SwipeRefreshLayout
        android:id="@+id/swipeRefreshLayout"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <android.support.v7.widget.RecyclerView
            android:id="@+id/recyclerView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"></android.support.v7.widget.RecyclerView>
    </android.support.v4.widget.SwipeRefreshLayout>
    <TextView
        android:text="Hello World!"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
</RelativeLayout>

*1:E/RecyclerView: No adapter attached; skipping layoutとログに出ます

RxJavaでやりたかったことが少しわかってきた

勉強が進んできて、ようやく

qiita.com

で紹介されていることの意味がわかってきて嬉しくなったので、今日はバックグラウンドのスレッドでしばらく待ってから、 UIスレッドでログを出す & それをボタン押して止める、みたいなサンプルを書きました。わいわい。

public class MainActivity extends AppCompatActivity {
    private Subscription mSubscription;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        mSubscription = Observable.from(new Integer[]{1,2,3,4,5})
                .map(new Func1<Integer, Integer>() {
                    @Override
                    public Integer call(Integer integer) {
                        try {
                            Thread.sleep(1000l);
                        } catch (InterruptedException e) {
                        }
                        return integer;
                    }
                })
                .repeat()
                .subscribeOn(Schedulers.newThread())
                .observeOn(AndroidSchedulers.mainThread())
        .subscribe(new Action1<Integer>() {
            @Override
            public void call(Integer integer) {
                System.out.print("replay ex BEGIN: ");
                for (int i = 0; i < integer; i++) {
                    System.out.print("*");
                }
                System.out.println(" :END");
                Log.d("replay ex", "current Time:" + System.currentTimeMillis() + "ms");
            }
        });

       FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
        fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Log.d("replay ex", "unsubscribe");
                mSubscription.unsubscribe();
            }
        });
    }

日付っぽい文字列を日付にする

SET @TEST_STR='xxx: yyy, datetime: 2016-03-25 22:22:16 +0900';

select STR_TO_DATE(SUBSTR(@TEST_STR, INSTR(@TEST_STR, 'datetime') + 10, 25), '%Y-%m-%d %H:%i:%s');
+--------------------------------------------------------------------------------------------+
| STR_TO_DATE(SUBSTR(@TEST_STR, INSTR(@TEST_STR, 'datetime') + 10, 25), '%Y-%m-%d %H:%i:%s') |
+--------------------------------------------------------------------------------------------+
| 2016-03-25 22:22:16                                                                        |
+--------------------------------------------------------------------------------------------+
1 row in set, 1 warning (0.00 sec)

こういうのをつかわなくていいように、テーブルを作ろう!!

RetrofitのファイルDLでハマった話

原因をはっきり検証したわけでなく体感的なメモに近いのですが、対応していて面白かったので、適当に業務に関する部分のコード削りながら、こっちに置きます。

  • Retrofitの通信処理をバックグラウンドに指定して、InputStream型のレスポンスの処理をメインスレッドに指定したらandroid.os.NetworkOnMainThreadException
    • レスポンスボディのログを吐いた場合はエラーが出ないが、レスポンスボディのログを吐かないとエラー
    • おそらくInputStreamを受け取った時点では全部のデータをサーバから受け取っていなくて、バッファーに読み込むときに足りなくなったらサーバからデータを追加で読み込んでいるとかでは
      • ログを吐いた場合エラーがでないのはログを吐くために一旦データをすべて読み込んでいるため?
      • 同じような原因で、読み込めたバイト数を指定せずにFileOutputStream#writeメソッドを呼んでいたらサーバ上のファイルサイズとDLしたファイルサイズがずれた
  • binaryのjpegutf-8JSONで読み込んで画像が途中からばけた

上記をふまえた注意点のコメントが入った雑なコードを置いておきます。

// Kotlinです
apiClient.downloadItemFile(itemId)
        .map {  response ->
            var downloadFile = writeResponseStreamToFile("/absolute/to/path", response)
            downloadFile // InputStreamを扱っている部分もバックグラウンドで処理されるようにobserveOnの置き所に注意
        }.map { downloadFile ->
            downloadFile?.let {
                insertContentProvider("fileName", "/absolute/to/path")
            } // ここはもしかしたらメインスレッドでもいいかも
        }
        . subscribeOn(Schedulers.newThread())
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe( ... )

// ApiClient#downloadItemFile()のインターフェイス
        @Streaming
        @GET("/items/{itemId}/download")
        fun downloadItemFile(
                @Path("itemId") itemId: Int,
                // RetrofitでJSON以外のファイルを扱う場合はContentTypeヘッダの設定を行うこと
                @Header("Content-Type") contentType: String = "image/jpg; charset=binary"
        ): Observable<Response>

// absoluteFilePathで渡されたパスのファイルへ、Responseのバイト列を書き込む
private fun writeResponseStreamToFile(absoluteFilePath: String, response: Response) : String? {
    var inputStream : InputStream? = null
    var outputStream : FileOutputStream? = null

    try {
        if (response.status == 200) {
            inputStream = response.body.`in`()
            var buff = kotlin.ByteArray(4096)
            val fileSize = response.body.length()
            var downloadedSize = 0L
            var readByte = 0

            outputStream = FileOutputStream(absoluteFilePath)

            while(downloadedSize < fileSize) {
                // InputStreamにすべてのデータがのっていない場合は、
                // ネットワーク上から追加でファイルを読み込みながらbuffにバイト列を書き込むので
                // 書き込めるバイト数は不定になるので注意
                readByte = inputStream.read(buff)
                if (readByte == -1) {
                    break
                }
                outputStream.write(buff, 0, readByte)  // バイト数はちゃんと指定しましょう
            }
            outputStream.flush()
        }
    } catch (e: IOException) {
        e.printStackTrace()
    } finally {
        inputStream?.close()
        outputStream?.close()
        return absoluteFilePath
    }
}

// fileNameで渡されたファイル名のファイルをContentProviderに登録
private fun insertContentProvider(fileName: String, absoluteFilePath: String) {
    var values = ContentValues()
    val contentResolver = mContext.contentResolver
    values.put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
    values.put(MediaStore.MediaColumns.TITLE, fileName)
    values.put("_data", absoluteFilePath);
    contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
}

http://shinh.hatenablog.com/entry/2016/03/11/142748

まずあまりマジメに相談せず勝手にやるってこと。相談すると、5割くらいの確率で「俺がやってるプロジェクトでその問題は解決するよー」とかいう返事が帰ってくる気がする。やる気が損なわれる。「それうまくいかないと思うよ」て言われてやっぱり失敗したら恥ずかしいよね。明らかな結論が出ない場合、どっちのアプローチが優れてるかとか机上で考えるのも時間のムダ。1週間から1ヶ月しか投資しないなら、作ってみて比べりゃいいんで。

とりあえずやってみたいし、作ってみたいし、作ってみて比べてみる is よさ。

アプリ側で使っていてAPIについて思ったこと

今度自分が書くとき用に。

  • 副作用のある操作がなく参照/バリデーション(登録などややこしい処理の場合)ができる
    • ユーザーの情報を確認したら一部情報が更新されるタイプのAPI,やや辛い
  • 一覧ではなく単体でデータの参照ができる
    • だいたいリソースは単体で404か確認したくなるものなので、やや辛い
  • 同じ形式と人間が思うデータが同じ型の変数で返ってくる(日付はDate型のStringかLong型)
    • クライアント側で対応できなくもないけれど、やや辛い
  • 更新と作成のときに渡すパラメータが同じである
    • クライアント側で対応できなくもないけれど、罠感があってやや辛い
  • 同じモデルのJSONは同じ形式をしている
    • クライアント側で他のモデルから補ったりすることになるので、やや辛い
  • やたら?や特殊文字がパラメータ名に入らない
    • 事前に対応できるかどうか調査してから
  • 一覧のデータはページネーション用のAPIが含まれていて欲しい
    • 一括で取得するのは厳しいデータ量があることをいつも想定しておく
  • ソートに使うパラメータがレスポンスに含まれる(サーバ側にリクエストを送り、サーバ側でソートしてから返す、というのでは応答が遅く感じるため)
  • テストのためでいいので、作成できるものは削除できるようにしておいて欲しい
    • アカウント登録など、手動でテストしたいときとても面倒...
    • デバッグ用画面やデバッグ用でいいので追加/削除の対になるAPIは欲しい...

Roarみたいなのを使えば更新等の時の入力と出力揃えて、うまくできそうだけど、まあね、 実際は入力と出力のパラメータは綺麗に一致しなかったりするよね、みたいなのはあるけど、 入力同士、出力同士は揃ってないとめんどくさい。

ruby-rails.hatenadiary.com