Сохраните значение перечисления в базе данных

#java #spring #spring-boot #enums #spring-data-jpa

Вопрос:

Я хочу использовать ПЕРЕЧИСЛЕНИЕ для отображения значений в строки таблицы базы данных:

Поисковые параметры бизнес-клиентов:

 @Getter
@Setter
public class BusinessCustomersSearchParams {

    private String title;

    private List<String> status;

    private LocalDateTime createdAt;

    private LocalDateTime updatedAt;
}
 

Спецификация:

 @Override
public Page<BusinessCustomersFullDTO> findBusinessCustomers(BusinessCustomersSearchParams params, Pageable pageable)
{
    Specification<BusinessCustomers> spec = (root, query, cb) -> {
        List<Predicate> predicates = new ArrayList<>();
        if (params.getTitle() != null) {
            predicates.add(cb.like(cb.lower(root.get("description")), "%"   params.getTitle().toLowerCase()   "%"));
        }

        final List<String> statuses = Optional.ofNullable(params.getStatus()).orElse(Collections.emptyList());
        if (statuses != null amp;amp; !statuses.isEmpty()){
            List<BusinessCustomersStatus> statusesAsEnum = statuses.stream()
                .map(status -> BusinessCustomersStatus.fromStatus(status))
                .collect(Collectors.toList())
                ;

            predicates.add(root.get("status").in(statusesAsEnum));
        }

        return cb.and(predicates.toArray(new Predicate[predicates.size()]));
    };
    return businessCustomersService.findAll(spec, pageable).map(businessCustomersMapper::toFullDTO);
}
 

Преобразователь атрибутов:

 import javax.persistence.AttributeConverter;
import javax.persistence.Converter;

@Converter
public class BusinessCustomersStatusAttributeConverter
    implements AttributeConverter<BusinessCustomersStatus, String> {

  public String convertToDatabaseColumn( BusinessCustomersStatus value ) {
    if ( value == null ) {
      return null;
    }

    return value.getStatus();
  }

  public BusinessCustomersStatus convertToEntityAttribute( String value ) {
    if ( value == null ) {
      return null;
    }

    return BusinessCustomersStatus.fromStatus( value );
  }

}
 

Enum:

 package org.merchant.database.service.businesscustomers;

public enum BusinessCustomersStatus {
    A("active"),
    O("onboarding"),
    N("not_verified"),
    V("verified"),
    S("suspended"),
    I("inactive");

    private String status;

    BusinessCustomersStatus(String status)
    {
        this.status = status;
    }

    public String getStatus() {
        return status;
    }

    public static BusinessCustomersStatus fromStatus(String status) {
        switch (status) {
            case "active": {
                return A;
            }

            case "onboarding": {
                return O;
            }

            case "not_verified": {
                return NV;
            }

            case "verified": {
                return V;
            }

            case "suspended": {
                return S;
            }

            case "inactive": {
                return I;
            }

            default: {
                throw new UnsupportedOperationException(
                    String.format("Unkhown status: '%s'", status)
                );
            }
        }
    }
}
 

Сущность:

 @Entity
@Table(name = "business_customers")
public class BusinessCustomers implements Serializable {
   
    ..........
    @Convert( converter = BusinessCustomersStatusAttributeConverter.class )
    private BusinessCustomersStatus status;
    ......
}
 

Полный пример кода: https://github.com/rcbandit111/Search_specification_POC

Я отправляю http-запрос с параметрами list?size=5amp;page=0amp;status=active,suspended и получаю результат с заглавными буквами «статус»: «АКТИВЕН».

Я хочу выполнить поиск и получить статус от FE для статуса, используя status=active , но сохраняя в поле строки базы данных только символ A.

Как я могу сохранить в базе данных ключ перечисления A?

Комментарии:

1. Обычно для сохранения «имени» перечисления в базе @Enumerated(EnumType.STRING) данных над свойством перечисления private BusinessCustomersStatus status; в Сущности используется следующая аннотация. BusinessCustomersStatusAttributeConverter По какой причине вы создали пользовательский конвертер?

2. Просто измените ACTIVE("active") на ACTIVE("ACTIVE") , и аналогично для всех остальных элементов. Или вообще избавьтесь от status элемента, так как вы, похоже, не хотите его использовать, и name() вместо этого используйте атрибут. Вы приложили немало усилий, чтобы создать отображение, которое вам, похоже, на самом деле не нужно. Почему?

3. Я обновил сообщение. Не могли бы вы дать мне совет, пожалуйста?

Ответ №1:

Чтобы сохранить фактическое значение перечисления в базе данных, вы можете сделать две вещи.

Один из них, как предложил @PetarBivolarski, изменяет метод convertToDatabaseColumn AttributeConverter и возвращает value.name() вместо value.getStatus() . Но, пожалуйста, имейте в виду, что кроме того, вам также потребуется обновить convertToEntityAttribute , чтобы учесть это изменение:

 import javax.persistence.AttributeConverter;
import javax.persistence.Converter;

@Converter
public class BusinessCustomersStatusAttributeConverter
    implements AttributeConverter<BusinessCustomersStatus, String> {

  public String convertToDatabaseColumn( BusinessCustomersStatus value ) {
    if ( value == null ) {
      return null;
    }

    return value.name();
  }

  public BusinessCustomersStatus convertToEntityAttribute( String value ) {
    if ( value == null ) {
      return null;
    }

    return BusinessCustomersStatus.valueOf( value );
  }

}
 

Если вы подумаете об этом, более простым решением будет просто сохранить status поле как @Enumerated :

 @Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder(toBuilder = true)
@Entity
@Table(name = "business_customers")
public class BusinessCustomers {

    //...

    @Enumerated(EnumType.STRING)
    @Column(name = "status", length = 20)
    private BusinessCustomersStatus status;

    //...
}
 

Это, кроме того, больше соответствует остальной части вашего кода.

Что касается вашей второй проблемы, приложение возвращается "status":"ACTIVE" , потому BusinessCustomersFullDTO что вы определяете поле состояния как String , и это поле получает результат процесса сопоставления @Mapstruct , выполненного BusinessCustomersMapper и.

Чтобы решить эту проблему, как я предлагал вам ранее, вы можете изменить свой Mapper , чтобы обработать желаемое пользовательское преобразование:

 import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Named;
import org.merchant.config.BaseMapperConfig;
import org.merchant.database.entity.BusinessCustomers;
import org.merchant.database.service.businesscustomers.BusinessCustomersStatus;
import org.merchant.dto.businesscustomers.BusinessCustomersFullDTO;

@Mapper(config = BaseMapperConfig.class)
public interface BusinessCustomersMapper {

    @Mapping(source = "status", target = "status", qualifiedByName = "businessCustomersToDTOStatus")
    BusinessCustomersFullDTO toFullDTO(BusinessCustomers businessCustomers);


    @Named("busineessCustomersToDTOStatus")
    public static String businessCustomersToDTOStatus(final BusinessCustomersStatus status) {
        if (status == null) {
            return null;
        }

        return status.getStatus();
    }
}
 

Если вы не предпочитаете это решение, возможно, вы можете использовать другой подход: он будет заключаться в следующем. Идея заключается в изменении поведения сериализации и десериализации Джексона BusinessCustomersFullDTO . На самом деле, в вашем случае необходимо только изменить логику сериализации.

Во-первых, определите status поле BusinessCustomersFullDTO в терминах BusinessCustomersStatus также:

 @Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder(toBuilder = true)
public class BusinessCustomersFullDTO {

    private long id;

    private String name;

    private String businessType;

    private BusinessCustomersStatus status;

    private String description;

    private String country;

    private String address1;
}
 

Чтобы завершить решение, пожалуйста, внесите следующие изменения в BusinessCustomersStatus перечисление:

 public enum BusinessCustomersStatus {
    A("active"),
    O("onboarding"),
    NV("not_verified"),
    V("verified"),
    S("suspended"),
    I("inactive");

    private String status;

    BusinessCustomersStatus(String status)
    {
        this.status = status;
    }

    // Define the status field as the enum representation by using @JsonValue
    @JsonValue
    public String getStatus() {
        return status;
    }

    // Use the fromStatus method as @JsonCreator
    @JsonCreator
    public static BusinessCustomersStatus fromStatus(String status) {
        if (StringUtils.isEmpty(status)) {
            return null;
        }

        switch (status) {
            case "active": {
                return A;
            }

            case "onboarding": {
                return O;
            }

            case "not_verified": {
                return NV;
            }

            case "verified": {
                return V;
            }

            case "suspended": {
                return S;
            }

            case "inactive": {
                return I;
            }

            default: {
                throw new UnsupportedOperationException(
                        String.format("Unkhown status: '%s'", status)
                );
            }
        }
    }
}
 

Обратите внимание на включение @JsonValue @JsonCreator аннотаций и: последнее используется для десериализации, которая мне кажется ненужной в вашем приложении, но на всякий случай.

Пожалуйста, ознакомьтесь с соответствующей документацией по предоставленным аннотациям Джексона.

Комментарии:

1. Спасибо вам за ответ. Я думаю, что в BusinessCustomersFullDTO строке private BusinessCustomersStatus status; должно быть private String status; для того, чтобы выполнить поиск по нескольким значениям?

2. Привет @PeterPenzov. Нет, пожалуйста, не смешивайте эти два понятия. С одной стороны, у вас есть BusinessCustomersSearchParams , и в этом dto status поле представлено как a List , чтобы вы могли фильтровать по разным значениям. Но здесь, у BusinessCustomersFullDTO вас есть единственный результат. Поскольку, похоже, вы предпочитаете не менять свой картограф, просто сохраняйте status как BusinessCustomersStatus status есть и используйте предложенные @JsonValue и @JsonCreator аннотации. Пожалуйста, не могли бы вы попробовать и посмотреть?

Ответ №2:

Обратите внимание на свой convertToDatabaseColumn() метод BusinessCustomersStatusAttributeConverter .

Он должен вернуться value.name() вместо value.getStatus() этого .

Комментарии:

1. Похоже, теперь мне нужно изменить что-то еще? Я java.lang.UnsupportedOperationException: Unknown status: 'ACTIVE' , наверное, доберусь сюда? github.com/rcbandit111/Search_specification_POC/blob/main/src/…

2. Вам вообще не нужен статусный член, поэтому вы можете просто удалить его. Просто используйте метод .name() ПЕРЕЧИСЛЕНИЯ или, как правило, лучше удалите весь пользовательский конвертер и используйте аннотацию @Enumerated(EnumType. СТРОКА) как объясняет @pleft в своем комментарии

3. @PeterPenzov, так что это означает, что вы определили enum в базе данных строчный регистр. Итак, вы пошли на все эти хлопоты, чтобы попасть в нижний регистр. Почему именно вы думаете, что вам нужно изменить свое мнение обо всем этом?

4. Потому что я хочу хранить только в БД A и возвращаться к active значению пользовательского интерфейса.

5. @PeterPenzov Что вы имеете в виду под лучшим решением? Что не так с моим решением? Или ты меня не спрашиваешь?