はじめに
この記事はDjangoの公式チュートリアル「はじめての Django アプリ作成」と同じものをSpring WebFluxで作成することでDjangoとSpring WebFluxの違いを理解することを目的としています。
違い一覧
DjangoとSpringで類似している要素を纏めてみました。
Django | Spring | 備考 |
---|---|---|
Model(モデル) | Model(モデル) | |
Template(テンプレート) | View(ビュー) | |
View(ビュー) | Controller(コントローラ) | |
SQLite3 | H2 Database | 基本のデータベース |
Django Admin | 無し | CMS支援 |
はじめてのアプリ作成、その 2
Database の設定
Django
mysite/settings.py
のENGINE
にdjango.db.backends.sqlite3
と定義されている通り、デフォルトでSQLite3を使います。データはdb.sqlite3
というファイルに保存されます。
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
Spring Webflux
SpringではH2 DatabaseというJavaプラットフォーム上で動作するRDBSをDjangoのsqlite3のような感じで使うみたいです。一般的にDBへの連結はJPA(Java Persistence API)というモジュールが役を担います。
しかし、JPAはRDBSの性質上ブロッキングIO方式なのでリアクティブなWebFluxの長所を生かしきれません。そこで登場するのがR2DBC(Reactive Relational Database Connectivity)です。まだ新しいプロジェクトですがリアクティブ向けのRDBSサポートを目標としています。
H2 Database
とR2DBC
と使うためには下記のようにbuild.gradleに依存関係を記述します。
...
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-r2dbc'
implementation 'org.springframework.boot:spring-boot-starter-webflux'
runtimeOnly 'com.h2database:h2'
runtimeOnly 'io.r2dbc:r2dbc-h2'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'io.projectreactor:reactor-test'
}
...
H2データベースは基本的にインメモリーDBとして動作しますが、設定でデータをファイルとして永続化できます。今回は設定なしでインメモリーDBとして使用します。
モデルの作成
Django
Djangoではアプリのmodels.py
にmodels.Model
のサブクラスを定義しmigrate
することでデータモデルを作成することができます。
1. models.py
にモデルを定義する。
from django.db import models
class Question(models.Model):
question_text = models.CharField(max_length=200)
pub_date = models.DateTimeField('date published')
class Choice(models.Model):
question = models.ForeignKey(Question, on_delete=models.CASCADE)
choice_text = models.CharField(max_length=200)
votes = models.IntegerField(default=0)
2. (もし追加していなかったら)mysite/settings.py
にアプリを登録する。
...
INSTALLED_APPS = [
'polls.apps.PollsConfig',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]
...
3. モデルの変更を読み取り差分をマイグレーションファイルにする
python manage.py makemigrations polls
4. DBに反映する
python manage.py migrate
Spring Webflux
現在のR2DBCには自動的にスキーマを定義してくれる機能はなく、データモデルのクラスとスキーマ両方を定義する必要があります。
1. スキーマファイルを作る
データベーススキーマを初期化するためsrc/main/resources/schema.sql
にスキーマを定義します。spring-bootはresourcesの最上段に位置するschema.sql
やdata.sql
ファイルを発見し自動に初期化します1。
公式のガイド(https://spring.pleiades.io/guides/gs/accessing-data-r2dbc/)のようにschema.sqlをinitializerでもう一度初期化しようとすると"Table already exists"エラーが発生します。
BEGIN;
--
-- Create model Question
--
CREATE TABLE question (
id serial NOT NULL PRIMARY KEY,
question_text varchar(200) NOT NULL,
pub_date timestamp with time zone NOT NULL
);
--
-- Create model Choice
--
CREATE TABLE choice (
id serial NOT NULL PRIMARY KEY,
choice_text varchar(200) NOT NULL,
votes integer NOT NULL,
question_id integer NOT NULL
);
--
-- Add constraint
--
ALTER TABLE choice
ADD CONSTRAINT choice_question_id_c5b4b260_fk_polls_question_id
FOREIGN KEY (question_id)
REFERENCES question (id);
CREATE INDEX choice_question_id_c5b4b260 ON choice (question_id);
COMMIT;
今回はDjangoのSQLをそのまま流用します。
2. エンティティを定義する
Djangoプロジェクトと一貫性を取るためsrc/main/java/com/example/mysite/polls
の下にmodels
というディレクトリーを作り、Question
とChoice
のエンティティ定義します。
package com.example.mysite.polls.models;
import java.time.ZonedDateTime;
import org.springframework.data.annotation.Id;
public class Question {
@Id
private Long id;
private final String question_text;
private final ZonedDateTime pub_date;
public Question(String question_text, ZonedDateTime pub_date) {
this.question_text = question_text;
this.pub_date = pub_date;
}
public Long getId() {
return this.id;
}
public void setId(Long id) {
this.id = id;
}
public String getQuestionText() {
return this.question_text;
}
public ZonedDateTime getPubDate() {
return this.pub_date;
}
@Override
public String toString() {
return String.format(
"Question[id=%d, question_text='%s', pub_date='%s']",
id, question_text, pub_date);
}
}
package com.example.mysite.polls.models;
import org.springframework.data.annotation.Id;
public class Choice {
@Id
private Long id;
private final String choice_text;
private final Integer votes;
private final Long question_id;
public Choice(String choice_text, Integer votes, Long question_id) {
this.choice_text = choice_text;
this.votes = votes;
this.question_id = question_id;
}
public Long getId() {
return this.id;
}
public void setId(Long id) {
this.id = id;
}
public String getChoiceText() {
return this.choice_text;
}
public Integer getVotes() {
return this.votes;
}
public Long getQuestionId() {
return this.question_id;
}
@Override
public String toString() {
return String.format(
"Choice[id=%d, choice_text='%s', votes='%s', question_id='%s']",
id, choice_text, votes, question_id);
}
}
3. エンティティを操作する
R2DBCではSpring Dataの抽象化であるReactiveCrudRepository
を使うかR2DBCのR2dbcEntityTemplate
を使うことができます2。下記の例はR2dbcEntityTemplate
を使った場合です。
package com.example.mysite;
import com.example.mysite.polls.models.Question;
import io.r2dbc.spi.ConnectionFactory;
import java.time.ZonedDateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.r2dbc.core.R2dbcEntityTemplate;
import org.springframework.data.r2dbc.connectionfactory.init.ConnectionFactoryInitializer;
import org.springframework.data.r2dbc.connectionfactory.init.ResourceDatabasePopulator;
import reactor.core.publisher.Mono;
@SpringBootApplication
public class MysiteApplication {
private static final Logger log = LoggerFactory.getLogger(MysiteApplication.class);
public static void main(String[] args) {
SpringApplication.run(MysiteApplication.class, args);
}
@Bean
public CommandLineRunner demo(ConnectionFactory connectionFactory) {
return (args) -> {
R2dbcEntityTemplate template = new R2dbcEntityTemplate(connectionFactory);
log.info("--------------1-----------------");
Mono<Question> saved = template.insert(Question.class)
.using(new Question("What's new?", ZonedDateTime.now()));
saved.subscribe(obj -> log.info(obj.toString()));
log.info("--------------2-----------------");
template.select(Question.class)
.first()
.doOnNext(it -> log.info(it.toString()))
.subscribe();
log.info("--------------3-----------------");
};
}
}
./gradlew bootrun
で実行すると以下のようなログが出力されます。
INFO 82641 --- [ main] com.example.mysite.MysiteApplication : Starting MysiteApplication using Java 17.0.2
INFO 82641 --- [ main] com.example.mysite.MysiteApplication : No active profile set, falling back to default profiles: default
INFO 82641 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data R2DBC repositories in DEFAULT mode.
INFO 82641 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 5 ms. Found 0 R2DBC repository interfaces.
INFO 82641 --- [ main] o.s.b.web.embedded.netty.NettyWebServer : Netty started on port 8080
INFO 82641 --- [ main] com.example.mysite.MysiteApplication : Started MysiteApplication in 2.0 seconds (JVM running for 2.324)
INFO 82641 --- [ main] com.example.mysite.MysiteApplication : --------------1-----------------
INFO 82641 --- [ main] com.example.mysite.MysiteApplication : Question[id=1, question_text='What's new?', pub_date='2022-02-22T01:20:24.411035+09:00[Asia/Tokyo]']
INFO 82641 --- [ main] com.example.mysite.MysiteApplication : --------------2-----------------
INFO 82641 --- [ main] com.example.mysite.MysiteApplication : Question[id=1, question_text='What's new?', pub_date='2022-02-22T01:20:24.411035+09:00']
INFO 82641 --- [ main] com.example.mysite.MysiteApplication : --------------3-----------------
Adminページ
DjangoではCMSに該当するDjango Adminというモデル管理インタフェースを提供します。しかし、これは一般的な仕様ではないのでSpringでは自分で一からAdminページ構成しなければいけません。
-
Initialize a Database Using Basic SQL Scripts, https://docs.spring.io/spring-boot/docs/current/reference/html/howto.html#howto.data-initialization.using-basic-sql-scripts ↩
-
R2DBC support, https://docs.spring.io/spring-data/r2dbc/docs/current/reference/html/#r2dbc.core ↩