Blazebit / blaze-persistence

Rich Criteria API for JPA providers
https://persistence.blazebit.com
Apache License 2.0
733 stars 87 forks source link

@Mapping(fetch = FetchStrategy.MULTISET) breaks boolean fields mapping. #1830

Open roma2341 opened 11 months ago

roma2341 commented 11 months ago

@Mapping(fetch = FetchStrategy.MULTISET) breaks boolean fields mapping.

Description

My DripTriggerEntityView class boolean fields are always false If I use @Mapping(fetch = FetchStrategy.MULTISET). If I use @Mapping(fetch = FetchStrategy.JOIN) everything is ok, boolean fields have correct values.

My views and method to fetch view:

 public Optional<DripCampaignEntityView> findOneById(Long id){
        CriteriaBuilder<DripCampaignEntity> cb = cbFactory.create(em, DripCampaignEntity.class)
                .where(DripCampaignEntity_.DELETED_AT).isNull()
                .where(DripCampaignEntity_.ID).eq(id);

        CriteriaBuilder<DripCampaignEntityView> postWithAuthorViewCriteriaBuilder =
                viewManager.applySetting(EntityViewSetting.create(DripCampaignEntityView.class), cb);
        return Optional.of(postWithAuthorViewCriteriaBuilder.getSingleResult());
    }

@EntityView(DripCampaignEntity.class)
public interface DripCampaignEntityFlatView {
    Date getCreatedAt();

    Date getUpdatedAt();

    Long getCreatedById();

    Long getLastModifiedById();

    Long getOwnerId();

    @IdMapping
    Long getId();

    String getName();

    Instant getPreferredSendDate();

    Instant getPreferredSendTime();

    boolean isEnabled();

    Instant getLastSentAt();

    int getStepsCount();

    Instant getDeletedAt();
}

@EntityView(DripCampaignEntity.class)
public interface DripCampaignEntityView extends DripCampaignEntityFlatView {
    @Mapping(fetch = FetchStrategy.MULTISET)
    List<DripCampaignStepEntityView> getSteps();

    @Mapping(fetch = FetchStrategy.MULTISET)
    Set<DripTriggerEntityView> getCampaignSubscriptionTriggers();
}

@EntityView(DripTriggerEntity.class)
@EntityViewInheritance
public interface DripTriggerEntityView {

    @IdMapping
    Long getId();
    Date getCreatedAt();

    Date getUpdatedAt();

    Long getCreatedById();

    Long getLastModifiedById();

    Long getOwnerId();

    boolean isRetrospectionEnabled();

    boolean isEnabled();

    DripTriggerType getType();
}

It generates query:

select dripcampai0_.id                                         as col_0_0_,
       (select json_arrayagg(json_object('f0', cast(case
                                                        when driptrigge1_.type = 'USER_ADDED_TO_CONTACT_LIST' then 1
                                                        when driptrigge1_.type = 'USER_TAG_ASSIGNED' then 2
                                                        else 0 end as char), 'f1', cast(driptrigge1_.id as char), 'f2',
                                         cast(driptrigge1_.created_at as char), 'f3',
                                         cast(driptrigge1_.created_by as char), 'f4',
                                         cast(driptrigge1_.enabled as char), 'f5',
                                         cast(driptrigge1_.last_modified_by as char), 'f6',
                                         cast(driptrigge1_.owner_id as char), 'f7',
                                         cast(driptrigge1_.retrospection_enabled as char), 'f8',
                                         cast(driptrigge1_.type as char), 'f9', cast(driptrigge1_.updated_at as char),
                                         'f10',
                                         (select json_arrayagg(json_object('f0', cast(crmcontact3_.id as char), 'f1',
                                                                           cast(crmcontact3_.content_type as char),
                                                                           'f2', cast(crmcontact3_.deleted_at as char),
                                                                           'f3', cast(crmcontact3_.description as char),
                                                                           'f4', cast(crmcontact3_.dynamic as char),
                                                                           'f5', cast(crmcontact3_.global as char),
                                                                           'f6',
                                                                           cast(crmcontact3_.last_reculculation_at as char),
                                                                           'f7', cast(crmcontact3_.name as char), 'f8',
                                                                           cast(crmcontact3_.temporary as char)))
                                          from drip_trigger_user_added_to_contact_list__contact_lists contactlis2_,
                                               crm_contact_list crmcontact3_
                                          where driptrigge1_.id = contactlis2_.trigger_id
                                            and contactlis2_.contact_list_id = crmcontact3_.id
                                            and driptrigge1_.type = 'USER_ADDED_TO_CONTACT_LIST'), 'f11',
                                         (select json_arrayagg(json_object('f0', cast(entitytag5_.id as char), 'f1',
                                                                           cast(entitytag5_.name as char), 'f2',
                                                                           cast(entitytag5_.scope as char), 'f3',
                                                                           cast(entitytag5_.shared as char)))
                                          from drip_trigger_contact_tag_assigned_selected_tags tags4_,
                                               tag entitytag5_
                                          where driptrigge1_.id = tags4_.trigger_id
                                            and tags4_.tag_id = entitytag5_.id
                                            and driptrigge1_.type = 'USER_TAG_ASSIGNED')))
        from drip_trigger driptrigge1_
                 left outer join drip_trigger_entity_user_tag_assigned_entity driptrigge1_1_
                                 on driptrigge1_.id = driptrigge1_1_.id
                 left outer join drip_trigger_user_added_to_contact_list driptrigge1_2_
                                 on driptrigge1_.id = driptrigge1_2_.id
        where driptrigge1_.drip_campaign = dripcampai0_.id)    as col_1_0_,
       dripcampai0_.created_at                                 as col_2_0_,
       dripcampai0_.created_by                                 as col_3_0_,
       dripcampai0_.deleted_at                                 as col_4_0_,
       dripcampai0_.enabled                                    as col_5_0_,
       dripcampai0_.last_modified_by                           as col_6_0_,
       dripcampai0_.last_sent_at                               as col_7_0_,
       dripcampai0_.name                                       as col_8_0_,
       dripcampai0_.owner_id                                   as col_9_0_,
       dripcampai0_.preferred_send_date                        as col_10_0_,
       dripcampai0_.preferred_send_time                        as col_11_0_,
       (select json_arrayagg(json_object('f0', cast(dripcampai6_.id as char), 'f1',
                                         cast(dripcampai6_.created_at as char), 'f2',
                                         cast(dripcampai6_.created_by as char), 'f3',
                                         cast(dripcampai6_.deleted_at as char), 'f4',
                                         cast(dripcampai6_.interval_seconds as char), 'f5',
                                         cast(dripcampai6_.last_modified_by as char), 'f6',
                                         cast(dripcampai6_.owner_id as char), 'f7',
                                         cast(dripcampai6_.reply_for as char), 'f8',
                                         cast(dripcampai6_.send_from as char), 'f9',
                                         cast(dripcampai6_.step_index as char), 'f10',
                                         cast(dripcampai6_.subject as char), 'f11',
                                         cast(dripcampai6_.template_id as char), 'f12', cast(dripcampai7_.body as char),
                                         'f13', cast(dripcampai7_.created_at as char), 'f14',
                                         cast(dripcampai7_.created_by as char), 'f15', cast(dripcampai7_.json as char),
                                         'f16', cast(dripcampai7_.last_modified_by as char), 'f17',
                                         cast(dripcampai7_.owner_id as char), 'f18',
                                         cast(dripcampai7_.updated_at as char), 'f19',
                                         cast(dripcampai6_.updated_at as char)))
        from drip_campaign_step dripcampai6_
                 inner join drip_campaign_template dripcampai7_ on dripcampai6_.template_id = dripcampai7_.id
        where dripcampai6_.drip_campaign_id = dripcampai0_.id) as col_12_0_,
       dripcampai0_.steps_count                                as col_13_0_,
       dripcampai0_.updated_at                                 as col_14_0_
from drip_campaign dripcampai0_
where (dripcampai0_.deleted_at is null)
  and (dripcampai0_.deleted_at is null)
  and dripcampai0_.id = 29

This SQL produces Json for trigger:

[
  {
    "f0": "2",
    "f1": "24",
    "f2": "2023-11-17 17:25:15.633000",
    "f3": "2062",
    "f4": "1",
    "f5": "2062",
    "f6": "2062",
    "f7": "1",
    "f8": "USER_TAG_ASSIGNED",
    "f9": "2023-11-17 18:02:03.137000",
    "f10": null,
    "f11": [
      {
        "f0": "96",
        "f1": "tag2",
        "f2": "USER",
        "f3": "0"
      }
    ]
  }
]

as you can see both f4(enabled) and f7(retrospection_enabled) are true, but object contains only false...

Expected behavior

boolean fields have valid value

Actual behavior

boolean fields always false

Steps to reproduce

already provided

Environment

blaze-persistence 1.6.8 spring-boot-starter-parent 2.7.17 Windows 11 Database Mysql 8

roma2341 commented 11 months ago

DripCampaignEntity:

@Table(name = "drip_campaign")
@Entity(name = "DripCampaign" )
@Audited
@Getter
@Setter
@Builder(toBuilder = true)
@AllArgsConstructor
@NoArgsConstructor
@Where(clause="deleted_at is null")
@NamedEntityGraph(name = DripCampaignEntity.DRIP_CAMPAIGN_ENTITY_GRAPH_ALL,
        attributeNodes = {
                @NamedAttributeNode(value = "steps", subgraph = "steps.template"),
                @NamedAttributeNode(value = "campaignSubscriptionTriggers"),
        },subgraphs = {
        @NamedSubgraph(name = "steps.template",
                attributeNodes =
                        {@NamedAttributeNode("template")})
        })
public class DripCampaignEntity extends AbstractAuditingResource<Long> {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id", nullable = false)
    private Long id;

    @Size(max = 1024)
    private String name;

    private Instant preferredSendDate;
    private Instant preferredSendTime;

    @Column(nullable = false, columnDefinition = "boolean default false")
    @Builder.Default
    private boolean enabled = false;

    @Column(updatable = false,insertable = false)
    private Instant lastSentAt;

    @Column(name="steps_count", nullable = false,  columnDefinition = "int default 0")
    private int stepsCount = 0;

    @OneToMany(mappedBy = "dripCampaign", fetch = FetchType.LAZY)
    @Where(clause="deleted_at is null")
    @OrderBy("stepIndex")
    @Builder.Default
    @Fetch(FetchMode.SUBSELECT)
    private List<DripCampaignStepEntity> steps = new ArrayList<>();

    @NotAudited
    @OneToMany(fetch = FetchType.LAZY,mappedBy = "dripCampaign",orphanRemoval = true,cascade={CascadeType.REMOVE})
    private Set<DripTriggerEntity> campaignSubscriptionTriggers = new HashSet<>();

    /*Don't use directly*/
    @NotAudited
    @OneToMany(mappedBy = "dripCampaign", fetch = FetchType.LAZY)
    @Builder.Default
    private Set<DripCampaignSubscriberEntity> subscribedUsers = new HashSet<>();

    @Column(updatable = false)
    private Instant deletedAt;

    public final static String DRIP_CAMPAIGN_ENTITY_GRAPH_ALL = "drip_campaign.all";

    @Override
    public final boolean equals(Object o) {
        if (this == o) return true;
        if (o == null) return false;
        Class<?> oEffectiveClass = o instanceof HibernateProxy ? ((HibernateProxy) o).getHibernateLazyInitializer().getPersistentClass() : o.getClass();
        Class<?> thisEffectiveClass = this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass() : this.getClass();
        if (thisEffectiveClass != oEffectiveClass) return false;
        DripCampaignEntity that = (DripCampaignEntity) o;
        return getId() != null && Objects.equals(getId(), that.getId());
    }

    @Override
    public final int hashCode() {
        return this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass().hashCode() : getClass().hashCode();
    }
}

DripTriggerEntity:

@Table(name = "drip_trigger")
@Entity(name="DripTrigger")
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name = "type",discriminatorType=DiscriminatorType.STRING)
@Getter
@Setter
public abstract class DripTriggerEntity extends AbstractAuditingResource<Long> {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id", nullable = false)
    protected Long id;
    /*
    * The variable determines whether the logic should be triggered for existing data (true)
    * or only for new ones (false).
    */
    @Column(nullable = false, columnDefinition = "boolean default false")
    protected boolean retrospectionEnabled = false;

    @Builder.Default
    @Column(nullable = false, columnDefinition = "boolean default false")
    protected boolean enabled = false;

    @Enumerated(EnumType.STRING)
    @Getter(AccessLevel.NONE)
    @Column(name="type", insertable = false, updatable = false)
    protected DripTriggerType type;

    @ManyToOne(fetch = FetchType.LAZY,optional = false)
    @JoinColumn(name = "drip_campaign", updatable = false)
    protected DripCampaignEntity dripCampaign;

    @Transient
    public DripTriggerType getTriggerType() {
        var discriminatorAnnotation = this.getClass().getAnnotation(DiscriminatorValue.class);
         if(discriminatorAnnotation == null) {
             throw new PublicCrmRuntimeException("Cannot get discriminator value of Base Entity class");
         }
         return DripTriggerType.valueOf(discriminatorAnnotation.value());
    }

    @Override
    public final boolean equals(Object o) {
        if (this == o) return true;
        if (o == null) return false;
        Class<?> oEffectiveClass = o instanceof HibernateProxy ? ((HibernateProxy) o).getHibernateLazyInitializer().getPersistentClass() : o.getClass();
        Class<?> thisEffectiveClass = this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass() : this.getClass();
        if (thisEffectiveClass != oEffectiveClass) return false;
        DripTriggerEntity that = (DripTriggerEntity) o;
        return getId() != null && Objects.equals(getId(), that.getId());
    }

    @Override
    public final int hashCode() {
        return this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass().hashCode() : getClass().hashCode();
    }
}
roma2341 commented 11 months ago

image Here is retreived object

roma2341 commented 11 months ago

image MultisetTupleTransformer received "1"

roma2341 commented 11 months ago

I think that problem is here: image BooleanBasicUserType.fromString("1") returns false

roma2341 commented 11 months ago

As i understand converter now supports only "true", "false" values, and doesn't support 1 or 0, but it takes 1 or 0 if we use multiset.

beikov commented 11 months ago

Hey there. The fix should be pretty simple, so if you want to take a stab at trying to provide a PR for this, I'd be very grateful. In the meantime, you should be able to register a custom BooleanBasicUserType with a custom conversion strategy via com.blazebit.persistence.view.spi.EntityViewConfiguration#registerBasicUserType.