【Node】mysql8 利用時 接続が失敗する
現象
こちらのパッケージを利用した際、接続先クライアントが mysql8 だった場合に以下のエラーが発生
www.npmjs.com
Error: ER_NOT_SUPPORTED_AUTH_MODE: Client does not support authentication protocol requested by server; consider upgrading MySQL client
原因
MySQL 8.0からパスワードの認証形式が変更され、ライブラリ側で互換対応がされていない。
解決方法
古いパスワード認証を利用する
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'password';
一応 issueも上がっているので そのうち発生はしなくなる
https://github.com/mysqljs/mysql/pull/1962
mysql sql 備忘録
CREATE USER
CREATE USER 'username'@'localhost' IDENTIFIED BY 'password'; GRANT ALL ON *.* TO 'username'@'localhost'; flush privileges;
CREATE TABLE
CREATE TABLE ${TABLENAME}( ${FIELDS} );
Option
制約 | コマンド |
---|---|
オートインクリメント | AUTO_INCREMENT |
一意制約 | PRIMARY KEY |
外部キー制約 | foreign key(${COLUMN}) references ${PARENT TABLE}($COLUMN) |
既存テーブルからCREATE文を作成
SHOW CREATE TABLE
チーム開発用APIスタブの紹介
マイクロサービスが進む中、APIのスタブの需要は非常に高くなっています。
それに伴い様々なAPI スタブが公開されており、
昔よりも遥かに簡単にスタブを用意することができる環境になっています。
しかしながら、意外と実際に利用して開発を進めていくと
以下の問題に ぶつかることが多くなりました。
さらに個人での開発であれば諦めて 逐次変更を加えることも出来ますが
チーム開発だと スタブをデプロイする必要があり
変更の度にデプロイが発生し非常に辛い思いをするケースが多発します。
その経験から 効率的な開発のために
エンドポイントの切替なしに レスポンス管理できる機能を持ったスタブを今回実装しました。
利用方法に関してはReadmeに任せるとして
主な機能として以下の機能を提供しています
もし、自分と同様の問題を抱えている場合は
導入を検討してみてもいいかもしれません。
( そして要望が出た際に issueを上げてもらえると嬉しいです )
【SpringBoot】 RestAPI チュートリアル
業務で急遽SpringBootを使うようになったので簡単に実装方法を纏めるため、 今回は簡単な書籍管理用のRestApiを作成してみます。
事前準備
EclipseMarketPlaceで以下のプラグインを追加しましょう。
Spring Tool Suite marketplace.eclipse.org
Gradle IDE marketplace.eclipse.org
MySQLでテーブル作成 アプリケーションで利用するデータベースを作成します。 今回はspringというdatabaseを作成していきます。
$ CREATE DATABASE spring $ USE spring
データベースの作成が出来たらBookテーブルを作成します。
$ CREATE TABLE `Book` ( `id` int(20) NOT NULL AUTO_INCREMENT, `title` varchar(20) DEFAULT NULL, PRIMARY KEY (`id`) )
プロジェクト作成
適当なディレクトリで build.gradle ファイルを作成します。 今回は /Users/${user}/eclipse-workspace/SpringRestAPI 配下で作成します。
buildscript { ext { springBootVersion = '1.5.8.RELEASE' } repositories { mavenCentral() } dependencies { classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") } } apply plugin: 'java' apply plugin: 'eclipse' apply plugin: 'org.springframework.boot' group = 'product' version = '0.0.1-SNAPSHOT' sourceCompatibility = 1.8 repositories { mavenCentral() } dependencies { compile('org.springframework.boot:spring-boot-starter-web') testCompile('org.springframework.boot:spring-boot-starter-test') compile "org.springframework.boot:spring-boot-starter-data-jpa" compile 'mysql:mysql-connector-java' compile 'io.springfox:springfox-swagger2:2.2.2' compile 'io.springfox:springfox-swagger-ui:2.2.2' compile 'com.google.code.gson:gson' }
作成した gradle ファイルを実行して Eclipse用プロジェクトを作成します。
$ mkdir -p src/main/java/app src/test/java/app $ mkdir -p src/main/resources/application.yml $ gradle eclipse
作成したプロジェクトをEclipseで読み込みます。 一般 > 既存のプロジェクトをワークスペースへ > 作成したプロジェクト ※ 今回なら /Users/${user}/eclipse-workspace/SpringRestAPI です。
Applicationクラスの作成
今回はテンプレートから作成していないので、自前でApplicationクラスを実装する必要があります。 src/main/java/app/ 配下に Applicationクラスを実装します。
package app; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class SpringRestAPIApplication { public static void main(String[] args) { SpringApplication.run(SpringRestAPIApplication.class, args); } }
外部設定の管理
SpringBootでは 外部設定値を application.yml で管理しています。 ※ .yml以外にも .xml や .properties でも管理できます。
src/main/resources/application.yml に以下の設定を追記して下さい。
spring: profiles.active: unit datasource: url: jdbc:mysql://localhost:3306/spring?autoReconnect=true&useSSL=false username: root password: driverClassName: com.mysql.jdbc.Driver server: port: 9000
ここでは MySQLへの接続設定を定義しています。 port指定に関しては 何故か自分の環境ではMySQLとポート(8080)がコンフリクトしていたので 回避策として 9000 番を利用するように指定しています。
また SSLに関しては MySQLにSSL接続するちおエラーになるためSSLの利用を無効化しています。 MySQL側の設定でも回避できるのですが、ここではSpring側の設定で回避します。
Modelの作成
書籍を管理するためのBookModelを作成します。 Modelクラスはアノテーションにより様々な制約を設けることが出来ます。 ただし、RailsのActiveModelの様に それ自体がDBへのアクセスを行うことは出来ません。
package app.models; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; @Entity public class Book { @Id @GeneratedValue private Long id; @Column private String title; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } }
今回はValidationなどは付与せずに必要最低限の設定のみ行っています。 詳細な機能についてはSpring Data JPAのリファレンスを参照することをお勧めします。
Repository の作成
前述した通りModelはそれ単独ではDBアクセスすることは出来ません。 JpaRepositoryを継承したinterfaceを実装することでCRUDメソッドが提供されます。
package app.repositories; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import app.models.Book; @Repository public interface BookRepository extends JpaRepository<Book, Long> { }
今回はinterfaceの継承のみですが、アノテーションでクエリを指定したりと色々と便利です。
Controller
続いて実際にリクエストを処理するControllerを作成します。 先程実装したModel, Repositoryを利用して CRUD を実装してみます。 ※DIコンテナの話は非常に長くなるので ここでは触れません。
package app.controllers; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.Errors; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; import app.models.Book; import app.repositories.BookRepository; @RestController @RequestMapping("/books") public class BookController { @Autowired private BookRepository book_repository; @RequestMapping(method = RequestMethod.GET) public Object index() { return book_repository.findAll(); } @RequestMapping(method = RequestMethod.GET, value="{id}") public Object show(@PathVariable("id") Long id) { return book_repository.findOne(id); } @RequestMapping(method = RequestMethod.POST) public Object create(@Validated @RequestBody Book book, Errors err) { book_repository.save(book); return book; } @RequestMapping(method = RequestMethod.PUT, value="{id}") public Object update(@PathVariable("id") Long id,@Validated @RequestBody Book book, Errors err) { book.setId(id); book_repository.save(book); return book; } @RequestMapping(method = RequestMethod.DELETE, value="{id}") public Object delete(@PathVariable("id") Long id) { book_repository.delete(id); return "200"; } }
アプリケーションを実行し、curl などでリクエストを送ってみましょう。
# INDEX curl -XGET -H "Content-Type:application/json" http://localhost:9000/books # INSERT curl -XPOST -H "Content-Type:application/json" http://localhost:9000/books -d '{ "title":"Java" }' # UPDATE curl -XPUT -H "Content-Type:application/json" http://localhost:9000/books/1 -d '{ "title":"PHP" }' # DELETE curl -XPOST -H "Content-Type:application/json" http://localhost:9000/books/1
実際にDBへのトランザクションが実行されているはずです。
共通エラー処理を実装する
SpringBootには共通エラーハンドリング機能が備わっています。 共通エラーハンドリングを行うとアプリケーションのエラー時の挙動を一元管理できるのでお薦めです。
まず、以下のディレクトリを作成します。
$ mkdir -p src/main/java/app/lib/exceptions/
まず、今回定義するエラークラスを作成します。
全エラークラスの振る舞いを定義するスーパークラスを実装します。 今回は単純にJSON形式でメッセージとHttpStatusを返すだけの機能とします。
package app.lib.exceptions; import org.springframework.http.HttpStatus; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.annotations.Expose; /** * HttpStatus エラー抽象クラス * @author ynobuhar */ public abstract class HttpStatusException extends RuntimeException { private static final long serialVersionUID = 1L; private Gson gson; @Expose private String response; @Expose private HttpStatus http_status; /** * コンストラクタ * @param http_status エラーステータスコード * @param message レスポンス値 */ public HttpStatusException(HttpStatus http_status, String response) { this.http_status = http_status; this.response = response; this.gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create(); } /** * レスポンスを取得 * @return レスポンス値 */ public String getResponse() { return gson.toJson(this); } /** * HttpStatus取得 * @return エラーステータス */ public HttpStatus getHttp_status() { return http_status; } }
HttpStatusExceptionを継承したエラー400用クラスを実装します。 とりあえずリクエストに不都合があれば このクラスを投げます。
package app.lib.exceptions; import org.springframework.http.HttpStatus; /** * HttpStatus 400 * @author ynobuhar */ public class BadRequestException extends HttpStatusException{ private static final long serialVersionUID = 1L; public BadRequestException() { super(HttpStatus.BAD_REQUEST, "不正なリクエストです"); } }
同じくこちらは500エラークラス。 予期しないエラーが発生した場合はこちらを投げます。
package app.lib.exceptions; import org.springframework.http.HttpStatus; /** * HttpStatus 500 * @author ynobuhar */ public class InternalServerErrorException extends HttpStatusException{ private static final long serialVersionUID = 1L; public InternalServerErrorException() { super(HttpStatus.INTERNAL_SERVER_ERROR, "予期しないエラーが発生しました"); } }
続いて 作成した lib配下に共通エラーハンドリングクラスを作成します。 ResponseEntityExceptionHandlerを継承したクラスは自動的にControllerで発生した例外を受け取ります。
package app.lib; import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.context.request.WebRequest; import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; import com.fasterxml.jackson.core.JsonProcessingException; import app.lib.exceptions.HttpStatusException; import app.lib.exceptions.InternalServerErrorException; /** * エラーハンドリングクラス * @author ynobuhar */ @RestControllerAdvice public class ExceptionHandleService extends ResponseEntityExceptionHandler { @ExceptionHandler(Exception.class) public ResponseEntity<?> handleHttpStatusException(Exception ex, WebRequest request) throws JsonProcessingException { //ハンドリングしていないExceptionは 500 エラーとして扱う HttpStatusException http_status_exception = ( ex instanceof HttpStatusException )? (HttpStatusException)ex : new InternalServerErrorException(); return handleExceptionInternal( http_status_exception, http_status_exception.getResponse(), new HttpHeaders(), http_status_exception.getHttp_status(), request); } }
試しにControllerで強制的に例外を発生させてみます。
@RequestMapping(method = RequestMethod.POST) public Object create(@Validated @RequestBody Book book, Errors err) { //強制エラー throw new BadRequestException(); book_repository.save(book); return book; }
リクエストを投げます
$ curl -XPOST -H "Content-Type:application/json" http://localhost:9000/books -d '{}' > {"response":"不正なリクエストです","http_status":"BAD_REQUEST"}
例外をcatchできました。
APIドキュメントを自動生成する
基本的にプロダクトでは柔軟な変更が求められます。 よくあるパターンとしては開発当初に作成されたドキュメントが変更に対応されず置き去りになるパターンです。 (システムのバージョンが2.0なのにドキュメントが1.Xで更新が止まっているなど)
この様な問題はドキュメントをコードから自動生成することで ある程度は回避することが可能です。 今回は その方法としてSwaggerを採用してみます。
Swaggerの詳しい使い方については以下のURLを参照して下さい swagger.io
まずは confディレクトリを作ります。 ※confディレクトリが必要という訳ではありませんが、構成上ここに置く方が しっくりするというだけです
$ mkdir -p src/main/java/app/conf
本題のSwagger クラスを実装します。
package app.conf; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import springfox.documentation.service.ApiInfo; import springfox.documentation.spi.DocumentationType; import springfox.documentation.spring.web.plugins.Docket; import springfox.documentation.swagger2.annotations.EnableSwagger2; import com.google.common.base.Predicate; import com.google.common.base.Predicates; /** * Swagger 設定クラス * URL: /swagger-ui.html * @author ynobuhar */ @Configuration @EnableSwagger2 public class Swagger { @Bean public Docket swaggerSpringMvcPlugin() { return new Docket(DocumentationType.SWAGGER_2) .select() .paths(paths()) .build() .apiInfo(apiInfo()); } /** * 仕様書生成対象のURLパスを指定 * @return */ @SuppressWarnings("unchecked") private Predicate<String> paths() { return Predicates.and( Predicates.or( Predicates.containsPattern("/books/*") )); } /** * 仕様書情報の取得 * @return */ private ApiInfo apiInfo() { return new ApiInfo( "SpringRestApi Web API", "SpringRestApi API 仕様書", "0.0.1", "", "your name", "MIT license", "https://opensource.org/licenses/MIT"); } }
Swagger.class を作成したことで 自動的にドキュメントが生成されるようになります。
アプリケーションを実行し、以下のパスにアクセスして確認してみます。 http://localhost:9000/swagger-ui.html
以下の様な画面が出ると思います。 以降、対象のAPIに変更がかかると自動的にドキュメントも更新されるようになります。
ここまでが一通りのAPI実装の流れになります。 テストは? と思わるでしょうが 正直 SpringBootのテストを書くのが非常に辛いので割愛しています。
【Rails4】kaminariをAPIで利用する
ページネーション用Gemで有名なkaminari。
ただ、使い方を調べてもAPIでの利用方法が見つからなかったので記載。
下記Gem導入済み
・kaminari
・active_model_serializers
modelの出力フォーマットをactive_model_serializersを利用して設定
./app/serializers/todo_serializer.rb
class TodoSerializer < ActiveModel::Serializer attributes :id, :body, :memo, :complete, :user_id end
kaminariの共通設定ファイルを定義
config/initializers/kaminari_config.rb
Kaminari.configure do |config| config.default_per_page = 7 #最大項目数 # config.max_per_page = nil #最大数/page # config.window = 4 # config.outer_window = 0 # config.left = 0 # config.right = 0 # config.page_method_name = :page # config.param_name = :page end Contact GitHub API Training Shop Blog About
kaminariをAPIに適用させる
config/initializers/active_model_serializer.rb
ActiveModel::Serializer.config.adapter = :json_api
controllerを実装する
class TodosController < ApplicationController def index #deviceを利用していますが、ここは各自読み替え todos = current_user.todos.page(params[:page] ||= 1) #meta 以下が今回の設定を適用させる render json: todos, meta: pagination_dict(todos) end #以下省略
出力確認
{ "data":[{"id":"11","type":"todos","attributes":{"body":"APIを実装する","memo":"","complete":false,"user-id":9}}, {"id":"12","type":"todos","attributes":{"body":"はてなに投稿する","memo":"","complete":false,"user-id":9}},{"id":"13","type":"todos","attributes":{"body":"記事を投稿する","memo":"","complete":false,"user-id":9}}], "links":{}, "meta":{"current-page":1,"next-page":null,"prev-page":null,"total-pages":1,"total-count":3} }
meta 及び links がnodeに存在すれば完了。
【Rails4】before_actionに引数を渡す
以下の様にbefore_actionに引数を渡した状態で定義してもエラーとなる。
before_action :test('hogehoge')
これは以下の様に修正することで解決できる。
before_action -> { test('hogehoge') }
フィルターを設定したい場合は以下の様に追記。
before_action -> { test('hogehoge') },only: [:index, :show]
少しハマったのでメモ。
【Rails4】Devise::MissingWarden エラーへの対処
RepecでControllerのテストを実行した際に発生。
以下エラー文
Devise::MissingWarden:
Devise could not find the `Warden::Proxy` instance on your request environment.
Make sure that your application is loading Devise and Warden as expected and
that the `Warden::Manager` middleware is present in your middleware stack.
原因
Device gem によるプロキシエラー。
対応
f you're using RSpec, you can put the following inside a file named spec/support/devise.rb or in your spec/spec_helper.rb (or spec/rails_helper.rb if you are using rspec-rails):
RSpec.configure do |config| config.include Devise::Test::ControllerHelpers, type: :controller config.include Devise::Test::ControllerHelpers, type: :view end
とのことなので以下の様に設定を追加
spec/rails_helper.rb
RSpec.configure do |config| config.include Devise::Test::ControllerHelpers, type: :controller config.include Devise::Test::ControllerHelpers, type: :view end