Use Liquibase to Safely Evolve Your Database Schema


1.概述

在本文中,我们将使用CSV文件外部化应用程序的设置数据,而不是对其进行硬编码。

此设置过程主要涉及在新系统上设置新数据。

2. CSV库

让我们首先介绍一个简单的库来处理CSV - Jackson CSV扩展:

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

当然,有许多可用的库可以在Java生态系统中使用CSV。

我们在这里与杰克逊合作的原因是 - 杰克逊可能已经在应用程序中使用,我们读取数据所需的处理相当简单。

3.设置数据

不同的项目需要设置不同的数据。

在本教程中,我们将设置用户数据 - 基本上准备一些默认用户的系统。

这是包含用户的简单CSV文件:

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

请注意文件的第一行是标题行 - 列出每行数据中字段的名称。

3. CSV数据加载器

让我们从创建一个简单的数据加载器开始,将CSV文件中的数据读入工作内存。

3.1。加载对象列表

我们将实现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();
    }
}

以下是其中一个关系 - 角色< - >权限 - 如何在一个简单的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。设置角色和权限

首先,让我们将角色和特权从磁盘加载到工作内存中,然后在设置过程中保留它们:

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

请注意,在我们将角色和权限加载到工作内存后,我们将逐个加载它们的关系。

4.2。设置初始用户

接下来 - 让我们将用户加载到内存中并保留它们:

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