ドライバからDBを操作するときはPreparedStatementを使う
今仕事で使っているコードではJavaからCassandraとMySQLを動かしているが、その両方で使っているのがPreparedStatement。
これはどういうものかというと、あらかじめクエリのひな形を用意しておいて、実際に投げるときに値をバインドするというもの。
例えばJavaのCassandra Driverなら、
PreparedStatement psSelectTestTable = session.prepare("SELECT * FROM test_table WHERE id = ?;");
のようにあらかじめクエリを宣言しておく。 そして投げるときは、
BoundStatement bdSelectTestTable = psSelectTestTable.bind("1");
ResultSetFuture resultSetFuture = session.executeAsync(bdSelectTestTable);
のようにする。PreparedStatementの"?"のプレースホルダ部分に1が入った状態で、クエリが投げられることになる。
このようなPreparedStatementに相当する機能は、Cassandra Driverだけの話ではなく、Pythonのmysqlclientなど、他のDBドライバにもある。
名前は異なれど、「クエリのひな形を用意して後から値をバインドする」という方式を取っているものが多いだろう。
PreparedStatementを使うと何が良いのかは、大きく2つある。
1つは、クエリをあらかじめ用意しておくことで、毎回コンパイルする手間を減らし、パフォーマンスを向上させること。
しかしこれは、単にクエリを1回投げるだけであれば意味がない。むしろ、アプリケーションのサーバサイドなど、同じクエリのひな形を何度も繰り返し使うときに有用となる。
この意味で、上にあげたようなCassandra DriverはPreparedStatementを積極的に使うべき例といえる。上の例ではクエリを1回しか投げていないが、Cassandraは非同期で多数のリクエストを処理するのが得意なDBなので、大規模データを高速に処理したい状況で使われることが多いためである。
もう1つは、SQLインジェクションの基本的な対策ができること。クエリの構文解析自体はプレースホルダの状態で行われ、それが通った後に値をはめ込んで実行する。このため、値の部分に悪意あるクエリが入力されたとしても、そもそもクエリとはみなされないため、実行されてしまうことがない。
CassandraのDELETEの振る舞いについて
Cassandraを障害時のバックアップとして使うにあたり、DELETE時の処理を考える過程で学んだこと。
DELETEしてもすぐには消えない
CassandraのDELETEコマンドは、RDBMSとは異なりすぐに消えるものではない。
ある行をDELETEしても、その行のデータはディスクに残り続ける。その代わり、tombstoneというフラグが立ち、ユーザが削除の指示をしたことがわかるようになっている。つまり、DELETEコマンドもまたUPDATEコマンドの一種にすぎない。
tombstoneが立った行は、スキーマでのgc_grace_seconds
で指定した秒が経過して初めてディスクから消去される。
これは、大量データの処理速度を最優先させるというCassandraの設計思想に基づいたものだ。
DELETEが起こるテーブルを設計するときのポイント
この性質は、テーブルの設計をする時にも考えなければいけない。
例えば、ある行をDELETEした後、残った行をSELECTでスキャンする場合。
このとき、gc_grace_seconds
以内であれば、DELETEした行はまだディスクには残っているため、当然スキャンの対象となる。「DELETEして行の数が減ったぞ」と思っていると、予想以上にSELECTに時間がかかってしまうことがある。
さらに、Cassandraはスキャンをする際に、tombstoneをコーディネーターに伝えるためメモリ上に保管しておく。このため、短時間に多くの行をDELETEしてtombstoneがたまると、パフォーマンスに影響を及ぼし、最悪の場合はサーバ上のヒープが吹っ飛ぶ可能性もある。
具体的な検証をまじえた解説は、Cassandra公式の以下のブログで行われている。
また、同じ値の主キーを何度もDELETE/INSERTする時も注意が必要になる。
これは特定の行を頻繁にUPDATEしていることと同じになる。ミリ秒単位でtimestampが同じになるほど頻繁にDELETE/INSERTをした結果、予想外の結果が返ってきたという例もあるようだ。
もちろんgc_grace_seconds
の設定があるのでtombstoneが永遠に残ることはないが、上記のようなことが短時間で頻繁に起こるのは良くない。
最も良いのは、削除したものも含めて常に主キーが一意となり、そして大量のスキャンをなるべく避けられるような設計のテーブルを作ることだろう。削除した行が再びSELECTやUPDATEの対象となる可能性を減らすことができれば、パフォーマンスへの影響も出にくくなる。
そのバグが本当にバグなのかを見極める
今の会社では、カスタマーサポートなど非エンジニアもイシューを上げられるようにしているが、彼らがバグとしてあげてきたイシューには、実は対応が不要なものも多く混じっている。
何から対応するか決めるときには、「バグ」と「仕様の問題」を混同しないように気をつけた方がいい。
- バグ: 仕様として想定されているのとは異なる動作をするということ。
- 仕様の問題: 動作自体は仕様の通り。ただしそれが特定のユーザにとって望ましくない可能性がある。
この2つは、その後の意思決定が大きく異なる。バグならもちろんすぐ潰すべきだ。
しかし仕様の問題なら、直そうとすると仕様変更になる。その変更が本当に全てのユーザにとって望ましい変更であるか、その問題はどれほど頻繁に発生しどれほど影響が大きいかなどを考えたうえで、対応するか見送るかを決めなければならない。
また、ごく特別な状況でしか起こらないという場合もありうる。
例えば、特定のブラウザでのみ発生する、特定の時刻でたまたま処理が重くなっていたためにエラーが起きる、などのようなことが実際にあった。このような場合、やはり即時対応はできない。
バグが上がってきた場合、まずはそれが本当にバグとして扱ってよいものかを立ち止まって考える必要があるだろう。
Pythonで深さ優先探索を実装する
前の記事で幅優先探索を実装したので、ついでにこちらも。
問題はAtCoder Typical Contest 001A。一応解説スライドはあるが、スタックを使った実装方法が書いてないので、自分でやってみることにした。
実装
from collections import deque def dfs(maze, visited, sh, sw): stack = deque([[sh, sw]]) visited[sh][sw] = 1 while stack: h, w = stack.pop() if maze[h][w] == "g": return "Yes" for j, k in ([1, 0], [-1, 0], [0, 1], [0, -1]): new_h, new_w = h+j, w+k if new_h < 0 or new_h >= H or \ new_w < 0 or new_w >= W: continue elif maze[new_h][new_w] != "#" and \ visited[new_h][new_w] == 0: visited[new_h][new_w] = 1 stack.append([new_h, new_w]) return "No" if __name__ == "__main__": H, W = map(int, input().split()) maze = [input() for _ in range(H)] for i in range(H): if maze[i].find("s") != -1: sh = i sw = maze[i].find("s") break visited = [[0]*W for _ in range(H)] print(dfs(maze, visited, sh, sw))
基本的には、幅優先探索の場合と流れはほぼ同じ。
ただし、今回は後入先出のスタックを使っているため、上下左右にマスが分岐する際に、どれかひとつをこれ以上進めなくなるまで調べ尽くす。
今回の問題のように、単にゴールまでのルートがあるかを素早く調べるには、深さ優先探索の方がよいだろう。
Pythonで幅優先探索を実装する
アルゴリズムの勉強のために、幅優先探索を書いてみた。
使ったのはAtCoder Beginers Contest 007Cの問題。この頃はアルゴリズムがそのまま出題されてたようだ。
特殊事項として、この問題ではスタートからゴールまでは必ず行くことができる前提がある。さらに周り中が壁で囲まれているので、盤面からはみ出すのを考慮する必要がない。
実装
from collections import deque def bfs(maze, visited, sy, sx, gy, gx): queue = deque([[sy, sx]]) visited[sy][sx] = 0 while queue: y, x = queue.popleft() if [y, x] == [gy, gx]: return visited[y][x] for j, k in ([1, 0], [-1, 0], [0, 1], [0, -1]): new_y, new_x = y+j, x+k if maze[new_y][new_x] == "." and \ visited[new_y][new_x] == -1: visited[new_y][new_x] = visited[y][x] + 1 queue.append([new_y, new_x]) if __name__ == "__main__": R, C = map(int, input().split()) sy, sx = map(int, input().split()) gy, gx = map(int, input().split()) sy, sx, gy, gx = sy-1, sx-1, gy-1, gx-1 maze = [input() for i in range(R)] visited = [[-1]*C for j in range(R)] print(bfs(maze, visited, sy, sx, gy, gx))
たった25行で書けた。
原理としては、今いるマスから上下左右に隣り合うマスの座標をキューに入れるとともに、visitedリストにも記録していくという繰り返し。
キューは先入先出なので、今いるマスから上下左右に隣り合うマスを全て探索した後、それらの上下左右に隣り合うマスを探索する、というように探索を行っていく。
全マス調べるため時間はかかるが、ゴールへの最短ルートを確実に求められる。
速度アップの工夫
実をいうと、上記のコードは2度目の提出だった。
その前に書いたバージョンでは、bfs関数を以下のようにしていた。
def bfs(maze, visited, sy, sx, gy, gx): queue = deque([[sy, sx]]) count = 0 while queue: #ここでforループを回している for _ in range(len(queue)): coord = queue.popleft() visited[coord[0]][coord[1]] = 1 if coord == [gy, gx]: return count for j, k in ([1, 0], [-1, 0], [0, 1], [0, -1]): next_coord = [coord[0]+j, coord[1]+k] if maze[next_coord[0]][next_coord[1]] == "." and \ visited[next_coord[0]][next_coord[1]] != 1: queue.append(next_coord) count += 1
visitedリストの値を0=未訪問と1=訪問のみとしていたため、bfs関数の中のwhile queue
以下でforループを回して、ループを何回まわったかをカウントしスタートからの距離を求めていた。しかし、テストケースの最初の4分の1くらいで実行時間が2秒を超え、あえなくTLEに。
そこで、visitedリストの値にスタートからの距離を直接書かせることにした。まずスタートマスの値を0、未訪問のマスの値を-1としておく。そして新しいマスを探索するときは、visitedリストから前にいたマスの値を取り出し、それに1を足した値を新たに書けばよい。
こうすることで、forループをひとつ減らすことができ、実行時間も全体で25msと大幅に速くなった。リストをうまく使うことで、回数カウント目的のforループをなくすというテクニックは、他の問題でも応用できそうだ。
teratail MANABIYAに行きました
ふだん質問者としても回答者としてもお世話になっているteratailが主催の、初めての大規模カンファレンスです。
会場は秋葉原にある廃校になった中学校です。ふだん勉強会といえばIT企業でやるのが当たり前なので、なんか新鮮な感じがします。
今日はとりあえず全部聞くのではなく、自分が聞きたくてたまらないと思うセッションに絞ってみました。参加したのは3つだけですが、とても満足してます。
技術者としての成長のための技術トレンド
わー人工知能だAWSだ競プロだといろいろ目移りして、どれも中途半端に終わってしまっているという課題感があったので、セッション中絶えず身につまされてました。
なんでも1万時間学べば100人に1人の人材になり、その軸が3つあれば100万人に1人の逸材になれるという有名な話があります。けれどそれは、1万時間(=仕事外だけだとだいたい10年)という膨大な時間を何に費やすかという選択でもあるわけです。
なので、新しく学ぶものは今メインにしている技術からなるべく離れていて、かつ心からワクワクできるものにすべきというのが、エンジニアが取るべき成長戦略になります。
自分が本当にやるべきこと・やりたいことを、目移りせずに絞っていけるようになりたいものです。
(ちなみに「本当は仕事外だけじゃなくてそれを仕事にしちゃうのが一番ですよ〜」とも言われました。身もふたもない)
2018年のWeb標準
Service Worker, Web Components, Web Paymentsのざっくりした紹介。人が部屋からあふれる盛況ぶりでした。
実際にそれぞれデモを見せてもらったのですが、特にWeb ComponentsとWeb Paymentsは衝撃的でした。
Web Componentsがあれば、<fancy-button>
みたいな独自のHTML要素を定義・再利用するのが当たり前になる!
Web Paymentsが普及すれば、決済フォームというものがなくなって、ECサイトの体験がまるきり変わるかもしれない!
という感じで、心の中で一人盛り上がってました。
もちろん、いろいろな細かい制約もたくさんあるでしょうし、そもそも本当に普及するかどうかもわからないのですが、わくわくするようなトレンドの種があれば、今のうちに勉強しておくのもいいかもしれないと思いました。
オブジェクト指向型言語と関数型言語
オブジェクト指向言語と関数型言語 | κeenのHappy Hacκing Blog
内容がとても多く、濃い密度で進んだセッションでした。
関数型プログラミングは全く知らないのですが、「オブジェクト指向はデータを抽象化し、関数型は処理を抽象化する」という説明はわかりやすかったです。
私含め世の中の多くのエンジニアは、オブジェクト指向プログラミングを当たり前のものとして日々過ごしていると思うのですが、それとは全く違うパラダイムがあり、しかもその世界は同じくらい広くて深いとわかると、やっぱりわくわくしますね。
その他
- 10%オフにつられてオライリーの技術書を買いました。重い。
- DataChainのブースで資料のかわりにどら焼きをもらいました。
- 給食風カレーうどんは本当に給食風でした。
- 空き時間にTwilio Challengeをやってマグカップをもらいました。APIをいじるのは楽しいです。
振り返ってみると意外と満喫してました。カンファレンスでの過ごし方がうまくなってきてるかもしれません。
次回があればまた参加したいです。
Javaでスレッドを作ってみる
JVMでは並行処理を実現するために、スレッドを作ることができる。
並列処理を使わなくてもよい処理を書いていたが、せっかくなので公式ドキュメントを見ながらスレッドを作ってみた。
スレッドを作る方法
スレッドを作るには2つの方法がある。ひとつはThreadクラスを継承してrunメソッドをオーバーライドすること。
公式に載ってた「初期値より大きい素数を計算する」コードを書いてみた。
class PrimeThread extends Thread { long minPrime; long nextNum; public PrimeThread(long minPrime) { this.nextNum = minPrime; } public void run() { System.out.println(calculate()); } public long calculate() { do this.nextNum += 1; while (!isPrime(this.nextNum)); return this.nextNum; } public boolean isPrime(long num) { if (num == 2) return true; if (num % 2 == 0) return false; for (int i = 3; i <= Math.sqrt(num); i += 2) { if (num % i == 0) return false; } return true; } } public class SampleThread { public static void main(String[] args) { PrimeThread primeThread = new PrimeThread(140); primeThread.start(); } }
もうひとつは、Runnableインターフェースを実装すること。
class PrimeRun implements Runnable { long minPrime; long nextNum; public PrimeRun(long minPrime) { this.nextNum = minPrime; } public void run() { System.out.println(calculate()); } \\ 以下同様なので中略 } public class SampleThread { public static void main(String[] args) { PrimeRun primeRun = new PrimeRun(140); new Thread(primeRun).start(); } }
2つのやり方の違い
この2つのやり方はどう違うのかと思ったけれど、そこまで違いはなさそうだ。
Threadクラスのstartメソッドのコードは以下のようになっている。
public synchronized void start() { if (threadStatus != 0) throw new IllegalThreadStateException(); boolean started = false; try { start0(); started = true; } finally { try { if (!started) { group.threadStartFailed(this); } } catch (Throwable ignore) { /* do nothing. If start0 threw a Throwable then it will be passed up the call stack */ } } } private native void start0(); @Override public void run() { if (target != null) { target.run(); } }
startメソッドは、C++で実装されたstart0メソッドを呼び出している。そしてstart0メソッドがrunメソッドを呼ぶのだが、このrunメソッドはオーバーライドされることが前提であり、親クラスの中の処理は何の意味も持たない。 ※ちなみにC++内でどんな処理をしているかは、以下の記事が詳しい。
Thread.javaのstart()はrun()をどのように呼ぶのか?(備忘録) - Qiita
一方Runnableインターフェースは、当然だが抽象メソッドとしてrunメソッドを定義しているだけだ。
public interface Runnable { public abstract void run(); }
どちらのやり方を使っても、結局runメソッドは全部自前のものを使うことになる。