Entity Framework を使用して SQLite に migration するまで
- 今回作成する Models の ER図(dbdiagram.io)
dotnet ef migrations script
により作成した DDL から生成
セットアップ
まずは、3系の sdk を使用して .NET Core で開発できるようセットアップします。
dotnet --list-sdks # sdk のバージョンを固定 dotnet new globaljson --sdk-version 3.1.408
Spring Initilizar 的な scaffolding 用のコマンドで webapi を開発します。
必要な tool や package を追加します。
dotnet new webapi -n sandbox # tool のバージョンをディレクトリで固定 dotnet new tool-manifest # tool の install dotnet tool install dotnet-ef --version 3.1.4 # package の追加 dotnet add package Microsoft.EntityFrameworkCore.Design --version 3.1.4 dotnet add package Microsoft.EntityFrameworkCore --version 3.1.4 dotnet add package Microsoft.EntityFrameworkCore.Sqlite --version 3.1.4 dotnet new gitignore
この時点で一度、起動するか確認。
dotnet run
Entity の定義
models ディレクトリをつくり、その中に Entity の定義をします。
hibernate と比較して、 Entity 自体に oneToMany といった relation を示す定義は書かないみたいです。
DbContext を継承した Context の設定用クラスが味噌っぽいです。
このクラスに Context 内で扱いたい Entity を記載します。 _context.Books.Where()
のように扱えます。
public DbSet<Book> Books { get; private set; } public DbSet<Author> Authors { get; private set; } public DbSet<Review> Reviews { get; private set; } public DbSet<Reviewer> Reviwers { get; private set; } public DbSet<BookAuthor> BookAuthors { get; private set; }
ManyToMany(N:N) の relation については中間テーブルが必要になるため、 OnModelCreating メソッドに定義を追加します。
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<BookAuthor>().HasKey(ba => new { ba.BookId, ba.AuthorId}); modelBuilder.Entity<BookAuthor>() .HasOne(ba => ba.Author) .WithMany(a => a.BookAuthors) .HasForeignKey(ba => ba.AuthorId); modelBuilder.Entity<BookAuthor>() .HasOne(ba => ba.Book) .WithMany(b => b.BookAuthors) .HasForeignKey(ba => ba.BookId); }
Framework による migration
Spring の Active Profiles のように複数の設定ファイルを切り替えて設定情報を注入できるようです。
開発用の設定として、 SQLite の接続情報を記載します。
// appsettings.Development.json "ConnectionStrings": { "DefaultConnection": "Data source=sandbox.db", "MSSQLConnection": "Server=localhost,1433;Initial Catalog=Sandbox; User ID=sa; Password=P@ssw0rd!;" }
Startup.cs の ConfigureServices メソッドが DI コンテナへの追加を行います。
ここに DI に必要な設定として、 interface と使用する実装のペアを記載して使用するのですが、 今回は、 Datasource への接続情報を Context に登録するようなイメージで設定を記載します。
が、接続情報は外部の設定ファイルから注入したいので、 appsettings.Development.json の設定名の key で指定します。
// Startup.cs // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { services.AddControllers(); services.AddDbContext<DataContext>(options => { options.UseSqlite(Configuration.GetConnectionString("DefaultConnection")); }); }
あとは、 migration を実行し、 Migrations フォルダが作成された後に、 update で DB に適用されます。
$ dotnet ef --version Entity Framework Core .NET Command-line Tools 3.1.4 dotnet ef migrations add InitialCreate # 削除する場合 # dotnet ef migrations list # dotnet ef migrations remove # DB に適用 dotnet ef database update # 適用したものを削除 # dotnet ef database drop
migration を行わず、 DDL を生成することもできるようです。 ここで MSSQL むけの DDL を生成して、 dbdiagram.io で ER図を作成としました。
-- dotnet ef migrations script -- CREATE TABLE IF NOT EXISTS "__EFMigrationsHistory" ( -- "MigrationId" TEXT NOT NULL CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY, -- "ProductVersion" TEXT NOT NULL -- ); CREATE TABLE "Authors" ( "Id" INTEGER NOT NULL CONSTRAINT "PK_Authors" PRIMARY KEY AUTOINCREMENT, "FirstName" TEXT NULL, "LastName" TEXT NULL ); CREATE TABLE "Books" ( "Id" INTEGER NOT NULL CONSTRAINT "PK_Books" PRIMARY KEY AUTOINCREMENT, "Title" TEXT NULL, "Published" TEXT NULL ); CREATE TABLE "Reviwers" ( "Id" INTEGER NOT NULL CONSTRAINT "PK_Reviwers" PRIMARY KEY AUTOINCREMENT, "FirstName" TEXT NULL, "LastName" TEXT NULL ); CREATE TABLE "BookAuthors" ( "BookId" INTEGER NOT NULL, "AuthorId" INTEGER NOT NULL, CONSTRAINT "PK_BookAuthors" PRIMARY KEY ("BookId", "AuthorId"), CONSTRAINT "FK_BookAuthors_Authors_AuthorId" FOREIGN KEY ("AuthorId") REFERENCES "Authors" ("Id") ON DELETE CASCADE, CONSTRAINT "FK_BookAuthors_Books_BookId" FOREIGN KEY ("BookId") REFERENCES "Books" ("Id") ON DELETE CASCADE ); CREATE TABLE "Reviews" ( "Id" INTEGER NOT NULL CONSTRAINT "PK_Reviews" PRIMARY KEY AUTOINCREMENT, "Headline" TEXT NULL, "ReviewText" TEXT NULL, "Rating" INTEGER NOT NULL, "ReviewerId" INTEGER NULL, "BookId" INTEGER NULL, CONSTRAINT "FK_Reviews_Books_BookId" FOREIGN KEY ("BookId") REFERENCES "Books" ("Id") ON DELETE RESTRICT, CONSTRAINT "FK_Reviews_Reviwers_ReviewerId" FOREIGN KEY ("ReviewerId") REFERENCES "Reviwers" ("Id") ON DELETE RESTRICT ); CREATE INDEX "IX_BookAuthors_AuthorId" ON "BookAuthors" ("AuthorId"); CREATE INDEX "IX_Reviews_BookId" ON "Reviews" ("BookId"); CREATE INDEX "IX_Reviews_ReviewerId" ON "Reviews" ("ReviewerId"); -- INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion") -- VALUES ('20210531222210_InitialCreate', '3.1.4');
作成された DB の確認
SQLite なので単一のファイルとして存在しています。 Mac には標準でクライアントが入っているので、繋いで確認します。
$ file sandbox.db sandbox.db: SQLite 3.x database, last written using SQLite version 3028 $ sqlite3 sandbox.db SQLite version 3.32.3 2020-06-18 14:16:19 sqlite> .database main: /Users/kiyotakeshi/gitdir/c-charp/c-charp-sandbox/sandbox/sandbox.db sqlite> .table Authors Books Reviwers BookAuthors Reviews __EFMigrationsHistory
DB が作成されていることが確認できました。
普段の開発では任意の SQLite に対応したクライアントを使用するといいと思います。 vscode なら以下のものが使いやすかったです。
Name: SQLite Id: alexcvzz.vscode-sqlite Description: Explore and query SQLite databases. Version: 0.12.0 Publisher: alexcvzz VS Marketplace Link: https://marketplace.visualstudio.com/items?itemName=alexcvzz.vscode-sqlite
reverse proxy として nginx をコンテナ起動し、 Spring Boot Application と連携する
nginx を reverse proxy としてコンテナ起動する際に、 http://host.docker.internal:8081/
と設定している意味について検証したメモ。
アーキテクチャのイメージ
localhost:8081 で起動している Spring Boot で作成したアプリケーションの前段に、 nginx を置いて、 localhost:80 でアプリケーションにアクセスできるようにします。
昔ながらの三層構造(Web,App,DB)だとこのような構成で動かすことが多いかと思います。
クラウド時代のデプロイとしては、
Web のレイヤーが AWS の ELB になり、ターゲットグループでアプリケーションの起動ポートを指定したり
Kubernetes の Service になったり
- その場合も、さらに前段には、パブリッククラウドが提供する LB がいるのが一般的
と複雑さが増しますが、今回はシンプルな構成としてみます。
設定する
nginx.conf を以下のように記載すると、実現できます。
events { worker_connections 16; } http { server { listen 80; server_name localhost; location / { proxy_pass http://host.docker.internal:8081/; proxy_redirect off; } } }
ここまでは調べると出てくる情報だったのですが、
http://host.docker.internal:8081/
とはなんぞやという感想です。
ホストの IP アドレスは変動します(あるいは、ネットワークへの接続がありません)。18.03 よりも前は、特定の DNS 名 host.docker.internal での接続を推奨していました。これはホスト上で内部の IP アドレスで名前解決します。これは開発用途であり、Docker Desktop forMac 外の本番環境では動作しません。
コンテナからホスト(今回だと Mac)の private ip を解決するもののようです。
host.docker.internal の動作を検証
上記の図となるような構成でコンテナを立ち上げます。
※参考リポジトリの docker-compose.yaml のコメントアウトを解除
$ docker-compose up -d
host.docker.internal
への疎通を確認します。
$ docker network inspect todo_default | grep "Gateway" "Gateway": "172.19.0.1" $ docker inspect reverse-proxy-nginx | egrep 'Gateway|IPAddress' "SecondaryIPAddresses": null, "Gateway": "", "IPAddress": "", "IPv6Gateway": "", "Gateway": "172.19.0.1", "IPAddress": "172.19.0.4", "IPv6Gateway": "", $ docker-compose exec nginx bash -c "apt-get update -y > /dev/null && apt install -y iputils-ping traceroute > /dev/null && ping host.docker.internal -c 2 && traceroute host.docker.internal" # 結果の抜粋 PING host.docker.internal (192.168.65.2) 56(84) bytes of data. 64 bytes from 192.168.65.2 (192.168.65.2): icmp_seq=1 ttl=37 time=0.142 ms 64 bytes from 192.168.65.2 (192.168.65.2): icmp_seq=2 ttl=37 time=0.232 ms --- host.docker.internal ping statistics --- 2 packets transmitted, 2 received, 0% packet loss, time 1ms rtt min/avg/max/mdev = 0.142/0.187/0.232/0.045 ms traceroute to host.docker.internal (192.168.65.2), 30 hops max, 60 byte packets 1 172.19.0.1 (172.19.0.1) 0.625 ms 0.557 ms 0.534 ms
host.docker.internal
が 192.168.65.2
に名前解決され、 Gateway を経由して到達しているようです。
Docker for Mac にて設定した、ネットワークのレンジであることが画像や、
Mac のプロセスでも確認できました。
$ ps aux | grep docker | grep '192.168.65.2' kiyotakeshi 1476 0.0 0.2 6200624 123032 ?? S 4:25PM 0:01.08 com.docker.vpnkit --ethernet fd:3 --diagnostics fd:4 --pcap fd:5 --vsock-path vms/0/connect --gateway-forwards /Users/kiyotakeshi/Library/Group Containers/group.com.docker/gateway_forwards.json --host-names host.docker.internal,docker.for.mac.host.internal,docker.for.mac.localhost --listen-backlog 32 --mtu 1500 --allowed-bind-addresses 0.0.0.0 --http /Users/kiyotakeshi/Library/Group Containers/group.com.docker/http_proxy.json --dhcp /Users/kiyotakeshi/Library/Group Containers/group.com.docker/dhcp.json --port-max-idle-time 300 --max-connections 2000 --gateway-ip 192.168.65.1 --host-ip 192.168.65.2 --lowest-ip 192.168.65.3 --highest-ip 192.168.65.254 --log-destination asl --gc-compact-interval 1800
ということで、図 のような、ルーティングとなっているようです。
コンテナ間通信なら、 http://host.docker.internal
は使わない
さて、ホストの側にアクセスしないで、コンテナ間通信する場合の、
reverse proxy の設定となると、 http://host.docker.internal
を使うのは不適切です。
理由としては、以下のものが考えられます。
コンテナ名で名前解決して疎通することができるから
(意図してホスト側の private ip に疎通するならまだしも)、直接的なコンテナ間通信をしていないから
Spring Boot のアプリケーションも image 化してコンテナ起動した場合は、
Nginx とアプリケーションでコンテナ間通信をするので、以下の図のような関係性に変わります。
その際の、 nginx.conf の設定はこちら。
events { worker_connections 16; } http { server { listen 80; server_name localhost; location / { # container name を指定 proxy_pass http://todo:8081/; proxy_redirect off; } } }
コンテナ名を指定するため、 http://host.docker.internal
は使用しません。
docker-compose.yaml(参考リポジトリでは、 app.yaml というファイル名)
services: postgres: image: postgres:11.10 container_name: todo-postgres ports: - 5432:5432 volumes: - .docker/postgres:/var/lib/postgresql/data environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: password POSTGRES_DB: todo POSTGRES_INITDB_ARGS: "--encoding=UTF-8" restart: always todo: image: todo # ビルドしておいた image を使用 container_name: todo ports: - 8081:8081 environment: SPRING_DATASOURCE_URL: jdbc:postgresql://todo-postgres:5432/todo depends_on: - postgres nginx: image: reverse-proxy-nginx build: ./reverse-proxy # Dockerfile がある任意の場所 container_name: reverse-proxy-nginx ports: - 80:80 depends_on: - todo
Dockerfile
FROM nginx:1.19 COPY nginx.conf /etc/nginx/nginx.conf
コンテナを起動後、 todo で名前解決できていることが確認できます。
docker-compose up -d # app.yaml を使用する場合は、 `docker-compose -f app.yaml exec nginx ...` $ docker-compose exec nginx bash -c "apt-get update -y > /dev/null && apt-get install dnsutils -y /dev/null && dig todo +short" 192.168.48.3
docker-compose で複数のコンテナを起動する際に、ネットワークモードの設定を明示的にしていないため、 bridge mode ですべてのコンテナが同一ネットワークに所属しています。
$ docker network ls | grep todo 8af1c7f7ab3c todo_default bridge local $ docker network inspect todo_default | jq -r '.[].Containers[] | [.Name, .IPv4Address]' [ "todo-postgres", "192.168.48.2/20" ] [ "reverse-proxy-nginx", "192.168.48.4/20" ] [ "todo", "192.168.48.3/20" ] $ docker-compose exec nginx bash -c "dig todo-postgres +short" 192.168.48.2
そのため、 reverse-proxy の設定箇所は、コンテナ名の指定の方が適切となります。
proxy_pass http://todo:8081/;
Spring Boot で MongoDB をつかってみる
NoSQL 触ったことがなかったので、 Spring Boot で MongoDB を使用してみます。
まずは、ローカル開発は Docker でやるために、Mongo のコンテナを起動します。
というかこの点に関してハマった点をまとめるエントリーです。
まずは、root user および admin database をセットアップせずに起動し、
Application から疎通するために必要な流れをみていきます。
- docker-compose.yaml
- MongoDB と mongo-express を記載
- 初期DBを指定
- この DB にコレクション(RDBでいう Table のようなイメージ)を作る必要あり
version: '3.1' services: mongo: container_name: spring-mongo image: mongo:4.2 restart: always environment: MONGO_INITDB_DATABASE: spring-mongo ports: - 27017:27017 volumes: - ./mongo:/data/db - ./mongo/init:/docker-entrypoint-initdb.d mongo-express: container_name: spring-mongo-express image: mongo-express restart: always ports: - 9080:8081 # コンテナの 8081 にマッピングしていればホスト側は任意 depends_on: - mongo
- application.yaml
spring: data: mongodb: # port: 27017 # if you set not default port host: localhost database: spring-mongo username: demo password: 1qazxsw2
host,database,username,password の記載は uri: mongodb://localhost/spring-mongo
とすることも可能です。
ここで指定している username,password はどこで設定したかというと、
image: mongo:4.2
の ENTRYPOINT である、 docker-entrypoint.sh にて実行される .js か .sh ファイル にて流し込んでいます。
$ ls -1 mongo/init 01_import_init_data.json 01_import_init_data.sh 02_create_user.js
let user = { user: 'demo', pwd: '1qazxsw2', roles: [{ role: 'readWrite', db: 'spring-mongo' }] }; db.createUser(user);
これで、 docker-compose up -d
をすると、
- spring-mongo という DB がつくられ( 指定しなかったら test という名前でつくられる )
- user(demo) が作成され
- コレクションやドキュメントを登録していれば
Application の接続先が準備できます。
MongoDB に繋いで確認してみます。 DB,Collection,Document が作成されています。
docker-compose exec mongo bash mongo > show dbs admin 0.000GB config 0.000GB local 0.000GB spring-mongo 0.000GB > use spring-mongo switched to db spring-mongo > show collections customer > db.customer.find() { "_id" : ObjectId("6024e6a39c82f64f379738ca"), "firstName" : "Mike", "lastName" : "Popcorn", "_class" : "com.kiyotakeshi.spring.mongo.Customer" } { "_id" : ObjectId("6024e6a39c82f64f379738cb"), "firstName" : "Sam", "lastName" : "Smith", "_class" : "com.kiyotakeshi.spring.mongo.Customer" }
これらを接続先として、 application.yaml で指定していたわけです。
あとは、アプリを起動すると...
./mvnw spring-boot:run
MongoDB につなぎこみ、検索できています。
※このアプリの実装はほぼ 参考 記載の Spring のガイドのままです
------------------------------ Customers found with findAll() 2021-02-14 13:30:26.807 INFO 6466 --- [ main] org.mongodb.driver.connection : Opened connection [connectionId{localValue:3, serverValue:12}] to localhost:27017 Customer{id='6024e6a39c82f64f379738ca', firstName='Mike', lastName='Popcorn'} Customer{id='6024e6a39c82f64f379738cb', firstName='Sam', lastName='Smith'} -------------------------------- Customer found with findByFirstName('Alice'): null -------------------------------- Customers found with findByLastName('Smith'): Customer{id='6024e6a39c82f64f379738cb', firstName='Sam', lastName='Smith'} 2021-02-14 13:30:26.848 INFO 6466 --- [extShutdownHook] org.mongodb.driver.connection : Closed connection [connectionId{localValue:3, serverValue:12}] to localhost:27017 because the pool has been closed. Process finished with exit code 0
root user および admin database をセットアップした場合
以下おまけ。
docker-compose に以下のような記載をしているものを見かけました。
# mongodb MONGO_INITDB_ROOT_USERNAME: root MONGO_INITDB_ROOT_PASSWORD: 1qazxsw2 # mongo-express environment: ME_CONFIG_MONGODB_ADMINUSERNAME: root ME_CONFIG_MONGODB_ADMINPASSWORD: 1qazxsw2
これらは、
- root user および admin database をセットアップ
- セットアップされていた場合の mongo-express から接続するための設定
をしている箇所で、この設定を有効にしている場合、 Application の接続時の設定も変更する必要がありました。
先ほどの設定で、そのままアプリを起動するとコネクション時に認証エラーに。
Caused by: org.springframework.data.mongodb.UncategorizedMongoDbException: Exception authenticating MongoCredential{mechanism=SCRAM-SHA-1, userName='demo', source='spring-mongo', password=<hidden>, mechanismProperties=<hidden>}; nested exception is com.mongodb.MongoSecurityException: Exception authenticating MongoCredential{mechanism=SCRAM-SHA-1, userName='demo', source='spring-mongo', password=<hidden>, mechanismProperties=<hidden>} Caused by: com.mongodb.MongoSecurityException: Exception authenticating MongoCredential{mechanism=SCRAM-SHA-1, userName='demo', source='spring-mongo', password=<hidden>, mechanismProperties=<hidden>} Caused by: com.mongodb.MongoCommandException: Command failed with error 18 (AuthenticationFailed): 'Authentication failed.' on server localhost:27017. The full response is {"ok": 0.0, "errmsg": "Authentication failed.", "code": 18, "codeName": "AuthenticationFailed"} 2021-02-14 13:42:18.968 INFO 6735 --- [ main] org.mongodb.driver.connection : Closed connection [connectionId{localValue:4}] to localhost:27017 because there was a socket exception raised by this connection.
以下のように、 application.yaml に authentication-database と接続情報を記載する必要があります。
host: localhost database: spring-mongo username: root # if you set "MONGO_INITDB_ROOT_USERNAME", change that username password: 1qazxsw2 # if you set "MONGO_INITDB_ROOT_*", in docker-compose authentication-database: admin # host,database,username,password の記載を一行にまとめる場合 # uri: mongodb://root:1qazxsw2@localhost/spring-mongo?authSource=admin
これで無事にコネクションできます。
@RequestBody をつけたら request body is missing のエラーになった
POST メソッドを受け付ける Controller に @RequestBody を付与した際に、タイトルの通りのエラーに遭遇したためメモ。
- 今回の検証内容のリポジトリ
- @PostMapping のアノテーションがついたメソッドに @RequestBody を付与することで同じ状況にできる
2021-01-20 03:05:42.522 WARN 2842 --- [nio-8081-exec-2] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.http.converter.HttpMessageNotReadableException: Required request body is missing: public org.springframework.http.ResponseEntity<com.kiyotakeshi.todo.entity.Todo> com.kiyotakeshi.todo.controller.TodoController.createTodo(com.kiyotakeshi.todo.entity.Todo)]
原因としては、 json ではない、あやまったリクエストを送っていたため。
curl --location --request POST 'http://localhost:8081/todo/?activityName=test&color=black&category=test'
正しいリクエストは以下の形式。postman で作ったものを curl としてコピー。
curl -i --location --request POST 'http://localhost:8081/todo/' \ --header 'Content-Type: application/json' \ --data-raw '{ "activityName":"test", "color":"black", "category":"test" }'
debugger で止めて、 Evaluate Expression すると、しっかりとオブジェクトに値が入っていることがわかる。
以下がレスポンス、正常に作成された。
HTTP/1.1 201 Location: /todo/10012 Content-Type: application/json Transfer-Encoding: chunked Date: Tue, 19 Jan 2021 17:50:34 GMT {"id":10012,"activityName":"test","color":"black","category":"test"}
ちなみに、レスポンスが json で返ってきているのは、Controller に付与している @RestController に、
@RestController public class TodoController { }
@ResponseBody が付与されているため。
※ちなみに @Controller (ステレオタイプアノテーション)がついてるから、 Component Scan され Bean化されてる
// org/springframework/web/bind/annotation/RestController.java @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Controller @ResponseBody public @interface RestController { (略) }
flyway × postgres × jpa のid自動採番がうまくいかないときの対処法
entity を JPA により永続化する際に primary key の自動採番に苦戦したためメモ。
JPA により DML,DDL を用意
以下のような、いたってシンプルな entity を用意する。
@Entity public class Todo { @Id private Long id; private String activityName; private String color; private String category; }
次にテストコードを用意し、JPA が発行した DDL を参考に、データソースを h2 から postgres に変更する。
@DataJpaTest class TodoTests { @Autowired TestEntityManager em; @Test void mapping() { var todo1 = this.em.find(Todo.class, 1000L); assertThat(todo1.getActivityName()).isEqualTo("go to supermarket"); assertThat(todo1.getCategory()).isEqualTo("housework"); assertThat(todo1.getColor()).isEqualTo("white"); } }
※発行された DDL
Hibernate: create table todo (id bigint generated by default as identity, activity_name varchar(255), category varchar(255), color varchar(255), primary key (id))
なお、アプリケーション起動時に、flyway の sql-based-migrations を使用し、postgres に初期テーブル、データを作成する。
ちなみに、postgres コンテナ を docker-compose で起動する際に初期DBを作成する手順は 以前の投稿を参考までに。
DML の定義ファイルを用意。
※あとでハマる原因となる、 primary key である id の型が bigint(Long 型の entity の定義から自動的に設定されたもののようです)
-- V1.0.0__add_todo_table.sql create table todo (id bigint not null, activity_name varchar(255), category varchar(255), color varchar(255), primary key (id));
DDL も JPA に自動生成してもらうために、テストケースを作成。
@Test void newTodo() { var todo = new Todo("new", "white", "test"); this.em.persistAndFlush(todo); }
Hibernate: insert into todo (id, activity_name, category, color) values (null, ?, ?, ?)
参考にし DDL の定義ファイルも用意。
-- V1.0.1__insert_todo_records.sql insert into todo (activity_name, category, color, id) values ('go to supermarket', 'housework', 'white', 1000); insert into todo (activity_name, category, color, id) values ('listen to music', 'hobby', 'white', 1001); insert into todo (activity_name, category, color, id) values ('make a presentation', 'job', 'black', 1002);
idの自動採番ができずに怒られる
アプリを起動し、
docker-compose up -d ./mvn spring-boot:run
POST リクエストをすると、
curl 'http://localhost:8081/todo/' -i -X POST \ -d 'activityName=test&color=black&category=test'
insert 時に id が null で怒られます。
Hibernate: insert into todo (activity_name, category, color) values (?, ?, ?) 2021-01-18 22:45:01.523 WARN 2262 --- [nio-8081-exec-3] o.h.engine.jdbc.spi.SqlExceptionHelper : SQL Error: 0, SQLState: 23502 2021-01-18 22:45:01.523 ERROR 2262 --- [nio-8081-exec-3] o.h.engine.jdbc.spi.SqlExceptionHelper : ERROR: null value in column "id" violates not-null constraint org.postgresql.util.PSQLException: ERROR: null value in column "id" violates not-null constraint Detail: Failing row contains (null, test, test, black).
flyway の migration によりアプリ起動時に作成されたテーブルの定義を確認してみます。 postgres のコマンドをコンテナ内部に渡して、確認すると、
# `-d todo` で指定の todo は DB # `\d todo` で todo table の定義を確認 docker-compose exec postgres bash -c 'psql -Upostgres -d todo -c "\d todo"'
V1.0.0__add_todo_table.sql で定義した通り、bigint です。
Table "public.todo" Column | Type | Collation | Nullable | Default ---------------+------------------------+-----------+----------+--------- id | bigint | | not null | activity_name | character varying(255) | | | category | character varying(255) | | | color | character varying(255) | | | Indexes: "todo_pkey" PRIMARY KEY, btree (id)
解決策
連番で採番し null を回避するには、 serial 型が使えそうです。
デフォルト値が連番を発生させる仕組みから割り当てられるようにしました。 また、NOT NULL 制約を適用することによって、null 値が明示的に挿入されないようにします
bigint を使用していたので、 bigserial を使用してみます。
-- V1.0.0__add_todo_table.sql create table todo (id bigserial not null, activity_name varchar(255), category varchar(255), color varchar(255), primary key (id));
コンテナを破棄、再生成して、アプリを起動しテーブルの定義を確認してみます。
docker-compose down && docker-compose up -d ./mvn spring-boot:run $ docker-compose exec postgres bash -c 'psql -Upostgres -d todo -c "\d todo"'
bigint で 自動採番される定義になっています。
Table "public.todo" Column | Type | Collation | Nullable | Default ---------------+------------------------+-----------+----------+---------------------------------- id | bigint | | not null | nextval('todo_id_seq'::regclass) activity_name | character varying(255) | | | category | character varying(255) | | | color | character varying(255) | | | Indexes: "todo_pkey" PRIMARY KEY, btree (id)
todo_id_seq
という設定についても確認すると、
docker-compose exec postgres bash -c 'psql -Upostgres -d todo -c "\d"'
sequence という Type で、
List of relations Schema | Name | Type | Owner --------+-----------------------+----------+---------- public | flyway_schema_history | table | postgres public | todo | table | postgres public | todo_id_seq | sequence | postgres (3 rows)
docker-compose exec postgres bash -c 'psql -Upostgres -d todo -c "\d todo_id_seq"'
1からインクリメントしていくもののようです。
Sequence "public.todo_id_seq" Type | Start | Minimum | Maximum | Increment | Cycles? | Cache --------+-------+---------+---------------------+-----------+---------+------- bigint | 1 | 1 | 9223372036854775807 | 1 | no | 1 Owned by: public.todo.id
あとは、 entity の定義を変更します。
@Entity public class Todo { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) // 自動インクリメントで一意の値を生成 private Long id; private String activityName; private String color; private String category; }
POST リクエストをすると、
curl 'http://localhost:8081/todo/' -i -X POST \ -d 'activityName=test&color=black&category=test' curl 'http://localhost:8081/todo/' -i -X POST \ -d 'activityName=test2&color=white&category=test'
1番から採番されて、entity が永続化できています。
curl -s http://localhost:8081/todo/ | jq -r '.[] | select(.id == 1 or .id == 2)'
{ "id": 1, "activityName": "test", "color": "black", "category": "test" } { "id": 2, "activityName": "test2", "color": "white", "category": "test" }
DB も確認してみる。
docker-compose exec postgres bash -c 'psql -Upostgres -d todo -c "select * from todo"'
id | activity_name | category | color ------+---------------------+-----------+------- 1000 | go to supermarket | housework | white 1001 | listen to music | hobby | white 1002 | make a presentation | job | black 1 | test | test | black 2 | test2 | test | white
もう一つのやり方
entity の定義に関して調べていると、自動採番の定義を @SequenceGenerator で設定するというやり方も見つけました。
@Entity public class Todo { @Id @GeneratedValue(generator = "todo_id_gen") @SequenceGenerator(name = "todo_id_gen", sequenceName = "todo_id_seq", allocationSize = 1) private Long id; private String activityName; private String color; private String category; }
sequence の定義も追加。
-- V1.0.0__add_todo_table.sql create table todo (id bigint not null, activity_name varchar(255), category varchar(255), color varchar(255), primary key (id)); create sequence todo_id_seq start 10001;
relation の定義はどうなるかみてみましょう。(コンテナの disposability は便利)
docker-compose down && docker-compose up -d ./mvn spring-boot:run docker-compose exec postgres bash -c 'psql -Upostgres -d todo -c "\d"'
sequence がつくられています。
List of relations Schema | Name | Type | Owner --------+-----------------------+----------+---------- public | flyway_schema_history | table | postgres public | todo | table | postgres public | todo_id_seq | sequence | postgres (3 rows)
詳しくみてみる。
docker-compose exec postgres bash -c 'psql -Upostgres -d todo -c "\d todo_id_seq"'
で POST リクエストをすると、
curl 'http://localhost:8081/todo/' -i -X POST \ -d 'activityName=test&color=black&category=test' curl 'http://localhost:8081/todo/' -i -X POST \ -d 'activityName=test2&color=white&category=test'
10001番から採番されて、entity が永続化できています。
curl -s http://localhost:8081/todo/ | jq -r '.[] | select(.id == 10001 or .id == 10002)'
{ "id": 10001, "activityName": "test", "color": "black", "category": "test" } { "id": 10002, "activityName": "test2", "color": "white", "category": "test" }
DB も確認してみる。
docker-compose exec postgres bash -c 'psql -Upostgres -d todo -c "select * from todo"'
id | activity_name | category | color -------+---------------------+-----------+------- 1000 | go to supermarket | housework | white 1001 | listen to music | hobby | white 1002 | make a presentation | job | black 10001 | test | test | black 10002 | test2 | test | white (5 rows)
さらにもう一つのやり方
DBの設定として、 sequence の定義と id フィールドとの関連付けをするという意味では、以下のように設定することも可能。
-- V1.0.0__add_todo_table.sql -- id は bigint として定義しつつ、sequence によるインクリメントを設定 create sequence todo_id_seq start 10001; create table todo (id bigint not null DEFAULT nextval('todo_id_seq'), activity_name varchar(255), category varchar(255), color varchar(255), primary key (id));
DB側で設定しているので、 @GeneratedValue,@SequenceGenerator は不要。
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
docker-compose down && docker-compose up -d ./mvn spring-boot:run docker-compose exec postgres bash -c 'psql -Upostgres -d todo -c "\d todo"'
Table "public.todo" Column | Type | Collation | Nullable | Default ---------------+------------------------+-----------+----------+---------------------------------- id | bigint | | not null | nextval('todo_id_seq'::regclass) activity_name | character varying(255) | | | category | character varying(255) | | | color | character varying(255) | | | Indexes: "todo_pkey" PRIMARY KEY, btree (id)
docker-compose exec postgres bash -c 'psql -Upostgres -d todo -c "\d todo_id_seq"'
Sequence "public.todo_id_seq" Type | Start | Minimum | Maximum | Increment | Cycles? | Cache --------+-------+---------+---------------------+-----------+---------+------- bigint | 10001 | 1 | 9223372036854775807 | 1 | no | 1
POST したら想定通りに id が採番されている。
docker-compose exec postgres bash -c 'psql -Upostgres -d todo -c "select * from todo"'
id | activity_name | category | color -------+---------------------+-----------+------- 1000 | go to supermarket | housework | white 1001 | listen to music | hobby | white 1002 | make a presentation | job | black 10001 | test | tests | black (4 rows)
以上です。
postgres のコンテナ起動時の設定について
コンテナ起動時に初期DBを作成済みにする
をモチベーションに environment
の設定に関して理解を深めるために動作検証したのでメモ。
ドキュメントの設定値を参考に検証。
TL;DR
- ローカル開発なのでロールを気にしない
- カスタムしたDBをコンテナ起動時に作成済みにしておきたい
のであれば、以下の docker-compose.yaml
を作成し、
postgres: image: postgres:11.10 container_name: postgres-playground ports: - 5432:5432 environment: POSTGRES_USER: postgres # この設定は省略可能 POSTGRES_PASSWORD: password POSTGRES_DB: custom
コンテナを起動すれば、カスタムしたDB(custom)を作成可能。
docker-compose up -d docker-compose exec postgres bash -c 'psql -U postgres -c "\l"'
List of databases Name | Owner | Encoding | Collate | Ctype | Access privileges -----------+----------+----------+------------+------------+----------------------- custom | postgres | UTF8 | en_US.utf8 | en_US.utf8 | postgres | postgres | UTF8 | en_US.utf8 | en_US.utf8 | template0 | postgres | UTF8 | en_US.utf8 | en_US.utf8 | =c/postgres + | | | | | postgres=CTc/postgres template1 | postgres | UTF8 | en_US.utf8 | en_US.utf8 | =c/postgres + | | | | | postgres=CTc/postgres (4 rows)
以下は検証。
まずはプレーンな状態
postgres: image: postgres:11.10 container_name: postgres-playground ports: - 5432:5432 environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: password # POSTGRES_DB: test
起動してコンテナ内部に入って確認すると、
docker-compose up -d docker-compose ps docker-compose exec postgres bash # 指定した role と同じ名前のDBにつなぎに行くため、 psql -U postgres -d postgres と同じ結果になる psql -U postgres \l
Owner が postgres で初期DBが作成されている。
List of databases Name | Owner | Encoding | Collate | Ctype | Access privileges -----------+----------+----------+------------+------------+----------------------- postgres | postgres | UTF8 | en_US.utf8 | en_US.utf8 | template0 | postgres | UTF8 | en_US.utf8 | en_US.utf8 | =c/postgres + | | | | | postgres=CTc/postgres template1 | postgres | UTF8 | en_US.utf8 | en_US.utf8 | =c/postgres + | | | | | postgres=CTc/postgres
POSTGRES_DB
を変更
POSTGRES_DB This optional environment variable can be used to define a different name for the default database that is created when the image is first started. If it is not specified, then the value of POSTGRES_USER will be used.
とのことで、デフォルトのDB名を変更できるようです。先ほどは未指定のため、POSTGRES_USER と同じ名前になってたようです。
postgres: image: postgres:11.10 container_name: postgres-playground ports: - 5432:5432 environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: password POSTGRES_DB: test # この値を変更
コンテナ内部から postgres のコマンドを実行し、DBを確認すると、
# 先ほどのコンテナを破棄 docker-compose down docker-compose up -d docker-compose ps docker-compose exec postgres bash # シェルから postgres 内部のコマンドを実行 psql -Upostgres -c "\l"
test の名前でDBが作成されている。が、postgres のDBも存在している。 Owner の権限でも drop できないため、こちらは基本的には消せないものと思われる。
List of databases Name | Owner | Encoding | Collate | Ctype | Access privileges -----------+----------+----------+------------+------------+----------------------- postgres | postgres | UTF8 | en_US.utf8 | en_US.utf8 | template0 | postgres | UTF8 | en_US.utf8 | en_US.utf8 | =c/postgres + | | | | | postgres=CTc/postgres template1 | postgres | UTF8 | en_US.utf8 | en_US.utf8 | =c/postgres + | | | | | postgres=CTc/postgres test | postgres | UTF8 | en_US.utf8 | en_US.utf8 |
POSTGRES_USER
を変更
This variable will create the specified user with superuser power and a database with the same name. If it is not specified, then the default user of postgres will be used.
スーパーユーザ権限を持つユーザと同じ名前のDBを作成する設定項目。
指定しない場合は、デフォルトユーザ postgres が使われる。
postgres: image: postgres:11.10 container_name: postgres-playground ports: - 5432:5432 environment: POSTGRES_USER: test POSTGRES_PASSWORD: password # POSTGRES_DB: test
コンテナに入るのもめんどくさいので、 postgres のコマンドをコンテナ内部に渡して結果を確認する。
docker-compose down docker-compose up -d docker-compose ps docker-compose exec postgres bash -c 'psql -Upostgres -c "\l"'
デフォルトのユーザを変更しているため、ロールが無いためエラー。
psql: FATAL: role "postgres" does not exist
変更したユーザのロールで確認すると、
docker-compose exec postgres bash -c 'psql -Utest -c "\l"'
Owner が test で指定したロールと同様のDBができている。
List of databases Name | Owner | Encoding | Collate | Ctype | Access privileges -----------+-------+----------+------------+------------+------------------- postgres | test | UTF8 | en_US.utf8 | en_US.utf8 | template0 | test | UTF8 | en_US.utf8 | en_US.utf8 | =c/test + | | | | | test=CTc/test template1 | test | UTF8 | en_US.utf8 | en_US.utf8 | =c/test + | | | | | test=CTc/test test | test | UTF8 | en_US.utf8 | en_US.utf8 |
POSTGRES_USER,POSTGRES_DB を変更
最後は、スーパーユーザ権限を持つユーザとデフォルトのDB名のどちらも指定。
postgres: image: postgres:11.10 container_name: postgres-playground ports: - 5432:5432 environment: POSTGRES_USER: test POSTGRES_PASSWORD: password POSTGRES_DB: employee
test ロールで変更したDBを指定すると、
docker-compose down && docker-compose up -d && docker-compose ps docker-compose exec postgres bash -c 'psql -U test -d employee -c "\l'
以下のようにDBができています。
List of databases Name | Owner | Encoding | Collate | Ctype | Access privileges -----------+-------+----------+------------+------------+------------------- employee | test | UTF8 | en_US.utf8 | en_US.utf8 | postgres | test | UTF8 | en_US.utf8 | en_US.utf8 | template0 | test | UTF8 | en_US.utf8 | en_US.utf8 | =c/test + | | | | | test=CTc/test template1 | test | UTF8 | en_US.utf8 | en_US.utf8 | =c/test + | | | | | test=CTc/test
Application Context へのさまざまな Bean 登録の方法
様々な Bean 登録の方法を確認する。
まずはベーシックな @ComponentScan により Application context に登録する方法。
public static void main(String[] args) { var context = new AnnotationConfigApplicationContext(ConfigurationComponentScan.class); var bean1 = context.getBean(Bean1.class); bean1.sayHello(); }
@ComponentScan public class ConfigurationComponentScan { }
ちなみに Spring Boot を使用した時は @SpringBootApplication に @ComponentScan が設定されている。
@SpringBootApplication public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) }) public @interface SpringBootApplication {
次は、Configuration class をつかって Bean登録をする。
public static void main(String[] args) { var context = new AnnotationConfigApplicationContext(AppConfig.class); var bean1 = context.getBean(Bean1.class); bean1.sayHello(); }
@Configuration public class AppConfig { @Bean public Bean1 getBean1() { return new Bean1(); } @Bean public Bean2 getBean2() { return new Bean2(); } @Bean public Bean3 getBean3() { return new Bean3(); } }
この @Configuration に @Component が設定されているので、 Configuration class も DIコンテナにて管理される。
@Component public @interface Configuration {
次は、 Application Context を作成時に、スキャンしたいコンポーネントのパッケージを指定するパターン。
public static void main(String[] args) { var context = new AnnotationConfigApplicationContext("com.kiyotakeshi.bean.creation.beans"); var bean1 = context.getBean(Bean1.class); bean1.sayHello(); }
可変長引数をとるので、複数のパッケージを指定することが可能。
ただし String で受け取るので型によるチェックがないため、 typo に注意。
AnnotationConfigApplicationContext のコンストラクタに渡した引数を scan している。
public AnnotationConfigApplicationContext(String... basePackages) { this(); scan(basePackages); refresh(); }
次は、 scan メソッドにパッケージを指定するパターン。
public static void main(String[] args) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); context.scan("com.kiyotakeshi.bean.creation.beans"); context.refresh(); Bean1 bean = context.getBean(Bean1.class); bean.sayHello(); }
次は、設定ファイルから読み取るパターン。
public static void main(String[] args) { var context = new ClassPathXmlApplicationContext("/beans.xml"); var bean1 = context.getBean(Bean1.class); bean1.sayHello(); }
や
public static void main(String[] args) { String beansXmlLocationOnFilesystem = FileSystemXmlApplicationContextExample.class.getResource("/beans.xml").toExternalForm(); var context = new FileSystemXmlApplicationContext(beansXmlLocationOnFilesystem); var bean1 = context.getBean(Bean1.class); bean1.sayHello(); }
で xml に定義した設定で DI コンテナにBean登録できる。 xml の定義名のマッピングが必要(typo にも注意しなければならない)。
<!-- src/main/resources/beans.xml --> <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="bean1" class="com.kiyotakeshi.bean.creation.beans.Bean1"> <!-- Bean1 class の attribute である private Bean2 bean2; に対応 --> <property name="bean2" ref="bean2"/> <property name="bean3" ref="bean3"/> </bean> <bean id="bean2" class="com.kiyotakeshi.bean.creation.beans.Bean2"/> <bean id="bean3" class="com.kiyotakeshi.bean.creation.beans.Bean3"/> </beans>