Entity erstellen, die für API gedacht ist

internet

Top Contributor
Moin zusammen,

ich möchte eine API erstellen. Dabei möchte ich Objekte aus meiner Applikation ausliefern.
Zum Beispiel einen "Customer".

Nun ist es allerdings so, dass nicht alle Felder von meiner bestehenden "Customer" Entity ausgeliefert werden sollen.

Was ich nun also gerne bräuchte ist eine (ich hoffe der Name ist richtig): CustomerDTO - Klasse.
Nun ist aber die Frage, wie erstelle ich diese?
Gibt es dazu einen "Trick", indem ich Dinge aus der bestehenden Customer Entity kopieren lassen kann etc.?
Oder wie gehe ich hier am Besten heran? Ich möchte diese als JSON zurückgeben können.

Ebenfalls wie würde diese aussehen? Ein kurzer Beispiel - Code, inkl. 2-3 Properties würden mir helfen?
- Ggf. irgendwelche Annotationen.... ?

Vielen Dank
 
Das ist nichts anderes, als eine ganz schlichte Java-Klasse, aus nur den nötigen Feldern, sinnvollen Konstruktoren und Gettern.
Kopiere von der Entität würde ich dabei nichts – die Felder sind ja meist so


Die Felder/Getter brauchen u.U. Annotationen, dass hängt vom verwendeten Framework ab (mit den meisten gehts auch ohne, musst du einfach mal testen).


Falls du schon Java 16 nutzt: Records sind da das Mittel der Wahl 🙂
 
Wenn du nur JSON ausliefern willst, dann reicht:

Java:
class User {

    @JsonIgnore
    private String password;
   
    ...
}
Wenn du ein spezielles DTO verwenden musst, kannst du auch einen Copy-Ctor oder eine Variante davon verwenden: https://www.baeldung.com/java-copy-constructor. Deine IDE macht das sicher mit ein paar Klicks, ohne das du etwas schreiben musst.
 
Danke für die Antworten.
Nein, ich nutze bisher nicht Java 16. Als IDE nutze ich Eclipse.

Eine separate Klasse wäre mir lieber.

Ebenfalls möchte ich bspw. für das Datumformat das Format ISO 8601 nutzen.
Was muss hier noch gemacht werden?
Die Felder/Getter brauchen u.U. Annotationen, dass hängt vom verwendeten Framework ab (mit den meisten gehts auch ohne, musst du einfach mal testen).
Die Felder/ Getter in der DTO Klasse meinst du? Oder die von der Entity Klasse?
Welche Frameworks meinst du? Ich verwendete JPA / Hibernate für die Entity Klassen.
 
Ebenfalls möchte ich bspw. für das Datumformat das Format ISO 8601 nutzen.
Was muss hier noch gemacht werden?
Je nach verwendete Lib nichts oder eine passende Anbotation. Dürfte bei den meiste aber „out of the Box“ funktionieren.


Die Felder/ Getter in der DTO Klasse meinst du? Oder die von der Entity Klasse?
Bezieht sich alles auf die DTOs, die Entitäten sind dabei völlig egal 🙂


Welche Frameworks meinst du? Ich verwendete JPA / Hibernate für die Entity Klassen.
Das, was zum Serialisieren benutzt wird. Bei Spring zB Jackson.
 
Ok, das heißt nun also, wenn ich diese Entity habe:

Java:
@Entity
@Table(name = "customer", indexes = { @Index(columnList = "customer_id") })
public class Customer implements Serializable {
    
    @Id
    @GeneratedValue(generator = "customer_id_seq", strategy = AUTO)
    @SequenceGenerator(name = "customer_id_seq", sequenceName = "customer_customer_id_seq", allocationSize = 1)
    @Column(name = "customer_id", nullable = false)
    private Long id = NO_ID;

    // ID Hash (für URL)
    @NotNull
    @Column(name = "id_hash", nullable = false)
    private String idHash;

    // Kundennummer
    @NotNull
    @Size(max = Constants.SIZE_MAX_STRING)
    @Column(name = "customer_number", length = Constants.SIZE_MAX_STRING, nullable = false)
    private String customerNumber = null;
    
}

Erstelle ich die CustomerDTO wie folgt (die ID´s sollen nicht ausgeliefert werden)

Java:
public class CustomerDTO  {
    
    private String customerNumber = null;
    
}

Was ist mit dieser Lib: http://modelmapper.org/ Ist diese empfehlenswert?
 
Erstelle ich die CustomerDTO wie folgt (die ID´s sollen nicht ausgeliefert werden)
Ja, halt mit allen, Feldern, die du brauchst.

(Getter und Konstruktor hast du vermutlich nur der Kürze halber weggelassen?)


Was ist mit dieser Lib: http://modelmapper.org/ Ist diese empfehlenswert?
Ich würde eher zu Mapstruct tendieren, aber ja, generell können solche Libs sinnvoll sein. Ganz generell lässt sich das aber nicht eindeutige sagen.
 
Habe mir gerade mal Mapstruct angeschaut...

Verstehe ich das richtig, dass ich für jede Entity ein Mapping-Interface brauche, also:
Java:
@Mapper
public interface CustomerMapper {
    
    CustomerMapper INSTANCE = Mappers.getMapper(CustomerMapper.class);
    CustomerDto createDtoObject(Customer customer);
}

Diese rufe dann eben so auf:
Java:
Customer customer = new Customer();
CustomerDto customerDto = CustomerMapper.INSTANCE.createDtoObject(customer);

Folgende Fragen habe ich:
1) Muss ich tatsächlich für jede Klasse eine Mapper Interface anlegen? Oder kann ich das auch nur mit einer machen und das irgendwie generisch machen?

2) Wo genau meinst du, dass ich das mit dem Datumformat ISO 8601 einstellen muss?
 
1) Muss ich tatsächlich für jede Klasse eine Mapper Interface anlegen? Oder kann ich das auch nur mit einer machen und das irgendwie generisch machen?
Nein, für jedes benötigte Mapping ein Interface. Ist etwas mehr Aufwand, als mit anderen Libs, aber dafür arbeitet das zur Compile-Zeit und nicht erst zur Laufzeit mit Reflection.


2) Wo genau meinst du, dass ich das mit dem Datumformat ISO 8601 einstellen muss?
Wie gesagt: das kommt drauf an, welches Feamework/welche Lib du nutzt.
Teste einfach mal, ob es schon out of the Box funktioniert.
 
Ok, irgendwie bekomme ich es aber nicht zum Laufen und bekomme folgenden Fehler:
Caused by: java.lang.RuntimeException: java.lang.ClassNotFoundException: Cannot find implementation for com.webservice.dto.mapper.CustomerMapper

Den Code habe ich ja oben schon gepostet, bzw. hier das Mapper Interface:

[CODE lang="java" title="Mapper"]@Mapper
public interface CustomerMapper {

CustomerMapper INSTANCE = Mappers.getMapper(CustomerMapper.class);

CustomerDto createDtoObject(Customer customer);
}[/CODE]


In meiner pom.xml
Java:
<dependency>
            <groupId>org.mapstruct</groupId>
            <artifactId>mapstruct</artifactId>
            <version>1.4.2.Final</version>
        </dependency>

Muss ich sonst noch irgendwas beachten?
 
Zuletzt bearbeitet:
Java:
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.8.1</version>
            <configuration>
                <!-- nur bei Verwendung mit Spring -->
                <compilerArgs>
                    <arg>-Amapstruct.defaultComponentModel=spring</arg>
                </compilerArgs>
                <annotationProcessorPaths>
                    <path>
                        <groupId>org.mapstruct</groupId>
                        <artifactId>mapstruct-processor</artifactId>
                        <version>${org.mapstruct.version}</version>
                    </path>
                </annotationProcessorPaths>
            </configuration>
        </plugin>
    </plugins>
</build>
 
danke, und das geht dann so:

Java:
    public static void main(String[] args) {

        Customer customer = new Customer();
        CustomerDto customerDto = CustomerMapper.INSTANCE.createDtoObject(customer);

    }

Und dann in Eclipse
RunAs -> Java Application

Bekomme auch hier leider den o.g. Fehler
 
Ok, habe es hinbekommen, lag wohl an Maven...
Noch eine Frage:

Wie kann ich eine Liste schnellstmöglich in eine CustomerDto - Liste umwandeln?
Versuche gerade sowas:

Java:
@Stateless
@Path("/customer")
public class CustomerWebservice {

    @Inject
    private CustomerService customerService;

    /**
     * Alle Kunden bekommen
     *
     * @param id
     * @return
     */
    @GET
    @Path("/")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response findAllCustomers() {

        LOGGER.info("START findAllCustomers");

        try {

            List<Customer> customerList = customerService.findAllCustomer();

            ???
            List<CustomerDto> customerDtoList = CustomerMapper.INSTANCE.createDtoObject(customer);

            return Response.ok(customerDto, MediaType.APPLICATION_JSON)
                    .build();

        } catch (Exception e) {
            LOGGER.error(ExceptionUtils.getFullStackTrace(e));
            Response.serverError().build();
        }

        LOGGER.info("END findAllCustomers");
        return Response.ok().build();
    }

Ich könnte es so machen?
Java:
List<Customer> customerList = customerService.findAllCustomer();
List<CustomerDto> customerDtoList = customerList.stream().map(mapper::createDtoObject)
                    .collect(Collectors.toList());
 
Zuletzt bearbeitet:
Dem Mapper-Interaface einfach eine Methode zum Mappen einer Liste hinzufügen, Mapstruct erstellt dass dann passend.
 
Habe nun noch eine weitere Frage, insbesondere wenn es zu Referenzen von der Entität kommt.
Nehmen wir an, dass "Customer" eine "Adresse" hat und wir die Hauptadresse als Referenz speichern. (Kunde kann 1... n Adressen haben)

Nun möchte ein User einen Kunde via REST API erstellen.

1) Erstellen der Referenz
Java:
public class CustomerDto {

    private String customerNumber;
    private Address addressMain;
    
}
Wenn ich einen neuen Kunden via API erstellen will, muss ich ja ein Objekt von "Address" übermitteln (oder auch AddressDTO), oder? Demnach würde der Code oben passen?

2) Nicht erstellen der Referenz, aber Referenz hinzufügen
Jeder Kunde kann zu einer Preisgruppe hinzugefügt werden. Die Preisgruppe hat der User bereits in seinem Account erstellt.
Hierfür sieht er zum Beispiel die ID, die in der Datenbank für diese Preisgruppe erstellt wird, zB ID = 4711.

Java:
public class CustomerDto {
    private String customerNumber;
    private String priceGroupId;
}

a) Wenn der User nun einen Kunden via API erstellen möchte, gibt er dann die Preisgruppe mit, also
customer.setPriceGroupId = 4711.
b) Objekt wird an meinem API Endpoint entgegegen genommen
c) Preisgruppe (Hibernate) wird gesucht und das gefundene Objekt an Customer (also nicht das DTO Objekt) referenziert.

Ist das so richtig?
 
Nimm für's Anlegen von Dingen extra DTOs und missbrauch nicht die anderen, nur weil die zufällig ähnlich aussehen 🙂

Wenn ich einen neuen Kunden via API erstellen will, muss ich ja ein Objekt von "Address" übermitteln
Das kommt darauf an, ob Kunden beim Anlegen genau eine Adresse erhalten müssen, das kannst du dir selbst überlegen.

a) Wenn der User nun einen Kunden via API erstellen möchte, gibt er dann die Preisgruppe mit, also
customer.setPriceGroupId = 4711.
b) Objekt wird an meinem API Endpoint entgegegen genommen
c) Preisgruppe (Hibernate) wird gesucht und das gefundene Objekt an Customer (also nicht das DTO Objekt) referenziert.

Ist das so richtig?
Wenn du das so umsetzt, ist das richtig. Das steht dir auch wieder völlig frei, wie du das machst.
 
Versuche gerade noch ein Mapping zu machen:
Java:
@Mapper(componentModel = "cdi")
public interface CustomerMapper {

    CustomerMapper INSTANCE = Mappers.getMapper(CustomerMapper.class);   

    @Mapping(source = "salutation", target = "salutationId", qualifiedByName = "salutationDto")
    CustomerDto toDTO(Customer customer);


    @Named("salutationDto")
    static String map(Salutation salutation) {

        if (salutation == null) {
            return null;
        }

        return salutation.getIdHash();
    }
}

Bekomme aber in der IDE folgende Fehlermeldung bei @Mapping:
Can't map property "Salutation salutation" to "String salutationId". Consider to declare/implement a mapping method: "String map(Salutation value)".

Habe es so:
Java:
public class CustomerDto {
   private String salutationId;
}

und in der Entity:
Java:
public class Customer {
   private Salutation salutation;
}

Was mache ich falsch?
 
Ne, das wars nicht - habe das falsche @Inject verwendet (import javax.inject.Named)

Aber nun noch ein andere Frage:
Ich möchte nun die ID aus der Datenbank finden, die zur "salutationId" gehört. Dazu möchte ich den SalutationService injecten, der dann mir die ID aus der DB lädt...


Dazu schwebt mir dann sowas vor:

Java:
@Mapper(componentModel = "cdi")
public interface CustomerMapper {

CustomerMapper INSTANCE = Mappers.getMapper(CustomerMapper.class);

@Mapping(source = "salutation", target = "salutationId", qualifiedByName = "salutationToDto")
CustomerDto toDTO(Customer customer);

@Inject
public SalutationService salutationService;

@Named("salutationToEntity")
public static Salutation mapSalutationToEntity(String salutationId) {

    if (salutationId == null) {
       return null;
      }

Salutation salutation = salutationService.findSalutationById(salutationId);

return salutation;
}
}

Bekomme aber diese Meldung - die Frage ist, wie kann ich dort eine Datenbank - Abfrage machen? Oder kann ich das gar nicht?
1618846967122.png
 
Ne, das wars nicht - habe das falsche @Inject verwendet (import javax.inject.Named)
static hat da trotzdem nichts zu suchen 🙂


Bekomme aber diese Meldung - die Frage ist, wie kann ich dort eine Datenbank - Abfrage machen? Oder kann ich das gar nicht?
Wieder: kein (implizites) static (und final)nutzen 😉

Da du den Mapper alsI Interface angibst, kannst du keine Felder und damit auch keine Injection nutzen.

Es könnte klappen, wenn du statt dem Interface eine abstrakte Klasse nutzt, müsstest du mal testen.
 
ähm, so ganz verstehe ich das nicht... habe nun das hier:

Java:
public class SalutationMapperResolver {

    @Inject
    private SalutationServiceBean salutationServiceBean;

    @ObjectFactory
    public Salutation resolve(String salutionId, @TargetType Class<Salutation> type) {
        
        if(salutionId == null)
            return null;
        
        Salutation salutation = salutationServiceBean.findSalutationById(Long.valueOf(1));
        
        return salutation;
    }

}

Hier komme ich aber nicht weiter...
Java:
@Mapper(componentModel = "cdi", uses = { SalutationMapperResolver.class })
public interface CustomerMapper {

    CustomerMapper INSTANCE = Mappers.getMapper(CustomerMapper.class);
    
    @Mapping(source = "salutation", target = "salutationId", qualifiedByName = "salutationToDto")
    CustomerDto toDTO(Customer customer);

    @Mapping(source = "salutationId", target = "salutation", qualifiedByName = "salutationToEntity")
    Customer toEntity(CustomerDto customerDto);


    @Named("salutationToDto")
    public default String mapSalutationToDto(Salutation salutation) {

        if (salutation == null) {
            return null;
        }

        return salutation.getIdHash();
    }

    @Named("salutationToEntity")
    public default Salutation mapSalutationToEntity(String salutationId) {

        if (salutationId == null) {
            return null;
        }
        
        Salutation salutation = SalutationMapperResolver ??????

        return null;
    }
}
 
So wie ich das verstehe musst Du nur das interface definieren, die Implementierung wird von MapStruct generiert.
Java:
public Salutation mapSalutationToEntity(String salutationId);
 
danke...

Habe noch eine andere Frage bzgl. dem Erstellen / Inserten von DTO - Objekten:

Ich möchte gerne ein CustomerDto in die Datenbank erstellen, wie kann ich nun hier die Exception abfangen, wenn bpsw. der Vorname gefüllt sein muss, er es aber nicht ist (also @NotNull bei private String surname).

Also im Grunde genommen, wenn der Insert nicht in die DB funktioniert, möchte ich einfach einen Fehlercode zurückgeben.

Ich bekomme eine u.a. eine Exception von:
javax.validation.ConstraintViolationException: Validation failed for classes

Ich habe es mal versucht diese abzufangen, aber das scheint wohl nicht zu gehen:

Java:
catch(ConstraintViolationException e) {
            ErrorMessage errorMessage = new ErrorMessage("No valid data",
                    Status.INTERNAL_SERVER_ERROR.getStatusCode());
            return Response.status(Status.INTERNAL_SERVER_ERROR).entity(errorMessage).type(MediaType.APPLICATION_JSON)
                    .build();
        }
 
Ich möchte gerne ein CustomerDto in die Datenbank erstellen
Ein DTO nutzt man, um eine Trennung zwischen API und Persistence zu haben, und würde die eben nicht in der ganzen Applikation benutzten, sondern nur im API-Layer.


Ich bekomme eine u.a. eine Exception von:

Ich habe es mal versucht diese abzufangen, aber das scheint wohl nicht zu gehen:
Wenn diese Exception innerhalb des try-Blocks geworfen wird, würde der catch-Block diese fangen.

Wenn das nicht so ist, wird die Exception entweder woanders geworfen oder es wird eine andere Exception geworfen (welche wiederum diese Exception als Cause haben kann).
Beides solltest du als erstes Mal kontrollieren.
 
Ein DTO nutzt man, um eine Trennung zwischen API und Persistence zu haben, und würde die eben nicht in der ganzen Applikation benutzten, sondern nur im API-Layer.
Da habe ich mich falsch ausgedrückt.
Ich bekomme ein CustomerDTO, wandle das in "Customer" um, und speichere es dann in die Datenbank.....

Java:
public Response createCustomer(
            CustomerDto customerDto) {

        try {

            Customer customer = customerMapper.toEntity(customerDto);
            customerService.addCustomer(customer);

            return Response.ok(customerDto, MediaType.APPLICATION_JSON)
                    .build();

        }
        catch (ConstraintViolationException e) {
            ErrorMessage errorMessage = new ErrorMessage("Failed",
                    Status.INTERNAL_SERVER_ERROR.getStatusCode());
            return Response.status(Status.INTERNAL_SERVER_ERROR).entity(errorMessage).type(MediaType.APPLICATION_JSON)
                    .build();
        }
   
        catch (Exception e) {
            LOGGER.error(ExceptionUtils.getFullStackTrace(e));

            ErrorMessage errorMessage = new ErrorMessage(e.getMessage(),
                    Status.INTERNAL_SERVER_ERROR.getStatusCode());

            return Response.status(Status.INTERNAL_SERVER_ERROR).entity(errorMessage).type(MediaType.APPLICATION_JSON)
                    .build();
        }


Add Customer ist einfach gehalten:

Java:
@Stateless
public class CustomerServiceBean implements CustomerService {

@PersistenceContext
private EntityManager entityManager;

   public Customer addCustomer(Customer customer)
       entityManager.persist(customer);
  }
}

Ich würde nun erwarten, dass ich in die ConstraintViolationException reinkomme, aber das ist nicht der Fall.
 
verstehe es leider nicht. Es scheint, dass eine javax.transaction.RollbackException

Caused by: javax.transaction.RollbackException: ARJUNA016053: Could not commit transaction.

und dort innerhalb eine ConstraintViolationException geworfen wird. Aber selbst wenn ich beide abfange, wird die Exception geworfen....
 
Zuletzt bearbeitet:
"Caused by" heißt, das die javax.transaction.RollbackException in noch einer anderen Exception gewrappt ist.

Du kannst auch die DTOs mit entsprechenden Constraints annotieren, mit JaxRS werden diese dann direkt geprüft und eine Fehlermeldung zurückgegeben, ohne dass du was machen musst.
 
Danke für die Info.

Ich habe das mal versucht mit @Valid etc. Siehe bspw.: https://javaee.github.io/tutorial/jaxrs-advanced002.html

Aber das scheint immer noch nicht zu gehen und die Exception wird immer noch im Log von Wildfly angezeigt, obwohl ich das nicht möchte und einfach nur 404 oder einen anderen Status Code zurück geben möchte...


CustomerDto:
Java:
public class CustomerDto {

    @NotNull
    @Schema(description = "Date of the first contact", required=true)
    private LocalDateTime firstContactDate;
 
    // Getter + Setter
}

Meine Mapper Klasse / Funktion:
Java:
public class CustomerMapper {

/**
     * Convert to Entity
     */
    public Customer toEntity(CustomerDto customerDto) {

        if (customerDto == null) {
            return null;
        }

        Customer customer = new Customer();

        customer.setFirstContactDate(customerDto.getFirstContactDate());
        return customer;
    }
}

Meine Entity - Klasse:

Java:
@Entity
@Table(name = "customer", indexes = { @Index(columnList = "customer_id, id_hash, customer_number") })
public class Customer implements Serializable {
 
    @NotNull
    @Column(name = "first_contact_date", nullable = false)
    private LocalDateTime firstContactDate;
 
    // Getter + Setter
}

Hier mein Endpoint:
Java:
[COLOR=#777777][/COLOR]
@POST
    @Path("/createCustomer")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response createCustomer(@Valid CustomerDto customerDto) {

        LOGGER.info("START createCustomer");

        try {

            Customer customer = customerMapper.toEntity(customerDto);
            customerService.addCustomer(customer);

            return Response.ok(customerDto, MediaType.APPLICATION_JSON)
                    .build();

        }
        catch (ConstraintViolationException e) {
            ErrorMessage errorMessage = new ErrorMessage(e.getMessage(),
                    Status.INTERNAL_SERVER_ERROR.getStatusCode());

            return Response.status(Status.INTERNAL_SERVER_ERROR).entity(errorMessage).type(MediaType.APPLICATION_JSON)
                    .build();
        }

        catch (Exception e) {
            LOGGER.error(ExceptionUtils.getFullStackTrace(e));

            ErrorMessage errorMessage = new ErrorMessage(e.getMessage(),
                    Status.INTERNAL_SERVER_ERROR.getStatusCode());

            return Response.status(Status.INTERNAL_SERVER_ERROR).entity(errorMessage).type(MediaType.APPLICATION_JSON)
                    .build();
        }

    }

Zurück kommt die Constrant Meldung von der Entity:

Caused by: javax.validation.ConstraintViolationException: Validation failed for classes [myapp.model.Customer] during persist time for groups [javax.validation.groups.Default, ]
List of constraint violations:[
ConstraintViolationImpl{interpolatedMessage='darf nicht null sein', propertyPath=firstContactDate, rootBeanClass=class myapp.model.Customer, messageTemplate='{javax.validation.constraints.NotNull.message}'}

Zudem eben davor:
Caused by: javax.transaction.RollbackException: ARJUNA016053: Could not commit transaction.

Verstehe leider nicht, was hier schief läuft
 
Zuletzt bearbeitet:

Zurück
Oben