Externe Setup-Daten über CSV in einer Spring-Anwendung

Veröffentlichen Sie Setup-Daten über CSV in einer Spring-Anwendung

1. Überblick

In diesem Artikel werdenexternalize the setup data of an application using CSV files verwendet, anstatt sie fest zu codieren.

Dieser Einrichtungsprozess befasst sich hauptsächlich mit dem Einrichten neuer Daten auf einem neuen System.

2. Eine CSV-Bibliothek

Beginnen wir mit der Einführung einer einfachen Bibliothek für die Arbeit mit CSV -Jackson CSV extension:


    com.fasterxml.jackson.dataformat
    jackson-dataformat-csv
    2.5.3

Es gibt natürlich eine Vielzahl verfügbarer Bibliotheken, um mit CSVs im Java-Ökosystem zu arbeiten.

Der Grund, warum wir uns hier für Jackson entscheiden, ist folgender: Es ist wahrscheinlich, dass Jackson bereits in der Anwendung verwendet wird und die Verarbeitung zum Lesen der Daten ziemlich einfach ist.

3. Die Setup-Daten

Unterschiedliche Projekte müssen unterschiedliche Daten einrichten.

In diesem Tutorial richten wir Benutzerdaten ein - im Grundepreparing the system with a few default users.

Hier ist die einfache CSV-Datei mit den Benutzern:

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

Beachten Sie, wie die erste Zeile der Dateithe header row ist - und listet die Namen der Felder in jeder Datenzeile auf.

3. CSV Data Loader

Beginnen wir mit der Erstellung eines einfachen Datenladers fürread up data from the CSV files into working memory.

3.1. Laden Sie eine Liste von Objekten

Wir implementieren dieloadObjectList()-Funktionalität, um eine vollständig parametrisierte Liste spezifischerObject aus der Datei zu laden:

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();
    }
}

Anmerkungen:

  • Wir haben dieCSVSchema basierend auf der ersten "Header" -Zeile erstellt.

  • Die Implementierung ist generisch genug, um jeden Objekttyp zu verarbeiten.

  • Wenn ein Fehler auftritt, wird eine leere Liste zurückgegeben.

3.2. Behandeln Sie viele zu viele Beziehungen

Verschachtelte Objekte werden in Jackson CSV nicht gut unterstützt. Wir müssen eine indirekte Methode verwenden, um Many-to-Many-Beziehungen zu laden.

Wir werden diesesimilar to simple Join Tables darstellen - also werden wir natürlich als Liste von Arrays von der Festplatte laden:

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();
    }
}

So wird eine dieser Beziehungen -Roles <→ Privileges - in einer einfachen CSV-Datei dargestellt:

role,privilege
ROLE_ADMIN,ADMIN_READ_PRIVILEGE
ROLE_ADMIN,ADMIN_WRITE_PRIVILEGE
ROLE_SUPER_USER,POST_UNLIMITED_PRIVILEGE
ROLE_USER,POST_LIMITED_PRIVILEGE

Beachten Sie, wie wir den Header in dieser Implementierung ignorieren, da wir diese Informationen nicht wirklich benötigen.

4. Daten einrichten

Jetzt verwenden wir eine einfacheSetup-Bean, um die gesamte Arbeit zum Einrichten von Berechtigungen, Rollen und Benutzern aus CSV-Dateien zu erledigen:

@Component
public class Setup {
    ...

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

    ...
}

4.1. Richten Sie Rollen und Berechtigungen ein

Laden Sie zunächstroles and privileges von der Festplatte in den Arbeitsspeicher und behalten Sie sie dann als Teil des Einrichtungsprozesses bei:

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();
}

Dann machen wir hier die beharrliche Arbeit:

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);
    }
}

Und hier sind unsereSetupService:

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); }
}

Beachten Sie, dass nach dem Laden von Rollen und Berechtigungen in den Arbeitsspeicher die Beziehungen nacheinander geladen werden.

4.2. Erstbenutzer einrichten

Als nächstes laden wir dieusers in den Speicher und behalten sie bei:

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();
}

Als nächstes konzentrieren wir uns darauf, die Benutzer zu behalten:

private void setupUsers() {
    List users = setupData.getUsers();
    for (User user : users) {
        setupService.setupUser(user);
    }
}

Und hier sind unsereSetupService:

@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);
    }
}

Und hier ist die Methode voncreateSimplePreference():

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);
}

Beachten Sie, dass wir vor dem Speichern eines Benutzers eine einfachePreference-Entität dafür erstellen und diese zuerst beibehalten.

5. Testen Sie den CSV Data Loader

Als nächstes führen wir einen einfachen Komponententest für unsereCsvDataLoader durch:

Wir werden die Ladeliste der Benutzer, Rollen und Privilegien testen:

@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());
}

Testen wir als Nächstes das Laden einiger Many-to-Many-Beziehungen über den Datenlader:

@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. Setup-Daten testen

Lassen Sie uns abschließend einen einfachen Unit-Test für unsere BeanSetupDatadurchführen:

@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. Fazit

In diesem kurzen Artikel haben wir eine alternative Einrichtungsmethode für die anfänglichen Daten untersucht, die normalerweise beim Start in ein System geladen werden müssen. Dies ist natürlich nur ein einfacher Proof of Concept und eine gute Basis, auf der man aufbauen kann -not a production ready solution.

Wir werden diese Lösung auch in der Reddit-Webanwendung verwenden, die vonthis ongoing case study erfasst wird.