Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Inheritance with Spring and Hibernate #262

Open
drenda opened this issue May 13, 2019 · 7 comments

Comments

Projects
None yet
2 participants
@drenda
Copy link

commented May 13, 2019

I'm using Spring 2.1 and Hibernate in my project. I use Lombok project, so usually I don't have get and setter.
Every @entity model in my project implements a basic class defined like this:

// https://hibernate.atlassian.net/browse/HHH-12091
@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
@EntityListeners({AuditingEntityListener.class, AbstractEntityListener.class})
@MappedSuperclass
@Data
public abstract class AbstractEntity implements Persistable<Long>, Serializable {

     @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY/* , generator = "native" */)
     private Long id;

    @Column(name = "sid", unique = true, nullable = false, updatable = false, length = 36)
    private String sid;

    @CreatedBy
    @Column(updatable = false)
    private String createdBy;

    @CreatedDate
    @Column(nullable = false, updatable = false)
    private Instant createdDate;

    @LastModifiedDate
    private Instant lastModifiedDate;

    @LastModifiedBy
    private String lastModifiedBy;

    @Version
    private long version = 1;

    @Transient
    private String createdByName;

    @Transient
    private String lastModifiedByName;

    @PrePersist
    public void initializeUUID() {
        if (getSid() == null) {
            setSid(UUID.randomUUID().toString());
        }
    }


    @Override
    @RestResource(exported = false)
    @JsonIgnore
    @Transient
    public boolean isNew() {
        return null == getId();
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        // if (!super.equals(obj))
        // return false;
        // if (getClass() != obj.getClass())
        // return false;
        AbstractEntity other = (AbstractEntity) obj;
        if (getSid() == null || other.getSid() == null) {
            return other.getSid() == null;
        } else if (getSid() == null || other.getSid() == null || !getSid().equals(other.getSid())) {
            if (getId() == null) {
                return other.getId() == null;
            } else {
                return getId().equals(other.getId());
            }
        }

        return true;
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = super.hashCode();
        result = prime * result + ((getSid() == null) ? 0 : getSid().hashCode());
        return result;
        // int hashCode = 17;
        // hashCode += null == getId() ? 0 : getId().hashCode() * 31;
        // return hashCode;
    }
}

and a concrete class:

@Data
@EqualsAndHashCode(callSuper = true)
public class Printer extends AbstractEntity {

    @NotBlank
    @Column(nullable = false)
    private String name;

    @NotBlank
    @Column(nullable = false)
    private String remoteAddress;

    @NotNull
    @Enumerated(EnumType.STRING)
    @Column(nullable = false, length = 30)
    private PrinterModel model;

    //The zoneId name, i.e. Europe/Rome
    private String zoneId;

    @NotNull
    @Column(nullable = false, columnDefinition = "BIT DEFAULT 0")
    private boolean ssl = false;

    @Size(max = 30)
    private String serialNumber;

    @NotNull
    @OnDelete(action = OnDeleteAction.CASCADE)
    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = "store_id", updatable = false)
    private Store store;

    @Transient
    private String url;

    @JsonIgnore
    public boolean isValidTimeZone(String zoneId) {
        try {
            ZoneId.of(zoneId);
        } catch (Exception e) {
            return false;
        }
        return true;
    }
}

I followed the tutorial:

@RestController
@Log4j2
public class GraphQLSampleController {

    private final GraphQL graphQL;

    @Autowired
    public GraphQLSampleController(PrinterQuery printerQuery, StoreQuery storeQuery) {

        //Schema generated from query classes
        GraphQLSchema schema = new GraphQLSchemaGenerator()
                .withResolverBuilders(
                        //Resolve by annotations
                        new AnnotatedResolverBuilder(),
                        //Resolve public methods inside root package
                        new PublicResolverBuilder("my.root.package"))
                .withOperationsFromSingleton(printerQuery)
                .withOperationsFromSingleton(storeQuery)
                .withValueMapperFactory(new JacksonValueMapperFactory())
                .generate();
        graphQL = GraphQL.newGraphQL(schema).build();

    }

    @PostMapping(value = "/graphql", consumes = MediaType.APPLICATION_JSON_UTF8_VALUE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
    @ResponseBody
    public Map<String, Object> indexFromAnnotated(@RequestBody Map<String, String> request, HttpServletRequest raw) {
        ExecutionResult executionResult = graphQL
                .execute(ExecutionInput.newExecutionInput()
                .query(request.get("query"))
                .operationName(request.get("operationName"))
                .context(raw)
                .build());
        return executionResult.toSpecification();
    }
}
@Component
@Log4j2
public class PrinterQuery {

    @Autowired
    private PrinterRepository printerRepository;

    @GraphQLQuery(name = "findAllPrinters")
    public List<Printer> findAll() {
        log.debug(SecurityContextHolder.getContext().getAuthentication());
        return printerRepository.findAllJoinStore();
    }
}

When I run the query I don't see any of my superclass fields. Am I doing something wrong or is this not supported?

@kaqqao

This comment has been minimized.

Copy link
Member

commented May 13, 2019

Lombok doesn't work properly with SPQR 0.9.9. Only the next release will be compatible. But that probably has nothing to do with your problem.

If the super class is in a different package, it will not get mapped unless you specify base packages.
Make sure you properly added graphql.spqr.base-packages=com.some.package,com.another.package to you application.properties.

This gets asked very often, please search around a bit before posting.

@drenda

This comment has been minimized.

Copy link
Author

commented May 13, 2019

Thanks for your reply @kaqqao . I read about the base package in other posts.
I confirm that classes are in different sub-packages but the base packate is the same. I understood that new PublicResolverBuilder("my.root.package")) was what I was looking for to set the base package.

Is graphql.spqr.base-packages something different from new PublicResolverBuilder("my.root.package"))?

I tried your property though but I've still the same problem.

Is there a larger documentation then https://github.com/leangen/graphql-spqr/blob/master/README.md ? Thanks

@drenda

This comment has been minimized.

Copy link
Author

commented May 13, 2019

Additional info: I saw the problem happens only with queries, with mutation I see also superclass's properties.

In graphiql for my mutation I see:

saveContact(contact: ContactInput): Long

so the input it's not a Contact but a ContactInput where I see all properties.

Instead in all queries I see:

findAllPrinters: [Printer]

and Printer has only fields of Printer.java but not the ones of the superclass.

Not sure this is helpful to clarify my problem. Thanks

@drenda

This comment has been minimized.

Copy link
Author

commented May 13, 2019

Ok, finally I figured out. Like you pointed out Lombok doesn't work properly with SPQR 0.9.9.

In fact to have superclass's fields in subclasses I need the annotation:

@GraphQLInterface(name = "AbstractEntity", implementationAutoDiscovery = true)

Unfortunately SPQR doesn't see superclass methods:

Failed to instantiate [myproject.server.rest.controllers.graphql.GraphQLSampleController]: Constructor threw exception; nested exception is graphql.schema.validation.InvalidSchemaException: invalid schema:
object type 'Contact' does not implement interface 'AbstractEntity' because field 'createdBy' is missing
object type 'Contact' does not implement interface 'AbstractEntity' because field 'createdByName' is missing
object type 'Contact' does not implement interface 'AbstractEntity' because field 'createdDate' is missing
object type 'Contact' does not implement interface 'AbstractEntity' because field 'id' is missing
object type 'Contact' does not implement interface 'AbstractEntity' because field 'lastModifiedBy' is missing
object type 'Contact' does not implement interface 'AbstractEntity' because field 'lastModifiedByName' is missing
object type 'Contact' does not implement interface 'AbstractEntity' because field 'lastModifiedDate' is missing
object type 'Contact' does not implement interface 'AbstractEntity' because field 'new' is missing
object type 'Contact' does not implement interface 'AbstractEntity' because field 'sid' is missing
object type 'Contact' does not implement interface 'AbstractEntity' because field 'version' is missing

So I was forced to replicate those methods in subclasses too. In this way SPQR works fine.
I'm glad next release of SPQR is supposed to support properly Lombok.

@kaqqao

This comment has been minimized.

Copy link
Member

commented May 14, 2019

Thanks for your reply @kaqqao . I read about the base package in other posts.
I confirm that classes are in different sub-packages but the base packate is the same. I understood that new PublicResolverBuilder("my.root.package")) was what I was looking for to set the base package.

Is graphql.spqr.base-packages something different from new PublicResolverBuilder("my.root.package"))?

While it is for the same purpose, it will only apply to the that specific resolver builder, and different builders are used at different level, so it's won't work in all cases.
To cover all the cases, use generator.withBasePackages("my.root.package").

I tried your property though but I've still the same problem.

The property is in case you're using SPQR Spring Starter, has the same effect as generator.withBasePackages.

Is there a larger documentation then https://github.com/leangen/graphql-spqr/blob/master/README.md ? Thanks

Unfortunately, no... That's the main reason this project is still in 0.x versions.
There's some examples here but you porobably already know that. Apart from that, the tests are the best place to look as they exemplify most use-cases.

In fact to have superclass's fields in subclasses I need the annotation:

@GraphQLInterface(name = "AbstractEntity", implementationAutoDiscovery = true)

In case your exposed (resolver) methods only ever refer to the interfaces, and no concrete types are ever in the signature, the implementations will then never end up in the schema. So what implementationAutoDiscovery does is finds and registeres the implementations dynamically. Not sure if and how that relates to your issue.

@drenda

This comment has been minimized.

Copy link
Author

commented May 15, 2019

Thanks for your reply. Everything is clear. Coming back to the original topic, just to make sure I got it well, at the moment is normal that I don't see superclass's fields in the concrete subclass with SPQR 0.9.9 and Lombok, right?

So given this superclass:

@Data
public abstract class AbstractEntity implements Persistable<Long>, Serializable {

     @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY/* , generator = "native" */)
     private Long id;

    @Column(name = "sid", unique = true, nullable = false, updatable = false, length = 36)
    private String sid;

    @CreatedBy
    @Column(updatable = false)
    private String createdBy;

    @CreatedDate
    @Column(nullable = false, updatable = false)
    private Instant createdDate;

    @LastModifiedDate
    private Instant lastModifiedDate;

    @LastModifiedBy
    private String lastModifiedBy;

    @Version
    private long version = 1;

    @Transient
    private String createdByName;

    @Transient
    private String lastModifiedByName;

and this concrede subclass:

@Data
@EqualsAndHashCode(callSuper = true)
public class Printer extends AbstractEntity {

    @NotBlank
    @Column(nullable = false)
    private String name;

    @NotBlank
    @Column(nullable = false)
    private String remoteAddress;

    @NotNull
    @Enumerated(EnumType.STRING)
    @Column(nullable = false, length = 30)
    private PrinterModel model;

    //The zoneId name, i.e. Europe/Rome
    private String zoneId;

    @NotNull
    @Column(nullable = false, columnDefinition = "BIT DEFAULT 0")
    private boolean ssl = false;

    @Size(max = 30)
    private String serialNumber;

    @NotNull
    @OnDelete(action = OnDeleteAction.CASCADE)
    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = "store_id", updatable = false)
    private Store store;

    @Transient
    private String url;

In GraphQL schema fields like id, sid, createdBy, etc, are not visible.
The only workaround I found is to implements superclass getters for that fields in each concrete class. Do you know a better way with less boilerplate code? (I've really a lot of classes and fields. I choosed Lombok for that).

Thanks

@kaqqao

This comment has been minimized.

Copy link
Member

commented Jun 4, 2019

As for the visibility of the inherited fields, basePackages is the only relevant option. Lombok or not shouldn't really matter, as long as the getter are there - they should be exposed. Make sure you do call GraphQLSchemaGenerator#withBasePackages, and don't override ResolverBuilders unless you know what you're doing and have a good reason to change them.
Where Lombok comes into play is the annotations on private fields. With Lombok you'd only be able to annotate the field, and SPQR 0.9.9 and earlier would ignore them.
SPQR 0.10.0 does take field annotations into account, so it's easy to use with Lombok.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.