Yesod と HDBC-mysql と haskell-relational-record で “Commands out of sync”
Yesod と HDBC-mysql と haskell-relational-record を一緒に使っているのだが、MySQL サーバーに「Commands out of sync; you can’t run this command now」って言われてぐぬぬってなって、最近デバッグをがんばっている。とりあえず途中経過をメモしておく。
そもそも MySQL プロトコルの呼び出し順とか知らないので苦戦していた。
この辺でエラーが出ている。結合をしなければエラーは出ない。
import Import hiding (on) import Database.Relational.Query (on) import qualified Model.Table.Account as Account import qualified Model.Table.Tweet as Tweet import qualified Model.Table.User as User getTweetR :: AccountIdParam -> TweetIdParam -> Handler Html getTweetR accountIdParam tweetIdParam = do user <- requireAuth p <- runRelational $ do accounts <- runQuery Account.selectAccount accountIdParam ts <- flip runQuery () $ relationalQuery $ relation $ do t <- query Tweet.tweet u <- query User.user on $ t ! Tweet.userId' .=. u ! User.id' wheres $ t ! Tweet.id' .=. value tweetIdParam return $ (,) |$| t |*| u return (accounts, ts) case p of ([account@(Account _ _ _ _)], [(tweet@(Tweet _ _ _ _ _), tweetUser@(User _ _ _ _))]) -> do form <- generateFormPost commentForm defaultLayout $ do headerWidget $ Just user tweetWidget account user tweet form _ -> notFound
とりあえず HDBC-mysql は FFI で libmysqlclient を呼んでるので C のレベルでどの関数を呼んでいるのかログに出してみる。コードはこんな感じ。めっちゃ地道にログを挟んでいった。一部引用すると下記。
mysql_stmt_prepare :: Ptr MYSQL_STMT -> CString -> CInt -> IO CInt mysql_stmt_prepare mysql a1 a2 = do s1 <- peekCString a1 hPutStrLn stderr $ "mysql_stmt_prepare " ++ (show mysql) ++ " " ++ (show s1) ++ " " ++ (show a2) cr <- mysql_stmt_prepare_ mysql a1 a2 hPutStrLn stderr $ "\t→ " ++ (show cr) return cr
そのログ出力が下記(別ページで開く)。同じ接続・同じステートメントに同じ色を塗った。
どうも mysql_stmt_execute
した後に mysql_stmt_fetch
せずに mysql_stmt_prepare
するとダメっぽい。C で確認してみると下記のコードのコメントアウトしてるところのありなしでエラーが出たり出なかったりするのでおそらく合ってる。
MYSQL_BIND result; char display_name[100]; unsigned long display_name_length; my_bool display_name_error; memset(&result, 0, 1); result.buffer = display_name; result.buffer_length = sizeof(display_name) * sizeof(display_name[0]); result.is_null = NULL; result.length = &display_name_length; result.error = &display_name_error; { my_bool error = mysql_stmt_bind_result(stmt, &result); if (error) { fprintf(stderr, "mysql_stmt_bind_result\n"); mysql_close(mysql); exit(1); } } // { // int fetch_result = mysql_stmt_fetch(stmt); // switch (fetch_result) { // case MYSQL_NO_DATA: // printf("mysql_stmt_fetch: MYSQL_NO_DATA\n"); // break; // case MYSQL_DATA_TRUNCATED: // printf("mysql_stmt_fetch: MYSQL_DATA_TRUNCATED\n"); // break; // default: // printf("mysql_stmt_fetch: %d\n", fetch_result); // } // printf("display_name: %s\n", display_name); // switch (result.buffer_type) { // case MYSQL_TYPE_BLOB: // printf("display_name type: MYSQL_TYPE_BLOB\n"); // default: // printf("display_name type: %d\n", result.buffer_type); // } // printf("display_name_length: %ld\n", display_name_length); // printf("display_name_error: %d\n", display_name_error); // } { my_ulonglong rows = mysql_stmt_affected_rows(stmt); printf("mysql_stmt_affected_rows: %ld\n", (long)rows); } { MYSQL_STMT *stmt2 = mysql_stmt_init(mysql); if (stmt == NULL) { fprintf(stderr, "mysql_stmt_init: error\n"); mysql_close(mysql); exit(1); } { const char * const stmt_str = "SELECT `email` FROM `user` WHERE `id` = ?"; int error = mysql_stmt_prepare(stmt2, stmt_str, strlen(stmt_str)); if (error) { fprintf(stderr, "mysql_stmt_prepare: error\n"); fprintf(stderr, "error message: %s\n", mysql_stmt_error(stmt2)); mysql_close(mysql); exit(1); } } }
この FFI 呼び出しがどういう経緯で呼ばれてるかを探すのはこれから。
接続を使い回すようにしたのがダメなのかもしれない。
解決しました。(2017.03.17)
@kakkun61 これですけど、 runQuery が lazy read なのが関係しているかも。lazy read で必要になる度に fetch されるのが気になります。runQuery' にすると改善したりしませんか?
— 日比野 啓 (Kei Hibino) (@khibino) 2017年3月16日
正格版の runQuery'
があったのですね。