はじめに
ZYYXのアドベントカレンダー企画で1本目の投稿となります
最近、SpringにてDTOやエンティティなどのデータオブジェクトを生成する際にLombokのBuilderが便利と感じたので、備忘録として記します
前提
ここでは、以下のようなEntityをDBから取得して、サービスクラスで姓名を結合した値をDTOとして生成し、返却する場合を考えます
※ MapStructを利用したMapperによる変換は使用しない
※ Javaのバージョンは11
Entityクラス
@ToString
@Getter
@Entity
@Table(name = "user")
@AllArgsConstructor
public class User {
/** ユーザーID */
@id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
BigInteger id;
/** 姓 */
@Column(name = "last_name")
String lastName;
/** 名 */
@Column(name = "first_name")
String firstName;
/** メールアドレス */
@Column(name = "email")
String email;
}
各パターン
Setterのみを利用するパターン
DBからEntityを取得後、DTOをnewにより生成し各フィールドをsetterにてセットするパターン
// DTO
@Data
public class UserDto {
/** ユーザーID */
BigInteger id;
/** 名前(姓 + 名) */
String name;
/** メールアドレス */
String email;
}
// サービスクラス
@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {
private final UserDao userDao;
@Override
public UserDto getBy(String userId) {
final User user = userDao.getById(userId);
if (user == null) return null;
UserDto dto = new UserDto();
dto.setId(user.getId());
String name = user.getSei() + user.getMei();
dto.setName(name);
dto.setEmail(user.getEmail());
return dto;
}
}
メリット
⭕️ DTOを可変として利用できる
⭕️ 基本的な記法のため初学者にも理解しやすい
デメリット
❌ DTOが可変なため、メソッド内で生成時と値が異なる可能性の考慮が必要となり可読性が下がる
❌ DTOをsetterにて都度セットする形式のため、フィールドに対するsetterの呼び忘れに気づきにくい
❌ コード量が多い
コンストラクタを利用するパターン
DBからEntityを取得後、コンストラクタを利用しDTOを生成するパターン
// DTO
@Value
public class UserDto {
/** ユーザーID */
BigInteger id;
/** 名前(姓 + 名) */
String name;
/** メールアドレス */
String email;
}
// サービスクラス
@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {
private final UserDao userDao;
@Override
public UserDto getBy(String userId) {
final User user = userDao.getById(userId);
if (user == null) return null;
String name = user.getSei() + user.getMei();
return new UserDto(id, name, email);
}
}
メリット
⭕️ コンストラクタが単一の場合、必須項目を強制できる(コンパイル時にセット漏れが検出可能)
⭕️ 不変とできるため、コードの可読性が高い
デメリット
❌ 引数が増えた場合に読みにくい
❌ 引数順を誤ってもバグに気づきにくい
→new UserDto(id, email, name)となっても、同じStringのためエラーとならない
❌ コンストラクタにてフィールドに任意項目がある場合、パターンごとにコンストラクタを作成する必要がある
Builderを利用するパターン
DBからEntityを取得後、Builderを利用しDTOを生成するパターン
// DTO
@Value
@Builder
public class UserDto {
/** ユーザーID */
BigInteger id;
/** 名前(姓 + 名) */
String name;
/** メールアドレス */
String email;
}
// サービスクラス
@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {
private final UserDao userDao;
@Override
public UserDto getBy(String userId) {
final User user = userDao.getById(userId);
if (user == null) return null;
String name = user.getSei() + user.getMei();
return UserDto.builder()
.id(user.getId())
.name(name)
.email(user.getEmail())
.build();
}
}
メリット
⭕️ フィールドに対してセットする値の対応がわかりやすい
⭕️ 不変とできるため、コードの可読性が高い
⭕️ セットするフィールドに任意項目かある場合でも1つのDTOのコードで対応ができる
デメリット
❌ DTO生成時にパラメータを設定し忘れても、気づきにくい
→setterのセットし忘れと同じ
まとめ
今回はDTOのフィールド数が少ないため、コンストラクタでも十分な選択肢となります
ただ、フィールド数が増えるとデメリットが大きくなり、可読性や保守性の観点からBuilderを利用したパターンがいいと感じました
また、Javaのバージョンが高ければRecord型も利用できるため、そちらも検討するといいと思います