30

I know how to implement spring data repositories,

Create an interface like this :

public interface CountryRepository extends CrudRepository<Country, Long> {}

Now Country is an AbstractCatalog and I have (a lot) more catalogs in my project.
I'm wondering if I can make only one repository that would work for all the catalogs:

public interface AbstractCatalogRepository extends CrudRepository<AbstractCatalog, Long> {}

Now I don't see a problem while saving, but if I want to search an AbstractCatalog I'm already sure that I'll hit the wall because the repository will not know which sub-class it must choose.

AbstractCatalog.class

@MappedSuperclass
public abstract class AbstractCatalog extends PersistentEntity {

    /**
     * The Constant serialVersionUID.
     */
    private static final long serialVersionUID = 1L;
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Integer id;
    /**
     * The code.
     */
    @Column(unique = true, nullable = false, updatable = false)
    private String code;
    /**
     * The description.
     */
    @Column(nullable = false)
    private String description;
    /**
     * The in use.
     */
    @Column(name = "IN_USE", nullable = false, columnDefinition = "bit default 1")
    private Boolean inUse = Boolean.TRUE;

    // getters and setters
}

Country.class

@Entity
@Table(name = "tc_country")
@AttributeOverrides({
    @AttributeOverride(name = "id", column =
            @Column(name = "COUNTRY_SID")),
    @AttributeOverride(name = "code", column =
            @Column(name = "COUNTRY_CODE")),
    @AttributeOverride(name = "description", column =
            @Column(name = "COUNTRY_DESCRIPTION"))})
public class Country extends AbstractCatalog {

    public static final int MAX_CODE_LENGTH = 11;

    @Column(name = "GEONAMEID", nullable = true, unique = false)
    private Long geonameid;

    // getter and setter
}

Has anyone any idea, how I could use only ONE repository for all the implementations of AbstractCatalog class without having to create the same interface over and over again with minimal differences in name and other properties?

Conscript
  • 607
  • 6
  • 21
chillworld
  • 4,207
  • 3
  • 23
  • 50

3 Answers3

20

If you aren't using table inheritance on the database side (e.g. super class table with descriminator column), AFAIK, and based off reading the JPA tutorial, this can't be done (i.e. simply using @MappedSuperclass annotation for your abstract class)

Mapped superclasses cannot be queried and cannot be used in EntityManager or Query operations. You must use entity subclasses of the mapped superclass in EntityManager or Query operations. Mapped superclasses can't be targets of entity relationships

Note, the JPA repository abstraction uses an EntityManager under the hood. I did a simple test, and what you will get (in the case of Hibernate implementation) an "IllegalArgumentException : not an entity AbstractClass"

On the other hand, if you do use table inheritance, then you can use the abstract type. I know you said "with just the minimal change" (and I guess my short answer is I don't think it's possible - probably for the reasons you guessed), so I guess the rest of this answer is for other inquiring minds ;-)

An example of a table inheritance strategy would be something like this (disclaimer: this is not the correct visualization for erd inheritance, but MySQL Workbench doesn't support it, but what I have below forward engineered the model to MYSQL the way it needs to be)

enter image description here

Where CountryCatalog has a FK/PK reference to the AbstractCatalog table pk (id). The AbstractCatalog table has a descriminatorColumn that will be used to determine to which subtype the supertype occurrence is related.

In terms of how you would code that, it would look something like

@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name="descriminatorColumn")
@Table(name="AbstractCatalog")
public abstract class AbstractCatalog {
    @Id
    private long id;
    ...
}

@Entity
@Table(name = "CountryCatalog")
public class CountryCatalog extends AbstractCatalog {
    // id is inherited
    ...
}

public interface AbstractCatalogRepository 
                 extends JpaRepository<AbstractCatalog, Long> {

}

@Repository
public class CountryCatalogServiceImpl implements CountryCatalogService {

    @Autowired
    private AbstractCatalogRepository catalogRepository;

    @Override
    public List<CountryCatalog> findAll() {
        return (List<CountryCatalog>)(List<?>)catalogRepository.findAll();
    }

    @Override
    public CountryCatalog findOne(long id) {
        return (CountryCatalog)catalogRepository.findOne(id);
    }   
}

Basically, in conclusion, what you are trying to do won't work if you don't have table inheritance. The class type for the repository needs to be an entity. If your tables aren't set up this way for inheritance, it just comes down to whether or not you want to change the tables. It may be a bit much just to avoid multiple repositories though.

Some references I used are here and here

Note: Everything in this answer is tested against Hibernate provider

Paul Samsotha
  • 205,037
  • 37
  • 486
  • 720
  • I thank you for your answer, I just edited the question and provided the code of the Pojo's. The reason why I'm in this direction thinking => I have in gui a custom component what works for all catalogs. Now I'm searching the repo at the hand of [a mapper](http://codereview.stackexchange.com/q/56722/35389) and if they had all the same repo it would simplify the mapper. – chillworld Aug 11 '14 at 11:56
  • It's not possible using `@MappedSuperclass`. The entity need to be queried by the concrete classes, as I've mentioned from the link. The type you pass as an argument is a class that will be queried for. You can't query a mapped super class, as it's not an entity. You can't just add an `@Entity` annotation either. That's not allowed. You _can_ do it if you have table inheritence. You need to decide if it's worth it or if you are allowed to change the tables. Did you try and run your code? If so, what happened when you did? – Paul Samsotha Aug 11 '14 at 12:04
  • see comment under @mavarazy answer. Worth the changes, not now cause project has a strict time limit and I can't decide this. (so there will be meetings,... all time I could use to create code) – chillworld Aug 11 '14 at 12:05
  • Yes, and your result supports my findings also. – Paul Samsotha Aug 11 '14 at 12:06
  • An aside, the post that that answer linked to does _not_ use `@MappedSuperclass`. The accepted answer to the OP the get rid of that annotation. – Paul Samsotha Aug 11 '14 at 12:08
  • new project and did this set up. Saving isn't possible in the abstract repo, you need to go to the specific repo for saving. BUT abstract repo can query like `findByAbstractField(AbstractField field)` and you get a list of `AbstractClass` who have all there correct implementations (you can use instanceof SubClass). Thx again. – chillworld Jul 29 '15 at 05:55
  • @chillworld you should post a solution for others to see. The more I thought about this after I posted it, the more I could see flaws and limitations in the implementation. I would be interested in seeing what you came up with. Also for others to see. You may even want to take the check mark. I personally don't think this is a complete solution on my part. – Paul Samsotha Jul 29 '15 at 05:58
  • 1
    added mine results of this project as answer, hope it's useful for some people. – chillworld Jul 29 '15 at 09:36
13

Oke, new project and I'm following this set up a little bit.
The problem was : We want to add attachments, but an attachment can be uploading a file, a link or a mail.

Pojo classes :

Attachment.java :

@Entity
@Table(name = "T_ATTACHMENT")
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name = "DISCRIMINATOR", discriminatorType = DiscriminatorType.STRING)
public abstract class Attachment {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "ATTACHMENT_SID")
    private Long id;

    @ManyToOne
    @JoinColumn(name = "TASK_SID", referencedColumnName = "TASK_SID", nullable = false, unique = false, insertable = true, updatable = true)
    private Task task;

    @ManyToOne
    @JoinColumn(name = "USER_SID", referencedColumnName = "USER_SID", nullable = false, unique = false, insertable = true, updatable = true)
    private User user;

    public Task getTask() {
        return task;
    }

    public void setTask(Task task) {
        this.task = task;
    }

    public User getUser() {
        return user;
    }

    public void setUser(User user) {
        this.user = user;
    }
}

FileAttachment.java :

@Entity
@Table(name = "T_FILE_ATTACHMENT")
@DiscriminatorValue("FILE")
public class FileAttachment extends Attachment {

    @Column(name = "NAME", nullable = false, unique = false)
    private String fileName;

    @Lob
    @Basic
    @Column(name = "FILE", nullable = false, unique = false)
    private byte[] file;

    public String getFileName() {
        return fileName;
    }

    public void setFileName(String fileName) {
        this.fileName = fileName;
    }

    public byte[] getFile() {
        return file;
    }

    public void setFile(byte[] file) {
        this.file = file;
    }
}

MailAttachment.java :

@Entity
@Table(name = "T_MAIL_ATTACHMENT")
@DiscriminatorValue("MAIL")
public class MailAttachment extends Attachment {

    @Column(name = "RECIPIENT", nullable = false, unique = false)
    private String to;
    @Column(name = "CC", nullable = true, unique = false)
    private String cc;
    @Column(name = "BCC", nullable = true, unique = false)
    private String bcc;
    @Column(name = "TITLE", nullable = true, unique = false)
    private String title;
    @Column(name = "MESSAGE", nullable = true, unique = false)
    private String message;

    public String getTo() {
        return to;
    }

    public void setTo(String to) {
        this.to = to;
    }

    public String getCc() {
        return cc;
    }

    public void setCc(String cc) {
        this.cc = cc;
    }

    public String getBcc() {
        return bcc;
    }

    public void setBcc(String bcc) {
        this.bcc = bcc;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}

LinkAttachment.java :

@Entity
@Table(name = "T_LINK_ATTACHMENT")
@DiscriminatorValue("LINK")
public class LinkAttachment extends Attachment {

    @Column(name = "DESCRIPTION", nullable = true, unique = false)
    private String description;

    @Column(name = "LINK", nullable = false, unique = false)
    private String link;

    public String getDescription() {
        return description == null ? getLink() : description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public String getLink() {
        return link;
    }

    public void setLink(String link) {
        this.link = link;
    }
}

Spring data repo's :

AttachmentRepository.java:

public interface AttachmentRepository extends CustomRepository<Attachment, Long> {    
    List<Attachment> findByTask(Task task);
}

CustomRepository.java :

public interface CustomRepository<E, PK extends Serializable> extends
                PagingAndSortingRepository<E, PK>,
                JpaSpecificationExecutor<E>, 
                QueryDslPredicateExecutor<E> {
    @Override
    List<E> findAll();
}

And at last the service :

@Service
public class AttachmentServiceImpl implements AttachmentService {

    @Inject
    private AttachmentRepository attachmentRepository;

    @Override
    public List<Attachment> findByTask(Task task) {
        return attachmentRepository.findByTask(task);
    }

    @Override
    @Transactional
    public Attachment save(Attachment attachment) {
        return attachmentRepository.save(attachment);
    }
}

This results in :

I can save to the abstract repo with any implementation I created, JPA will do it correct.

If I call findByTask(Task task) I get a List<Attachment> of all the subclasses, and they have the correct subclass in the back.
This means, you can make a renderer who do instanceof and you can customize your rendering for each subclass.

Downside is, you still need to create custom specific repository's, but only when you want to query on a specific property what is in the subclass or when you only want 1 specific implementation in stead of all implementations.

chillworld
  • 4,207
  • 3
  • 23
  • 50
  • 1
    discriminatorType = DiscriminatorType.STRING - this is not required... STRING is already a default value for this field. – Daniel Hajduk May 16 '17 at 12:38
1

What DB are you using?

If it's JPA, take a look at Can I use a generic Repository for all children of a MappedSuperClass with Spring Data JPA?

If it's Mongo you need to properly tune Jackson polymorphism configuration http://wiki.fasterxml.com/JacksonPolymorphicDeserialization

So this is possible.

Community
  • 1
  • 1
mavarazy
  • 7,562
  • 1
  • 34
  • 60
  • Case with the `@Inheritance` => No identifier specified for entity : Country.
    Case with the `@MappedSuperClass` => java.lang.IllegalArgumentException: Could not create query metamodel for method public abstract be.model.AbstractCatalog
    – chillworld Aug 11 '14 at 11:41
  • I think the problem in '@Inheritance' is with '@AttributeOverride', try to explicitly declare '@Id' column in Country – mavarazy Aug 11 '14 at 12:14
  • I'm a little lost, how can I add @Id in country when it's in the `AbstractCatalog` or do I have to change the getters/setters to abstract methods and move the variable to the implementation? – chillworld Aug 11 '14 at 14:00