Impossible WHERE noticed after reading const tables
Explainの結果の中に見かけない出力を見つけたので調べました。
↑の記事によると、「ユニークキーを使って絞り込んだ後、データが見つからなかった場合に発生する」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のリファレンスマニュアルをまためくると、
テーブルには、一致するレコードが最大で 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)
これによると、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でやりたかったことが少しわかってきた
勉強が進んできて、ようやく
で紹介されていることの意味がわかってきて嬉しくなったので、今日はバックグラウンドのスレッドでしばらく待ってから、 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のjpegをutf-8のJSONで読み込んで画像が途中からばけた
上記をふまえた注意点のコメントが入った雑なコードを置いておきます。
// 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が含まれていて欲しい
- 一括で取得するのは厳しいデータ量があることをいつも想定しておく
- ソートに使うパラメータがレスポンスに含まれる(サーバ側にリクエストを送り、サーバ側でソートしてから返す、というのでは応答が遅く感じるため)
- テストのためでいいので、作成できるものは削除できるようにしておいて欲しい
Roarみたいなのを使えば更新等の時の入力と出力揃えて、うまくできそうだけど、まあね、 実際は入力と出力のパラメータは綺麗に一致しなかったりするよね、みたいなのはあるけど、 入力同士、出力同士は揃ってないとめんどくさい。