Doma2を使えるようにしたい

追加
メソッドの戻り値について
2つのパターンがある
- 変更可能エンティティ
- 不変エンティティ
変更可能エンティティ
@Entity
@Table(name = "t_product")
class ProductEntity {
var id: String = ""
var name: String = ""
var price: Int = 0
var stock: Int = 0
companion object {
fun create(id: String, name: String, price: Int, stock: Int): ProductEntity {
val product = ProductEntity()
product.id = id
product.name = name
product.price = price
product.stock = stock
return product
}
}
}
DAO
@Dao
@ConfigAutowireable
interface ProductDao {
// @Insert(duplicateKeys = ["id"])
@Insert
fun insert(product: ProductEntity): Int
}
main
fun main(args: Array<String>) {
val context: ApplicationContext = AnnotationConfigApplicationContext(
ShoppingApplication::class.java)
val productDao: ProductDao = context.getBean(ProductDao::class.java)
val product = ProductEntity.create(
id = "test_id",
name = "product",
price = 100,
stock = 10
)
val result = productDao.insert(product)
println(result)
}
実行結果
> Task :bootRun
20:42:24.641 [main] INFO org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseFactory -- Starting embedded database: url='jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=false', username='sa'
1
戻り値をUnitにするとエラーになる
エラー: [DOMA4001] The return type must be int that indicates the affected rows count.
戻り値をResultにするとエラーになる
エラー: [DOMA4001] The return type must be int that indicates the affected rows count.
不変エンティティ
エンティティクラスを要素とするorg.seasar.doma.jdbc.Result
を戻り値にしないといけない
Entity
@Entity(immutable = true) // 不変エンティティ
@Table(name = "t_product")
data class ProductEntity(
@Id
val id: String,
val name: String,
val price: Int,
val stock: Int
)
Dao
@Dao
@ConfigAutowireable
interface ProductDao {
@Insert
fun insert(product: ProductEntity): Result<ProductEntity>
}
main
fun main(args: Array<String>) {
val context: ApplicationContext = AnnotationConfigApplicationContext(
ShoppingApplication::class.java)
val productDao: ProductDao = context.getBean(ProductDao::class.java)
val product = ProductEntity(
id = "test_id",
name = "product",
price = 100,
stock = 10
)
val result = productDao.insert(product)
println(result.entity)
}
実行結果
> Task :bootRun
20:04:10.682 [main] INFO org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseFactory -- Starting embedded database: url='jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=false', username='sa'
ProductEntity(id=test_id, name=product, price=100, stock=10)
Deprecated Gradle features were used in this build, making it incompatible with Gradle 9.0.
You can use '--warning-mode all' to show the individual deprecation warnings and determine if they come from your own scripts or plugins.
For more on this, please refer to https://docs.gradle.org/8.5/userguide/command_line_interface.html#sec:command_line_warnings in the Gradle documentation.
BUILD SUCCESSFUL in 1s
10 actionable tasks: 5 executed, 5 up-to-date
1月 04, 2025 8:04:10 午後 com.okr.shopping.infla.doma.dao.ProductDaoImpl insert
情報: [DOMA2220] ENTER : CLASS=com.okr.shopping.infla.doma.dao.ProductDaoImpl, METHOD=insert
1月 04, 2025 8:04:10 午後 com.okr.shopping.infla.doma.dao.ProductDaoImpl insert
情報: [DOMA2076] SQL LOG : PATH=[null],
insert into t_product (id, name, price, stock) values ('test_id', 'product', 100, 10)
1月 04, 2025 8:04:10 午後 com.okr.shopping.infla.doma.dao.ProductDaoImpl insert
情報: [DOMA2221] EXIT : CLASS=com.okr.shopping.infla.doma.dao.ProductDaoImpl, METHOD=insert
20:04:10: 'bootRun' の実行を完了しました。
DAOのメソッドをIntにするとエラーになる
/shopping/build/tmp/kapt3/stubs/main/com/okr/shopping/infla/doma/dao/ProductDao.java:9: エラー: [DOMA4222] When the immutable entity class is a parameter type for the method annotated with such as @Insert, @Update and @Delete, the return type must be org.seasar.doma.jdbc.Result. The type argument of org.seasar.doma.jdbc.Result must be same entity class with the parameter type of the method.
public abstract int insert(@org.jetbrains.annotations.NotNull()
DAOのメソッドをUnitにするとエラーになる
/shopping/build/tmp/kapt3/stubs/main/com/okr/shopping/infla/doma/dao/ProductDao.java:9: エラー: [DOMA4222] When the immutable entity class is a parameter type for the method annotated with such as @Insert, @Update and @Delete, the return type must be org.seasar.doma.jdbc.Result. The type argument of org.seasar.doma.jdbc.Result must be same entity class with the parameter type of the method.
public abstract void insert(@org.jetbrains.annotations.NotNull()

追加処理には2パターン
- 自動生成されるSQLを利用する
- 手動生成のSQLを利用するパターン
自動生成されるSQL
- パラメータの型はEntityでなければならない
- NULLであってはならない
- パラメータは1つだけ

Error
エラーが起きた場合の挙動
プライマリーキーが重複したデータをInsertしてみる
data.sql
INSERT INTO sample
(id, name) VALUES
('1', 'sample01')
;
main関数
try {
sampleDao.insert(
Sample(
id = "1",
name = "sample"
)
)
} catch (e: Exception) {
// 例外クラス名を出力
println("スローされたクラスは::"+e.javaClass.name)
}
実行結果
> Task :bootRun
11:07:40.694 [main] INFO org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseFactory -- Starting embedded database: url='jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=false', username='sa'
11:07:40.777 [main] WARN org.seasar.doma.boot.autoconfigure.DomaAutoConfiguration -- StandardDialect was selected because no explicit configuration and it is not possible to guess from 'spring.datasource.url property'
スローされたクラスは::org.seasar.doma.jdbc.UniqueConstraintException
domaの例外クラスがスローされた

Entity
Entityはテーブルか検索結果のセットに対応するもの
命名規則
@Entity
でクラスを作る場合
テーブル名: sample
Entityクラス名: Sample
ただし、DBには大文字小文字を区別するものと区別しないものや設定によって変更できたりするので、
データベースの種類にあまり依存しないようにするために@Table
を利用した方が良さそう
実験1
以下のようにテーブル名とEntityクラス名を一致させない
テーブル名: sample_table
Entityクラス名: Sample
create ta if not exists sample_table (
id varchar(100) primary key,
name varchar(100)
);
INSERT INTO sample_table
(id, name) VALUES
('i01', 'o01')
;
Entity
@Entity(immutable = true)
data class Sample(
val id: String,
val name: String,
)
DAO
@Dao
@ConfigAutowireable
interface SampleDao {
@Select
fun selectAll(): List<Sample>
}
selectAll.sql
select * from sample_table
main関数
fun main(args: Array<String>) {
val context: ApplicationContext = AnnotationConfigApplicationContext(
ShoppingApplication::class.java
)
val sampleDao: SampleDao = context.getBean(SampleDao::class.java)
sampleDao.selectAll().forEach {
println(it)
}
}
実行結果
> Task :bootRun
21:44:58.360 [main] INFO org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseFactory -- Starting embedded database: url='jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=false', username='sa'
Sample(id=i01, name=o01) // データを取得できている
SELECTのSQLを手動で作成する場合は、テーブル名はsqlに書かれているので、命名規則を意識する必要はない
Entityに検索結果をマッピングしてくれる
実験2
実験1と同様に以下のようにテーブル名とEntityクラス名を一致させない
テーブル名: sample_table
Entityクラス名: Sample
今度はSQLを自動生成するインサートメソッドを行う
Entity
@Entity(immutable = true)
data class Sample(
val id: String,
val name: String,
)
DAO
@Dao
@ConfigAutowireable
interface SampleDao {
@Insert
fun insert(entity: Sample): Result<Sample>
}
main関数
fun main(args: Array<String>) {
val context: ApplicationContext = AnnotationConfigApplicationContext(
ShoppingApplication::class.java
)
val sampleDao: SampleDao = context.getBean(SampleDao::class.java)
sampleDao.selectAll().first().copy(id = "new id", name = "new name").let {
sampleDao.insert(it)
}
}
実行結果
Exception in thread "main" org.seasar.doma.jdbc.JdbcException: [DOMA2016] Cannot get java.sql.PreparedStatement.
PATH=[null].
SQL=[insert into Sample (id, name) values (?, ?)].
The cause is as follows: org.h2.jdbc.JdbcSQLSyntaxErrorException: テーブル "SAMPLE" が見つかりません
Table "SAMPLE" not found; SQL statement:
insert into Sample (id, name) values (?, ?) [42102-224]
at org.seasar.doma.internal.jdbc.util.JdbcUtil.prepareStatement(JdbcUtil.java:37)
at org.seasar.doma.jdbc.command.ModifyCommand.prepareStatement(ModifyCommand.java:75)
at org.seasar.doma.jdbc.command.ModifyCommand.execute(ModifyCommand.java:46)
at com.okr.shopping.infla.doma.dao.SampleDaoImpl.insert(SampleDaoImpl.java:84)
at com.okr.shopping.ShoppingApplicationKt.main(ShoppingApplication.kt:55)
Caused by: org.h2.jdbc.JdbcSQLSyntaxErrorException: テーブル "SAMPLE" が見つかりません
insert into Sample (id, name) values (?, ?)
のクエリが実行されたため、テーブルがないよというエラーが発生
Entity名がそのままテーブル名として扱われているのがわかる

よくデータベース設計をする際の命名規則としては小文字で_区切りで単語を区切ることが多いので、
@Entity(naming = NamingType.SNAKE_LOWER_CASE)
を使うと明示的に指定が不要になる
Entity
@Entity(immutable = true, naming = NamingType.SNAKE_LOWER_CASE)
data class SampleTable(
val id: String,
val name: String,
)
ただ、domaCodeGenというGradleのプラグインが配布されており、DBスキーマを読み込んでコードを自動生成してくれて、テーブル名とカラム名に関しても自動で明示的に設定する記述を書いてくれるので、それを利用するのが良さそう。

SpringBootとdomaの関係
クラス図
domaのConfigクラス及び実装クラスのDomaConfigがHikariDatasourceをdomaに提供して、コネクションプールやjdbcドライバをdomaから呼び出せるようにしている。
doma-spring-boot-starterを入れれば、springbootでdatasourceをDIコンテナに登録すればよしなにやってくれるということを理解してれば良さそう

検索
ページネーション
検索オプションを利用する。
MicroSoft SQLServerとDB2の場合、SQLファイルの記述に条件がある
SelectOptionsでoffsetとlimitを指定して、それをDAOメソッドの引数で渡す。
Null許容にしておけば、どっちでも使えるので、Selectでは大体入れておくと便利かも?
DAO
@Dao
@ConfigAutowireable
interface ProductDao {
// SelectOptionsをnull許容で定義しておくと便利そう
@Select
fun selectAll(options: SelectOptions?): List<ProductEntity>
}
main関数
fun main(args: Array<String>) {
val context: ApplicationContext = AnnotationConfigApplicationContext(
ShoppingApplication::class.java
)
val productDao = context.getBean(ProductDao::class.java)
val dialect = context.getBean(Dialect::class.java)
println(dialect.javaClass.name)
try {
println("商品一覧")
productDao.selectAll().forEach { println(it) } // SelectOptionsなし
println("商品一覧(2件)")
productDao.selectAll(SelectOptions.get().offset(1).limit(2)).forEach { println(it) } SelectOptionsあり
} catch (e: Exception) {
// 例外クラス名を出力
println("スローされたクラスは::"+e.javaClass.name)
println(e.message)
}
}
検索結果とEntity
検索結果をEntityにマッピングする時はカラム名と一致する命名規則のプロパティまたは@Columnで指定したnameのついたプロパティにマッピングされる。
ただし、複数テーブルをjoinした検索結果でカラム名が同じものがあり、@Tableと@Columnを指定しない場合、joinされる側が有効になった。
sampleテーブル
mysql> describe sample;
+-------+--------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------+--------------+------+-----+---------+-------+
| id | varchar(100) | NO | PRI | NULL | |
| name | varchar(100) | YES | | NULL | |
+-------+--------------+------+-----+---------+-------+
mysql> select * from sample;
+----+----------+
| id | name |
+----+----------+
| 1 | sample01 |
+----+----------+
sample_subテーブル
mysql> describe sample_sub;
+-----------+--------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-----------+--------------+------+-----+---------+-------+
| id | varchar(100) | NO | PRI | NULL | |
| parent_id | varchar(100) | YES | MUL | NULL | |
| sub_name | varchar(100) | YES | | NULL | |
+-----------+--------------+------+-----+---------+-------+
mysql> select * from sample_sub;
+-----+-----------+------------+
| id | parent_id | sub_name |
+-----+-----------+------------+
| s01 | 1 | sub_name01 |
+-----+-----------+------------+
SQL
select * from sample join sample_sub on sample.id = sample_sub.parent_id
idカラムが被っている
Entity
@Entity(immutable = true)
@Table(name = "sample")
data class SampleJoin(
val id: String,
val name: String,
@Column(name = "parent_id")
val parentId: String,
@Column(name = "sub_name")
val subName: String,
)
dao
@Dao
@ConfigAutowireable
interface SampleJoinDao {
@Select
fun selectSampleJoin(): List<SampleJoin>
}
main関数
fun main(args: Array<String>) {
val context: ApplicationContext = AnnotationConfigApplicationContext(
ShoppingApplication::class.java
)
val sampleJoinDao = context.getBean(SampleJoinDao::class.java)
sampleJoinDao.selectSampleJoin().forEach {
println(it)
}
}
1とs01どっちがセットされる?
> Task :bootRun
SampleJoin(id=s01, name=sample01, parentId=1, subName=sub_name01)
joinされた側のsample_subテーブルのカラムが指定された。
からならずなのかドキュメントに特に記載がない
コード的にはここ
protected ENTITY build(ResultSet resultSet) throws SQLException {
assertNotNull(resultSet);
if (indexMap == null) {
indexMap = createIndexMap(resultSet.getMetaData(), entityType);
}
Map<String, Property<ENTITY, ?>> states = new HashMap<>(indexMap.size());
for (Map.Entry<Integer, EntityPropertyType<ENTITY, ?>> entry : indexMap.entrySet()) {
Integer index = entry.getKey();
EntityPropertyType<ENTITY, ?> propertyType = entry.getValue();
Property<ENTITY, ?> property = propertyType.createProperty();
fetch(resultSet, property, index, jdbcMappingVisitor);
states.put(propertyType.getName(), property); // <=ここでカラム名をキーにしたマップに詰めている
}
ENTITY entity = entityType.newEntity(states);
if (!entityType.isImmutable()) {
entityType.saveCurrentStates(entity);
}
return entity;
}
でEntityに詰め替えているが、Mapで値を持っていて、カラム名をキーにしているので、最後に詰められたものが勝つようになっている見たい。
順序的には、selectの結果のカラムの順番による