SpringアプリケーションでCSVを介して設定データを外部化する

データ]

1概要

この記事では、アプリケーションの設定データをハードコーディングする代わりに、CSVファイルを使用して外部化します。

このセットアッププロセスは主に新しいシステムに新しいデータをセットアップすることに関係しています。

** 2 CSVライブラリ

**

まずは、CSVで作業するための簡単なライブラリを紹介しましょう。

<dependency>
    <groupId>com.fasterxml.jackson.dataformat</groupId>
    <artifactId>jackson-dataformat-csv</artifactId>
    <version>2.5.3</version>
</dependency>

JavaエコシステムでCSVを扱うための利用可能なライブラリはたくさんあります。

ここで私たちがJacksonを使っているのは、そのアプリケーションでJacksonが既に使用されている可能性があり、データを読み取るために必要な処理はかなり簡単なことです。

3セットアップデータ

プロジェクトごとに異なるデータを設定する必要があります。

このチュートリアルでは、ユーザーデータを設定します - 基本的に 少数のデフォルトユーザーでシステムを準備します

ユーザーを含む簡単なCSVファイルは次のとおりです。

id,username,password,accessToken
1,john,123,token
2,tom,456,test

ファイルの最初の行がどのように ヘッダー行 になっているかに注意してください - データの各行にあるフィールドの名前をリストします。

3 CSVデータローダ

  • CSVファイルから作業メモリにデータを読み込む** ための簡単なデータローダーを作成することから始めましょう。

3.1. オブジェクトのリストを読み込む

ファイルから完全にパラメータ化された特定の Object のListを読み込むために loadObjectList() 機能を実装します。

public <T> List<T> loadObjectList(Class<T> type, String fileName) {
    try {
        CsvSchema bootstrapSchema = CsvSchema.emptySchema().withHeader();
        CsvMapper mapper = new CsvMapper();
        File file = new ClassPathResource(fileName).getFile();
        MappingIterator<T> 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ではあまりサポートされていません - 多対多の関係を読み込むために間接的な方法を使う必要があります。

私たちはこれらを 単純な結合テーブル に似たものとして表現します - だから当然私たちは配列のリストとしてディスクからロードします:

public List<String[]> 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<String[]> 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つ - ロール<→特権__ - は、単純な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設定データ

今度は、CSVファイルから特権、役割、およびユーザーを設定するためのすべての作業を行うために、単純な Setup Beanを使用します。

@Component
public class Setup {
    ...

    @PostConstruct
    private void setupData() {
        setupRolesAndPrivileges();
        setupUsers();
    }

    ...
}

4.1. 役割と特権の設定

まず、 ロールと特権 をディスクから作業メモリにロードしてから、セットアッププロセスの一環としてそれらを保持しましょう。

public List<Privilege> getPrivileges() {
    return csvDataLoader.loadObjectList(Privilege.class, PRIVILEGES__FILE);
}

public List<Role> getRoles() {
    List<Privilege> allPrivileges = getPrivileges();
    List<Role> roles = csvDataLoader.loadObjectList(Role.class, ROLES__FILE);
    List<String[]> rolesPrivileges = csvDataLoader.
      loadManyToManyRelationship(SetupData.ROLES__PRIVILEGES__FILE);

    for (String[]rolePrivilege : rolesPrivileges) {
        Role role = findRoleByName(roles, rolePrivilege[0]);
        Set<Privilege> privileges = role.getPrivileges();
        if (privileges == null) {
            privileges = new HashSet<Privilege>();
        }
        privileges.add(findPrivilegeByName(allPrivileges, rolePrivilege[1]));
        role.setPrivileges(privileges);
    }
    return roles;
}

private Role findRoleByName(List<Role> roles, String roleName) {
    return roles.stream().
      filter(item -> item.getName().equals(roleName)).findFirst().get();
}

private Privilege findPrivilegeByName(List<Privilege> allPrivileges, String privilegeName) {
    return allPrivileges.stream().
      filter(item -> item.getName().equals(privilegeName)).findFirst().get();
}

それでは、ここで永続化作業を行います。

private void setupRolesAndPrivileges() {
    List<Privilege> privileges = setupData.getPrivileges();
    for (Privilege privilege : privileges) {
        setupService.setupPrivilege(privilege);
    }

    List<Role> 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<Privilege> privileges = role.getPrivileges();
        Set<Privilege> persistedPrivileges = new HashSet<Privilege>();
        for (Privilege privilege : privileges) {
            persistedPrivileges.add(privilegeRepository.findByName(privilege.getName()));
        }
        role.setPrivileges(persistedPrivileges);
        roleRepository.save(role); }
}

ロールと特権の両方を作業メモリにロードした後、それらの関係を1つずつロードする方法に注意してください。

4.2. 初期ユーザーの設定

次に、 users をメモリにロードし、それらを保持しましょう

public List<User> getUsers() {
    List<Role> allRoles = getRoles();
    List<User> users = csvDataLoader.loadObjectList(User.class, SetupData.USERS__FILE);
    List<String[]> usersRoles = csvDataLoader.
      loadManyToManyRelationship(SetupData.USERS__ROLES__FILE);

    for (String[]userRole : usersRoles) {
        User user = findByUserByUsername(users, userRole[0]);
        Set<Role> roles = user.getRoles();
        if (roles == null) {
            roles = new HashSet<Role>();
        }
        roles.add(findRoleByName(allRoles, userRole[1]));
        user.setRoles(roles);
    }
    return users;
}

private User findByUserByUsername(List<User> users, String username) {
    return users.stream().
      filter(item -> item.getUsername().equals(username)).findFirst().get();
}

次に、ユーザーの永続化に焦点を当てましょう。

private void setupUsers() {
    List<User> 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<Role> roles = user.getRoles();
        Set<Role> persistedRoles = new HashSet<Role>();
        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<User> users = csvDataLoader.
      loadObjectList(User.class, CsvDataLoader.USERS__FILE);
    assertFalse(users.isEmpty());
}

@Test
public void whenLoadingRolesFromCsvFile__thenLoaded() {
    List<Role> roles = csvDataLoader.
      loadObjectList(Role.class, CsvDataLoader.ROLES__FILE);
    assertFalse(roles.isEmpty());
}

@Test
public void whenLoadingPrivilegesFromCsvFile__thenLoaded() {
    List<Privilege> privileges = csvDataLoader.
      loadObjectList(Privilege.class, CsvDataLoader.PRIVILEGES__FILE);
    assertFalse(privileges.isEmpty());
}

次に、データローダーを使って、多対多の関係をロードしてみましょう。

@Test
public void whenLoadingUsersRolesRelationFromCsvFile__thenLoaded() {
    List<String[]> usersRoles = csvDataLoader.
      loadManyToManyRelationship(CsvDataLoader.USERS__ROLES__FILE);
    assertFalse(usersRoles.isEmpty());
}

@Test
public void whenLoadingRolesPrivilegesRelationFromCsvFile__thenLoaded() {
    List<String[]> rolesPrivileges = csvDataLoader.
      loadManyToManyRelationship(CsvDataLoader.ROLES__PRIVILEGES__FILE);
    assertFalse(rolesPrivileges.isEmpty());
}

6. テスト設定データ

最後に、私たちのBean SetupData に対して簡単な単体テストを実行しましょう。

@Test
public void whenGettingUsersFromCsvFile__thenCorrect() {
    List<User> users = setupData.getUsers();

    assertFalse(users.isEmpty());
    for (User user : users) {
        assertFalse(user.getRoles().isEmpty());
    }
}

@Test
public void whenGettingRolesFromCsvFile__thenCorrect() {
    List<Role> roles = setupData.getRoles();

    assertFalse(roles.isEmpty());
    for (Role role : roles) {
        assertFalse(role.getPrivileges().isEmpty());
    }
}

@Test
public void whenGettingPrivilegesFromCsvFile__thenCorrect() {
    List<Privilege> privileges = setupData.getPrivileges();
    assertFalse(privileges.isEmpty());
}

7. 結論

このクイック記事では、起動時にシステムにロードする必要がある初期データの代替セットアップ方法について説明しました。

これはもちろん単なる概念実証であり、その上に構築するのに適した基盤です - すぐに使用できるソリューション ではありません。

私達はまたリンクによって追跡されるRedditウェブアプリケーションでこのソリューションを使用するつもりです:/case-study-a-reddit-app-with-spring[この進行中のケーススタディ]。