Springアプリケーションで設定データをCSVで外部化する
1. 概要
この記事では、ハードコーディングする代わりに、externalize the setup data of an application using CSV filesを使用します。
このセットアッププロセスは、主に新しいシステムでの新しいデータのセットアップに関係しています。
2. CSVライブラリ
まず、CSVを操作するための簡単なライブラリ(Jackson CSV extension)を紹介します。
com.fasterxml.jackson.dataformat
jackson-dataformat-csv
2.5.3
もちろん、JavaエコシステムでCSVを操作するための利用可能なライブラリが多数あります。
ここでJacksonを使用する理由は、Jacksonがアプリケーションですでに使用されている可能性が高く、データを読み取るために必要な処理がかなり単純であるためです。
3. セットアップデータ
プロジェクトごとに異なるデータを設定する必要があります。
このチュートリアルでは、ユーザーデータ(基本的にはpreparing the system with a few default users)を設定します。
ユーザーを含む簡単なCSVファイルは次のとおりです。
id,username,password,accessToken
1,john,123,token
2,tom,456,test
ファイルの最初の行がthe header rowであることに注意してください–データの各行のフィールドの名前をリストします。
3. CSVデータローダー
read up data from the CSV files into working memoryへの単純なデータローダーを作成することから始めましょう。
3.1. オブジェクトのリストをロードする
loadObjectList()機能を実装して、ファイルから特定のObjectの完全にパラメーター化されたリストをロードします。
public List loadObjectList(Class type, String fileName) {
try {
CsvSchema bootstrapSchema = CsvSchema.emptySchema().withHeader();
CsvMapper mapper = new CsvMapper();
File file = new ClassPathResource(fileName).getFile();
MappingIterator readValues =
mapper.reader(type).with(bootstrapSchema).readValues(file);
return readValues.readAll();
} catch (Exception e) {
logger.error("Error occurred while loading object list from file " + fileName, e);
return Collections.emptyList();
}
}
ノート:
-
最初の「ヘッダー」行に基づいてCSVSchemaを作成しました。
-
実装は、あらゆるタイプのオブジェクトを処理するのに十分汎用的です。
-
エラーが発生すると、空のリストが返されます。
3.2. 多対多の関係を処理する
ネストされたオブジェクトは、Jackson CSVでは十分にサポートされていません。多対多の関係を読み込むには、間接的な方法を使用する必要があります。
これらのsimilar to simple Join Tablesを表すので、当然、ディスクから配列のリストとしてロードします。
public List loadManyToManyRelationship(String fileName) {
try {
CsvMapper mapper = new CsvMapper();
CsvSchema bootstrapSchema = CsvSchema.emptySchema().withSkipFirstDataRow(true);
mapper.enable(CsvParser.Feature.WRAP_AS_ARRAY);
File file = new ClassPathResource(fileName).getFile();
MappingIterator readValues =
mapper.reader(String[].class).with(bootstrapSchema).readValues(file);
return readValues.readAll();
} catch (Exception e) {
logger.error(
"Error occurred while loading many to many relationship from file = " + fileName, e);
return Collections.emptyList();
}
}
これらの関係の1つであるRoles <→ Privilegesが単純なCSVファイルでどのように表されるかを次に示します。
role,privilege
ROLE_ADMIN,ADMIN_READ_PRIVILEGE
ROLE_ADMIN,ADMIN_WRITE_PRIVILEGE
ROLE_SUPER_USER,POST_UNLIMITED_PRIVILEGE
ROLE_USER,POST_LIMITED_PRIVILEGE
この実装ではヘッダーを実際には必要としないため、ヘッダーを無視していることに注意してください。
4. 設定データ
次に、単純なSetup Beanを使用して、CSVファイルから特権、ロール、およびユーザーを設定するすべての作業を実行します。
@Component
public class Setup {
...
@PostConstruct
private void setupData() {
setupRolesAndPrivileges();
setupUsers();
}
...
}
4.1. 役割と権限の設定
まず、roles and privilegesをディスクから作業メモリーにロードしてから、セットアッププロセスの一部として永続化します。
public List getPrivileges() {
return csvDataLoader.loadObjectList(Privilege.class, PRIVILEGES_FILE);
}
public List getRoles() {
List allPrivileges = getPrivileges();
List roles = csvDataLoader.loadObjectList(Role.class, ROLES_FILE);
List rolesPrivileges = csvDataLoader.
loadManyToManyRelationship(SetupData.ROLES_PRIVILEGES_FILE);
for (String[] rolePrivilege : rolesPrivileges) {
Role role = findRoleByName(roles, rolePrivilege[0]);
Set privileges = role.getPrivileges();
if (privileges == null) {
privileges = new HashSet();
}
privileges.add(findPrivilegeByName(allPrivileges, rolePrivilege[1]));
role.setPrivileges(privileges);
}
return roles;
}
private Role findRoleByName(List roles, String roleName) {
return roles.stream().
filter(item -> item.getName().equals(roleName)).findFirst().get();
}
private Privilege findPrivilegeByName(List allPrivileges, String privilegeName) {
return allPrivileges.stream().
filter(item -> item.getName().equals(privilegeName)).findFirst().get();
}
次に、ここで永続的な作業を行います。
private void setupRolesAndPrivileges() {
List privileges = setupData.getPrivileges();
for (Privilege privilege : privileges) {
setupService.setupPrivilege(privilege);
}
List roles = setupData.getRoles();
for (Role role : roles) {
setupService.setupRole(role);
}
}
そしてここに私たちのSetupServiceがあります:
public void setupPrivilege(Privilege privilege) {
if (privilegeRepository.findByName(privilege.getName()) == null) {
privilegeRepository.save(privilege);
}
}
public void setupRole(Role role) {
if (roleRepository.findByName(role.getName()) == null) {
Set privileges = role.getPrivileges();
Set persistedPrivileges = new HashSet();
for (Privilege privilege : privileges) {
persistedPrivileges.add(privilegeRepository.findByName(privilege.getName()));
}
role.setPrivileges(persistedPrivileges);
roleRepository.save(role); }
}
ロールと特権の両方を作業メモリにロードした後、それらの関係を1つずつロードする方法に注意してください。
4.2. 初期ユーザーの設定
次へ–usersをメモリにロードし、永続化します。
public List getUsers() {
List allRoles = getRoles();
List users = csvDataLoader.loadObjectList(User.class, SetupData.USERS_FILE);
List usersRoles = csvDataLoader.
loadManyToManyRelationship(SetupData.USERS_ROLES_FILE);
for (String[] userRole : usersRoles) {
User user = findByUserByUsername(users, userRole[0]);
Set roles = user.getRoles();
if (roles == null) {
roles = new HashSet();
}
roles.add(findRoleByName(allRoles, userRole[1]));
user.setRoles(roles);
}
return users;
}
private User findByUserByUsername(List users, String username) {
return users.stream().
filter(item -> item.getUsername().equals(username)).findFirst().get();
}
次に、ユーザーの永続化に焦点を当てましょう。
private void setupUsers() {
List users = setupData.getUsers();
for (User user : users) {
setupService.setupUser(user);
}
}
そしてここに私たちのSetupServiceがあります:
@Transactional
public void setupUser(User user) {
try {
setupUserInternal(user);
} catch (Exception e) {
logger.error("Error occurred while saving user " + user.toString(), e);
}
}
private void setupUserInternal(User user) {
if (userRepository.findByUsername(user.getUsername()) == null) {
user.setPassword(passwordEncoder.encode(user.getPassword()));
user.setPreference(createSimplePreference(user));
Set roles = user.getRoles();
Set persistedRoles = new HashSet();
for (Role role : roles) {
persistedRoles.add(roleRepository.findByName(role.getName()));
}
user.setRoles(persistedRoles);
userRepository.save(user);
}
}
そしてここにcreateSimplePreference()メソッドがあります:
private Preference createSimplePreference(User user) {
Preference pref = new Preference();
pref.setId(user.getId());
pref.setTimezone(TimeZone.getDefault().getID());
pref.setEmail(user.getUsername() + "@test.com");
return preferenceRepository.save(pref);
}
ユーザーを保存する前に、そのユーザー用の単純なPreferenceエンティティを作成し、それを最初に永続化する方法に注意してください。
5. CSVデータローダーのテスト
次に、CsvDataLoaderで簡単な単体テストを実行しましょう。
ユーザー、ロール、および特権のロードリストをテストします。
@Test
public void whenLoadingUsersFromCsvFile_thenLoaded() {
List users = csvDataLoader.
loadObjectList(User.class, CsvDataLoader.USERS_FILE);
assertFalse(users.isEmpty());
}
@Test
public void whenLoadingRolesFromCsvFile_thenLoaded() {
List roles = csvDataLoader.
loadObjectList(Role.class, CsvDataLoader.ROLES_FILE);
assertFalse(roles.isEmpty());
}
@Test
public void whenLoadingPrivilegesFromCsvFile_thenLoaded() {
List privileges = csvDataLoader.
loadObjectList(Privilege.class, CsvDataLoader.PRIVILEGES_FILE);
assertFalse(privileges.isEmpty());
}
次に、データローダーを介して多対多の関係の読み込みをテストしてみましょう。
@Test
public void whenLoadingUsersRolesRelationFromCsvFile_thenLoaded() {
List usersRoles = csvDataLoader.
loadManyToManyRelationship(CsvDataLoader.USERS_ROLES_FILE);
assertFalse(usersRoles.isEmpty());
}
@Test
public void whenLoadingRolesPrivilegesRelationFromCsvFile_thenLoaded() {
List rolesPrivileges = csvDataLoader.
loadManyToManyRelationship(CsvDataLoader.ROLES_PRIVILEGES_FILE);
assertFalse(rolesPrivileges.isEmpty());
}
6. テストセットアップデータ
最後に、BeanSetupDataで簡単な単体テストを実行しましょう。
@Test
public void whenGettingUsersFromCsvFile_thenCorrect() {
List users = setupData.getUsers();
assertFalse(users.isEmpty());
for (User user : users) {
assertFalse(user.getRoles().isEmpty());
}
}
@Test
public void whenGettingRolesFromCsvFile_thenCorrect() {
List roles = setupData.getRoles();
assertFalse(roles.isEmpty());
for (Role role : roles) {
assertFalse(role.getPrivileges().isEmpty());
}
}
@Test
public void whenGettingPrivilegesFromCsvFile_thenCorrect() {
List privileges = setupData.getPrivileges();
assertFalse(privileges.isEmpty());
}
7. 結論
このクイック記事では、通常、起動時にシステムにロードする必要がある初期データの代替セットアップ方法を検討しました。 もちろん、これは単純な概念実証であり、その上に構築するのに適した基盤です–not a production ready solution。
また、this ongoing case studyによって追跡されるRedditWebアプリケーションでもこのソリューションを使用します。