Sprint Bootを試してみた。 恒例で、まず簡単なREST APIを作ってみる。例のごとくデータベースはSQLiteとする。

まずpom.xml

ぶっちゃけ、この依存関係が一番ムズかった。パッケージ多すぎてどのヤツで決めれば良いかのドキュメントが分からなさ過ぎる。モチロン個々の機能も知らなければならないのだが、Javaの一番よくワカランのはコードより依存関係・環境構築関連だ。このあたりを上手く探り当てないとどうにもならない。

採用したものを述べると、次のようなものであるらしい。

  • spring-boot-starter-web: Springのベース
  • spring-boot-starter-data-jpa: データベース系のアレコレが提供される。薄いEntityFrameworkみたいなモンと理解
  • hibernate-community-dialects: SQLiteを使うのに必要。HibernateはORMの1つで、しかしSQLiteは公式サポートではないので亜流版じゃないといけないらしい
  • sqlite-jdbc: SQLite用のドライバと解釈している

で、Javaの場合EF的な立ち位置にはJava Persistence APIなるものがあるのだが、Springの恐らく公式実装でSpring Data JPAなるものがある。それ以外にメジャーなのでHibernateと言うのがあって、どちらも左記のスタンダードの実装になる。が、SQLiteだと対応してないので亜流を使うと。なんなんだこれは。

<parent>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-parent</artifactId>
	<version>3.3.8</version>
	<relativePath /> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>myspiringapp</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>myspiringapp</name>
<description>Demo project for Spring Boot</description>
<url />
<licenses>
	<license />
</licenses>
<developers>
	<developer />
</developers>
<scm>
	<connection />
	<developerConnection />
	<tag />
	<url />
</scm>
<properties>
	<java.version>23</java.version>
</properties>
<dependencies>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-web</artifactId>
	</dependency>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-data-jpa</artifactId>
	</dependency>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-test</artifactId>
		<scope>test</scope>
	</dependency>
	<dependency>
		<groupId>org.hibernate.orm</groupId>
		<artifactId>hibernate-community-dialects</artifactId>
	</dependency>
	<dependency>
		<groupId>org.xerial</groupId>
		<artifactId>sqlite-jdbc</artifactId>
	</dependency>
</dependencies>

<build>
	<plugins>
		<plugin>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-maven-plugin</artifactId>
		</plugin>
	</plugins>
</build>

次にapplication.properties。これはappsettings.jsonみたいなもんだな多分。

spring.application.name=myspiringapp
spring.datasource.url=jdbc:sqlite:database.db
spring.datasource.driver-class-name=org.sqlite.JDBC
spring.jpa.show-sql=true
spring.jpa.database-platform=org.hibernate.community.dialect.SQLiteDialect
spring.jpa.hibernate.ddl-auto=update

マイグレーションの仕方とかあるんだろうか。ちょっと分かってないのだが、これやっただけで勝手にテーブル作ってくれたのでとりあえず良しとしよう。

シークレットとかはどうするかはまだ分かってない。

正直ここまでがアホみたいに苦労した。以下からはサッと行けた。

まずモデルを作っておく。EntityTableのアノテーションが提供されているので、それを付けておく。

package com.example.myspiringapp.models;

import java.time.LocalDateTime;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

@Entity
@Table(name = "todos")
public class Todo {
	@Id
	@GeneratedValue
	public long id;
	
	@Column(name = "title")
	public String title;
	
	@Column(name = "created_at")
	public LocalDateTime createdAt;
	
	@Column(name = "updated_at")
	public LocalDateTime updatedAt;
}

spring-boot-starter-data-jpaにはJpaRepositoryというインターフェースが提供されており、これを使うと良い感じにCRUD用メソッドが載る。接続も上手いことしてくれる。ORMの素晴らしさが一気に溢れ出る瞬間だ。この瞬間だけ脳汁ドバドバ出る。

package com.example.myspiringapp.repositories;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import com.example.myspiringapp.models.Todo;

@Repository
public interface TodoRepository extends JpaRepository<Todo, Long> {

}

これをサービス層でくるんでおく。@Autowiredは恐らくDI的なヤツであり、基本的に勝手に注入されるらしい。

package com.example.myspiringapp.services;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.example.myspiringapp.models.Todo;
import com.example.myspiringapp.repositories.TodoRepository;

@Service
public class TodoService {
	@Autowired
	private TodoRepository todoRepository;

	public List<Todo> all() {
		return this.todoRepository.findAll();
	}

	public Optional<Todo> find(long id) {
		return this.todoRepository.findById(id);
	}

	public Todo create(Todo todo) {
		todo.createdAt = LocalDateTime.now();
		todo.updatedAt = LocalDateTime.now();
		return this.todoRepository.save(todo);
	}

	public Optional<Todo> update(Long id, Todo todo) {
		var t = find(id);
		if (t.isEmpty()) {
			return t;
		}
		var record = t.get();
		record.title = todo.title;
		record.updatedAt = LocalDateTime.now();
		return Optional.of(this.todoRepository.save(record));
	}

	public boolean delete(long id) {
		if (this.todoRepository.existsById(id)) {
			this.todoRepository.deleteById(id);
			return true;
		}
		return false;
	}
}

最後にコントローラを付けたら完成だ。
ちなみに404ってどう返すの、みたいな所はResponseEntityという型があることが分かった。

package com.example.myspiringapp.controllers;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import com.example.myspiringapp.models.Todo;
import com.example.myspiringapp.services.TodoService;

@RestController
public class TodoController {
	@Autowired
	private TodoService todoService;

	@GetMapping("/todos")
	public List<Todo> index() {
		return todoService.all();
	}
	
	@PostMapping("/todos")
	public Todo create(@RequestBody Todo todo) {
		return todoService.create(todo);
	}
	
	@GetMapping("/todos/{id}")
	public ResponseEntity<Todo> show(@PathVariable long id) {
		return todoService
				.find(id).map(ResponseEntity::ok)
				.orElseGet(() -> ResponseEntity.notFound().build());
	}
	
	@PutMapping("/todos/{id}")
	public ResponseEntity<Todo> update(@PathVariable long id, @RequestBody Todo todo) {
		return todoService.update(id, todo)
				.map(ResponseEntity::ok)
				.orElseGet(() -> ResponseEntity.notFound().build());	
	}
	
	@DeleteMapping("/todos/{id}")
	public void delete(@PathVariable long id) {
		todoService.delete(id);
	}
}

余談として、どうもEclipseで起動するとたまーに下手すると停止ボタン押し忘れて開きっぱなしのローカルホストポートのプロセスが殺されないことがある。止め方がワカラン。なのでとりあえずターミナルからkillする。PowerShellで自分用ではコマンドレット化したが、Windowsなら以下でID探してkillすれば良い。

netstat -ano | findstr 8080

ひぇぇ。なんかGoとかNodeのフレームワークってやっぱ楽ちんなんだなあ。というかここまでだと.NETもまあまあ恋しいものだ。

しかし断片的に初歩は理解できたかもしれない。最初のステップは踏めたかな。とりあえず今段階での実験はここまでとする。