Java >> Java tutorial >  >> Tag >> Spring

Håndtering af domæneobjekter i Spring MVC

Jeg blev for nylig overrasket over, hvordan en kodebase havde offentlige standardkonstruktører (dvs. nul-argument-konstruktører) i alle deres domæneentiteter og havde gettere og sættere for alle markerne. Efterhånden som jeg gravede dybere, fandt jeg ud af, at grunden til, at domæneentiteterne er, som de er, i høj grad skyldes, at teamet mener, det var påkrævet af web/MVC-rammerne. Og jeg tænkte, at det ville være en god mulighed for at opklare nogle misforståelser.

Specifikt vil vi se på følgende tilfælde:

  1. Ingen indstiller for genereret ID-felt (dvs. det genererede ID-felt har en getter, men ingen setter)
  2. Ingen standardkonstruktør (f.eks. ingen offentlig nulargumentkonstruktør)
  3. Domæneenhed med underordnede enheder (f.eks. er underordnede enheder ikke eksponeret som en modificerbar liste)

Bindende webanmodningsparametre

Først nogle detaljer og lidt baggrund. Lad os basere dette på en specifik web/MVC-ramme – Spring MVC. Når du bruger Spring MVC, binder dens databinding anmodningsparametre efter navn. Lad os bruge et eksempel.

@Controller
@RequestMapping("/accounts")
... class ... {
    ...
    @PostMapping
    public ... save(@ModelAttribute Account account, ...) {...}
    ...
}

Givet ovenstående controller, der er knyttet til "/accounts", hvor kan en Account forekomst kommer fra?

Baseret på dokumentation vil Spring MVC få en instans ved hjælp af følgende muligheder:

  • Fra modellen, hvis den allerede er tilføjet via Model (som via @ModelAttribute metode i den samme controller).
  • Fra HTTP-sessionen via @SessionAttributes .
  • Fra en URI-stivariabel passeret gennem en Converter .
  • Fra påkaldelsen af ​​en standardkonstruktør.
  • (Kun for Kotlin) Fra påkaldelsen af ​​en "primær konstruktør" med argumenter, der matcher Servlet-anmodningsparametre; argumentnavne bestemmes via JavaBeans @ConstructorProperties eller via runtime-beholdte parameternavne i bytekoden.

Forudsat en Account objekt er ikke tilføjet i sessionen, og at der ingen @ModelAttribute er metode , Spring MVC ender med at instansiere en ved at bruge dens standardkonstruktør og binde webanmodningsparametre efter navn . For eksempel indeholder anmodningen "id" og "navn" parametre. Spring MVC vil forsøge at binde dem til "id" og "name" bønneegenskaber ved at påberåbe sig henholdsvis "setId" og "setName" metoder. Dette følger JavaBean-konventionerne.

Ingen Setter-metode for genereret ID-felt

Lad os starte med noget simpelt. Lad os sige, at vi har en Account domæneenhed. Den har et ID-felt, der er genereret af det persistente lager, og giver kun en getter-metode (men ingen setter-metode).

@Entity
... class Account {
    @Id @GeneratedValue(...) private Long id;
    ...
    public Account() { ... }
    public Long getId() { return id; }
    // but no setId() method
}

Så hvordan kan vi få Spring MVC til at binde anmodningsparametre til en Account domæne enhed? Er vi tvunget til at have en public setter-metode for et felt, der er genereret og skrivebeskyttet?

I vores HTML-formular vil vi ikke placere "id" som en anmodningsparameter. Vi vil placere den som en stivariabel i stedet for.

Vi bruger en @ModelAttribute metode. Det kaldes forud for anmodningshåndteringsmetoden. Og det understøtter stort set de samme parametre som en almindelig anmodningshåndteringsmetode. I vores tilfælde bruger vi det til at hente en Account domæneenhed med den givne unikke identifikator, og brug den til yderligere binding. Vores controller ville se nogenlunde sådan ud.

@Controller
@RequestMapping("/accounts")
... class ... {
    ...
    @ModelAttribute
    public Account populateModel(
            HttpMethod httpMethod,
            @PathVariable(required=false) Long id) {
        if (id != null) {
            return accountRepository.findById(id).orElseThrow(...);
        }
        if (httpMethod == HttpMethod.POST) {
            return new Account();
        }
        return null;
    }

    @PutMapping("/{id}")
    public ... update(...,
            @ModelAttribute @Valid Account account, ...) {
        ...
        accountRepository.save(account);
        return ...;
    }

    @PostMapping
    public ... save(@ModelAttribute @Valid Account account, ...) {
        ...
        accountRepository.save(account);
        return ...;
    }
    ...
}

Når du opdaterer en eksisterende konto, vil anmodningen være en PUT til "/accounts/{id}" URI. I dette tilfælde skal vores controller hente domæneenheden med den givne unikke identifikator og give det samme domæneobjekt til Spring MVC for yderligere binding, hvis nogen. Feltet "id" behøver ikke en indstillingsmetode.

Når du tilføjer eller gemmer en ny konto, vil anmodningen være en POST til "/konti". I dette tilfælde skal vores controller oprette en ny domæneenhed med nogle anmodningsparametre, og giv det samme domæneobjekt til Spring MVC for yderligere binding, hvis nogen. For nye domæneenheder efterlades "id"-feltet null . Den underliggende persistensinfrastruktur vil generere en værdi ved lagring. Stadig vil "id"-feltet ikke have brug for en indstillingsmetode.

I begge tilfælde er @ModelAttribute metode populateModel kaldes forud til den tilknyttede anmodningshåndteringsmetode. På grund af dette var vi nødt til at bruge parametre i populateModel for at afgøre, hvilket tilfælde det bruges i.

Ingen standardkonstruktør i domæneobjekt

Lad os sige, at vores Account domæneentiteten leverer ikke en standardkonstruktør (dvs. ingen nul-argument-konstruktør).

... class Account {
    public Account(String name) {...}
    ...
    // no public default constructor
    // (i.e. no public zero-arguments constructor)
}

Så hvordan kan vi få Spring MVC til at binde anmodningsparametre til en Account domæne enhed? Det giver ikke en standardkonstruktør.

Vi kan bruge en @ModelAttribute metode. I dette tilfælde ønsker vi at oprette en Account domæneenhed med anmodningsparametre, og brug den til yderligere binding. Vores controller ville se sådan ud.

@Controller
@RequestMapping("/accounts")
... class ... {
    ...
    @ModelAttribute
    public Account populateModel(
            HttpMethod httpMethod,
            @PathVariable(required=false) Long id,
            @RequestParam(required=false) String name) {
        if (id != null) {
            return accountRepository.findById(id).orElseThrow(...);
        }
        if (httpMethod == HttpMethod.POST) {
            return new Account(name);
        }
        return null;
    }

    @PutMapping("/{id}")
    public ... update(...,
            @ModelAttribute @Valid Account account, ...) {
        ...
        accountRepository.save(account);
        return ...;
    }

    @PostMapping
    public ... save(@ModelAttribute @Valid Account account, ...) {
        ...
        accountRepository.save(account);
        return ...;
    }
    ...
}

Domæneenhed med underordnede enheder

Lad os nu se på en domæneenhed, der har underordnede enheder. Noget som dette.

... class Order {
    private Map<..., OrderItem> items;
    public Order() {...}
    public void addItem(int quantity, ...) {...}
    ...
    public Collection<CartItem> getItems() {
        return Collections.unmodifiableCollection(items.values());
    }
}

... class OrderItem {
    private int quantity;
    // no public default constructor
    ...
}

Bemærk, at varerne i en ordre ikke er eksponeret som en modificerbar liste. Spring MVC understøtter indekserede egenskaber og binder dem til en matrix, liste eller anden naturligt ordnet samling. Men i dette tilfælde er getItems metode returnerer en samling, der ikke kan ændres. Dette betyder, at en undtagelse vil blive kastet, når et objekt forsøger at tilføje/fjerne elementer til/fra det. Så hvordan kan vi få Spring MVC til at binde anmodningsparametre til en Order domæne enhed? Er vi tvunget til at afsløre bestillingsvarerne som en foranderlig liste?

Ikke rigtig. Vi skal afholde os fra at udvande domænemodellen med præsentationslagsproblemer (som Spring MVC). I stedet gør vi præsentationslaget til en klient af domænemodellen. For at håndtere denne sag opretter vi en anden type, der overholder Spring MVC, og holder vores domæneenheder agnostiske over for præsentationslaget.

... class OrderForm {
    public static OrderForm fromDomainEntity(Order order) {...}
    ...
    // public default constructor
    // (i.e. public zero-arguments constructor)
    private List<OrderFormItem> items;
    public List<OrderFormItem> getItems() { return items; }
    public void setItems(List<OrderFormItem> items) { this.items = items; }
    public Order toDomainEntity() {...}
}

... class OrderFormItem {
    ...
    private int quantity;
    // public default constructor
    // (i.e. public zero-arguments constructor)
    // public getters and setters
}

Bemærk, at det er helt i orden at oprette en præsentationslagstype, der kender til domænetiteten. Men det er ikke i orden at gøre domæneenheden opmærksom på præsentationslagsobjekter. Mere specifikt, præsentationslag OrderForm kender til Order domæneenhed. Men Order kender ikke til præsentationslag OrderForm .

Sådan kommer vores controller til at se ud.

@Controller
@RequestMapping("/orders")
... class ... {
    ...
    @ModelAttribute
    public OrderForm populateModel(
            HttpMethod httpMethod,
            @PathVariable(required=false) Long id,
            @RequestParam(required=false) String name) {
        if (id != null) {
            return OrderForm.fromDomainEntity(
                orderRepository.findById(id).orElseThrow(...));
        }
        if (httpMethod == HttpMethod.POST) {
            return new OrderForm(); // new Order()
        }
        return null;
    }

    @PutMapping("/{id}")
    public ... update(...,
            @ModelAttribute @Valid OrderForm orderForm, ...) {
        ...
        orderRepository.save(orderForm.toDomainEntity());
        return ...;
    }

    @PostMapping
    public ... save(@ModelAttribute @Valid OrderForm orderForm, ...) {
        ...
        orderRepository.save(orderForm.toDomainEntity());
        return ...;
    }
    ...
}

Afsluttende tanker

Som jeg har nævnt i tidligere indlæg, er det i orden at få dine domæneobjekter til at ligne en JavaBean med offentlige standard nul-argument-konstruktører, gettere og -sættere. Men hvis domænelogikken begynder at blive kompliceret og kræver, at nogle domæneobjekter mister sin JavaBean-hed (f.eks. ingen offentlig nul-argument-konstruktør, ikke flere sættere), skal du ikke bekymre dig. Definer nye JavaBean-typer for at tilfredsstille præsentationsrelaterede bekymringer. Udvand ikke domænelogikken.

Det er alt for nu. Jeg håber det hjælper.

Endnu en gang tak til Juno for at hjælpe mig med prøverne. De relevante stykker kode kan findes på GitHub.


Java tag