Een eenvoudige e-commerce-implementatie met Spring

1. Overzicht van onze e-commercetoepassing

In deze tutorial implementeren we een eenvoudige e-commercetoepassing. We ontwikkelen een API met Spring Boot en een clienttoepassing die de API gebruikt met Angular.

In principe kan de gebruiker producten van een productlijst toevoegen aan / verwijderen uit een winkelwagentje en een bestelling plaatsen.

2. Backend-gedeelte

Om de API te ontwikkelen, gebruiken we de nieuwste versie van Spring Boot. We gebruiken ook de JPA- en H2-database voor de persistentie-kant van de dingen.

Voor meer informatie over Spring Boot,je zou onze Spring Boot-serie artikelen kunnen bekijken en als je wilt Bekijk een andere serie om vertrouwd te raken met het bouwen van een REST API.

2.1. Afhankelijkheden van Maven

Laten we ons project voorbereiden en de vereiste afhankelijkheden importeren in ons pom.xml.

We hebben een aantal belangrijke Spring Boot-afhankelijkheden nodig:

 org.springframework.boot spring-boot-starter-data-jpa 2.2.2.RELEASE org.springframework.boot spring-boot-starter-web 2.2.2.RELEASE 

Vervolgens de H2-database:

 com.h2database h2 1.4.197 runtime 

En tot slot - de Jackson-bibliotheek:

 com.fasterxml.jackson.datatype jackson-datatype-jsr310 2.9.6 

We hebben Spring Initializr gebruikt om het project snel op te zetten met de benodigde afhankelijkheden.

2.2. De database opzetten

Hoewel we de in-memory H2-database uit de doos kunnen gebruiken met Spring Boot, zullen we nog enkele aanpassingen maken voordat we beginnen met het ontwikkelen van onze API.

Goed schakel H2-console in in onze application.properties het dossier zodat we de staat van onze database kunnen controleren en kijken of alles verloopt zoals we hadden verwacht.

Het kan ook handig zijn om SQL-query's op de console te loggen tijdens het ontwikkelen van:

spring.datasource.name = ecommercedb spring.jpa.show-sql = true # H2 instellingen spring.h2.console.enabled = true spring.h2.console.path = / h2-console

Nadat we deze instellingen hebben toegevoegd, hebben we toegang tot de database op // localhost: 8080 / h2-console gebruik makend van jdbc: h2: mem: ecommercedb als JDBC-URL en gebruiker sa zonder wachtwoord.

2.3. De projectstructuur

Het project zal worden georganiseerd in verschillende standaardpakketten, met de Angular-applicatie in de frontend-map:

├───pom.xml ├───src ├───main │ ├───frontend │ ├───java │ │ └───com │ │ └───baeldung │ │ └────ecommerce │ │ │ EcommerceApplication.java │ │ ├───controller │ │ ├───dto │ │ ├───uitzondering │ │ ├───model │ │ ├───opslag │ │ └───service │ │ │ └───resources │ │ application.properties │ ├───static │ └───templates └───test └───java └───com └───baeldung └───ecommerce E-commerceApplicationIntegrationTest. Java

We moeten opmerken dat alle interfaces in het repository-pakket eenvoudig zijn en de CrudRepository van Spring Data uitbreiden, dus we laten ze hier achterwege.

2.4. Afhandeling van uitzonderingen

We hebben een exception handler nodig voor onze API om eventuele uitzonderingen goed te kunnen afhandelen.

U kunt meer informatie over het onderwerp vinden in onze artikelen over foutafhandeling voor REST met Spring en aangepaste afhandeling van foutmeldingen voor REST API-artikelen.

Hier houden we de focus op ConstraintViolationException en onze gewoonte ResourceNotFoundException:

@RestControllerAdvice openbare klasse ApiExceptionHandler {@SuppressWarnings ("rawtypes") @ExceptionHandler (ConstraintViolationException.class) openbare ResponseEntity-handle (ConstraintViolationException e) {ErrorResponse-fouten = nieuwe ErrorResponse (); voor (ConstraintViolation-schending: e.getConstraintViolations ()) {ErrorItem error = new ErrorItem (); error.setCode (schending.getMessageTemplate ()); error.setMessage (schending.getMessage ()); errors.addError (fout); } retourneer nieuwe ResponseEntity (fouten, HttpStatus.BAD_REQUEST); } @SuppressWarnings ("rawtypes") @ExceptionHandler (ResourceNotFoundException.class) openbare ResponseEntity-handle (ResourceNotFoundException e) {ErrorItem error = new ErrorItem (); error.setMessage (e.getMessage ()); retourneer nieuwe ResponseEntity (fout, HttpStatus.NOT_FOUND); }}

2.5. Producten

Als je meer kennis nodig hebt over persistentie in het voorjaar, zijn er veel nuttige artikelen in de serie Spring Persistence.

Onze applicatie ondersteunt alleen producten uit de database lezen, dus we moeten er eerst wat toevoegen.

Laten we een eenvoudig maken Product klasse:

@Entity public class Product {@Id @GeneratedValue (strategy = GenerationType.IDENTITY) privé Lange id; @NotNull (message = "Productnaam is vereist.") @Basic (optioneel = false) private String-naam; privé Dubbele prijs; privé String pictureUrl; // alle argumenten contructor // standaard getters en setters}

Hoewel de gebruiker niet de mogelijkheid heeft om producten toe te voegen via de applicatie, ondersteunen we het opslaan van een product in de database om de productlijst vooraf in te vullen.

Een eenvoudige service is voldoende voor onze behoeften:

@Service @Transactional public class ProductServiceImpl implementeert ProductService {// productRepository constructor injectie @Override public Iterable getAllProducts () {return productRepository.findAll (); } @Override public Product getProduct (lange id) {retourneer productRepository .findById (id) .orElseThrow (() -> nieuwe ResourceNotFoundException ("Product niet gevonden")); } @Override public Product save (Productproduct) {return productRepository.save (product); }}

Een eenvoudige controller behandelt verzoeken om de lijst met producten op te halen:

@RestController @RequestMapping ("/ api / products") public class ProductController {// productService constructor injection @GetMapping (value = {"", "/"}) public @NotNull Iterable getProducts () {retourneer productService.getAllProducts (); }}

Het enige dat we nu nodig hebben om de productlijst aan de gebruiker te tonen, is om enkele producten daadwerkelijk in de database te plaatsen. Daarom maken we gebruik van CommandLineRunner klasse om een Boon in onze hoofdtoepassingsklasse.

Op deze manier voegen we producten in de database in tijdens het opstarten van de applicatie:

@Bean CommandLineRunner runner (ProductService productService) {return args -> {productService.save (...); // meer producten }

Als we nu onze applicatie starten, kunnen we de productlijst opvragen via // localhost: 8080 / api / products. Ook als we naar // localhost: 8080 / h2-console en log in, we zullen zien dat er een tabel is met de naam PRODUCT met de producten die we zojuist hebben toegevoegd.

2.6. Bestellingen

Aan de API-kant moeten we POST-verzoeken inschakelen om de bestellingen op te slaan die de eindgebruiker zal maken.

Laten we eerst het model maken:

@Entity @Table (naam = "orders") openbare klasse Order {@Id @GeneratedValue (strategie = GenerationType.IDENTITY) privé Lange id; @JsonFormat (patroon = "dd / MM / jjjj") privé LocalDate dateCreated; privé String-status; @JsonManagedReference @OneToMany (mappedBy = "pk.order") @ Geldige privélijst orderProducts = nieuwe ArrayList (); @Transient openbare Double getTotalOrderPrice () {dubbele som = 0D; Lijst orderProducts = getOrderProducts (); voor (OrderProduct op: orderProducts) {sum + = op.getTotalPrice (); } retourbedrag; } @Transient openbare int getNumberOfProducts () {retourneer this.orderProducts.size (); } // standaard getters en setters}

We moeten hier een paar dingen opmerken. Zeker een van de meest opmerkelijke dingen is om vergeet niet om de standaardnaam van onze tafel te wijzigen. Omdat we de klas een naam hebben gegeven Bestellen, standaard de tabel met de naam BESTELLEN moet worden gemaakt. Maar omdat dat een gereserveerd SQL-woord is, hebben we toegevoegd @Table (name = "bestellingen") om conflicten te vermijden.

Verder hebben we er twee @Transient methoden die een totaalbedrag voor die bestelling en het aantal producten daarin retourneren. Beide vertegenwoordigen berekende gegevens, dus het is niet nodig om deze in de database op te slaan.

Eindelijk hebben we een @Een te veel relatie die de details van de bestelling vertegenwoordigt. Daarvoor hebben we een andere entiteitsklasse nodig:

@Entity openbare klasse OrderProduct {@EmbeddedId @JsonIgnore privé OrderProductPK pk; @Column (nullable = false) privé Geheel getal hoeveelheid; // default constructor public OrderProduct (Order order, Product product, Integer hoeveelheid) {pk = new OrderProductPK (); pk.setOrder (bestelling); pk.setProduct (product); this.quantity = hoeveelheid; } @Transient openbaar product getProduct () {retourneer this.pk.getProduct (); } @Transient publiek Double getTotalPrice () {retourneer getProduct (). GetPrice () * getQuantity (); } // standaard getters en setters // hashcode () en equals () methoden}

We hebben een samengestelde primaire sleutelhier:

@Embeddable openbare klasse OrderProductPK implementeert Serializable {@JsonBackReference @ManyToOne (optioneel = false, fetch = FetchType.LAZY) @JoinColumn (naam = "order_id") privé Orderorder; @ManyToOne (optioneel = false, fetch = FetchType.LAZY) @JoinColumn (name = "product_id") privéproductproduct; // standaard getters en setters // hashcode () en equals () methoden}

Die lessen zijn niets te ingewikkeld, maar we moeten er rekening mee houden dat in Bestel Product klasse zetten we @JsonIgnore op de primaire sleutel. Dat komt omdat we niet willen serialiseren Bestellen onderdeel van de primaire sleutel omdat deze overbodig zou zijn.

We hebben alleen de Product om aan de gebruiker te worden getoond, dus daarom hebben we voorbijgaande getProduct () methode.

Vervolgens hebben we een eenvoudige service-implementatie nodig:

@Service @Transactional openbare klasse OrderServiceImpl implementeert OrderService {// orderRepository constructor injectie @Override public Iterable getAllOrders () {retourneer this.orderRepository.findAll (); } @Override public Order create (Order order) {order.setDateCreated (LocalDate.now ()); retourneer this.orderRepository.save (order); } @Override public void update (Order order) {this.orderRepository.save (order); }}

En een controller toegewezen aan / api / bestellingen ermee omgaan Bestellen verzoeken.

Het belangrijkste is de creëren() methode:

@PostMapping openbare ResponseEntity create (@RequestBody OrderForm-formulier) {List formDtos = form.getProductOrders (); validateProductsExistence (formDtos); // maak orderlogica // vul order met producten order.setOrderProducts (orderProducts); this.orderService.update (bestelling); String uri = ServletUriComponentsBuilder .fromCurrentServletMapping () .path ("/ orders / {id}") .buildAndExpand (order.getId ()) .toString (); HttpHeaders headers = nieuwe HttpHeaders (); headers.add ("Locatie", uri); retourneer nieuwe ResponseEntity (order, headers, HttpStatus.CREATED); }

Allereerst, we accepteren een lijst met producten met hun bijbehorende hoeveelheden. Daarna, we kijken of alle producten bestaan in de database en maak en bewaar vervolgens een nieuwe bestelling. We bewaren een referentie naar het nieuw gemaakte object, zodat we er bestelgegevens aan kunnen toevoegen.

Tenslotte, we maken een koptekst "Locatie".

De gedetailleerde implementatie staat in de repository - de link ernaar wordt vermeld aan het einde van dit artikel.

3. Frontend

Nu we onze Spring Boot-applicatie hebben opgebouwd, is het tijd om te verhuizen het hoekige deel van het project. Om dit te doen, moeten we eerst Node.js met NPM installeren en daarna een Angular CLI, een opdrachtregelinterface voor Angular.

Het is heel eenvoudig om beide te installeren, zoals we konden zien in de officiële documentatie.

3.1. Het Angular Project opzetten

Zoals we al zeiden, zullen we gebruiken Hoekige CLI om onze applicatie te maken. Om de zaken eenvoudig te houden en alles op één plek te hebben, houden we onze Angular-applicatie binnen het / src / main / frontend map.

Om het te maken, moeten we een terminal (of opdrachtprompt) openen in het / src / main map en voer uit:

ng nieuwe frontend

Hiermee worden alle bestanden en mappen gemaakt die we nodig hebben voor onze Angular-applicatie. In het bestand pakage.jsonkunnen we controleren welke versies van onze afhankelijkheden zijn geïnstalleerd. Deze tutorial is gebaseerd op Angular v6.0.3, maar oudere versies zouden het werk moeten doen, tenminste versie 4.3 en nieuwer (HttpClient die we hier gebruiken, werd geïntroduceerd in Angular 4.3).

Dat moeten we opmerken we zullen al onze opdrachten uitvoeren vanaf de /voorkant map tenzij anders vermeld.

Deze setup is voldoende om de Angular-applicatie te starten door uit te voeren ng serveren opdracht. Standaard draait het op // localhost: 4200 en als we daar nu heen gaan, zien we dat de Angular-basisapplicatie is geladen.

3.2. Bootstrap toevoegen

Voordat we verder gaan met het maken van onze eigen componenten, moeten we eerst toevoegen Bootstrap aan ons project, zodat we onze pagina's er mooi uit kunnen laten zien.

Hiervoor hebben we maar een paar dingen nodig. Ten eerste moeten wevoer een commando uit om het te installeren:

npm install --save bootstrap

en om vervolgens tegen Angular te zeggen om het daadwerkelijk te gebruiken. Hiervoor moeten we een bestand openen src / main / frontend / angular.json en voeg toe node_modules / bootstrap / dist / css / bootstrap.min.css onder "Stijlen" eigendom. En dat is het.

3.3. Componenten en modellen

Voordat we beginnen met het maken van de componenten voor onze applicatie, kijken we eerst hoe onze app eruit zal zien:

Nu gaan we een basiscomponent maken met de naam e-commerce:

ng g c e-commerce

Dit zal onze component binnen de / frontend / src / app map. Om het te laden bij het opstarten van de applicatie, zullen weneem het opin de app.component.html:

Vervolgens maken we andere componenten binnen deze basiscomponent:

ng g c / e-commerce / producten ng g c / e-commerce / bestellingen ng g c / e-commerce / winkelwagen

Natuurlijk hadden we al die mappen en bestanden desgewenst handmatig kunnen maken, maar in dat geval zouden we dat moeten doen vergeet niet om die componenten te registreren in ons AppModule.

We hebben ook enkele modellen nodig om onze gegevens gemakkelijk te manipuleren:

exportklasse Product {id: nummer; naam: string; prijs: aantal; pictureUrl: tekenreeks; // alle argumenten constructor}
exportklasse ProductOrder {product: Product; hoeveelheid: aantal; // alle argumenten constructor}
exportklasse ProductOrders {productOrders: ProductOrder [] = []; }

Het laatst genoemde model komt overeen met ons Bestelformulier op de backend.

3.4. Basiscomponent

Bovenaan onze e-commerce component, plaatsen we een navigatiebalk met de Home-link aan de rechterkant:

 Baeldung E-commerce 
  • HOME (huidig)

We zullen vanaf hier ook andere componenten laden:

We moeten in gedachten houden dat, om de inhoud van onze componenten te zien, we de navbar class, moeten we wat CSS toevoegen aan de app.component.css:

.container {padding-top: 65px; }

Laten we eens kijken naar de .ts bestand voordat we de belangrijkste onderdelen becommentariëren:

@Component ({selector: 'app-ecommerce', templateUrl: './ecommerce.component.html', styleUrls: ['./ecommerce.component.css']}) exportklasse EcommerceComponent implementeert OnInit {private collapsed = true; orderFinished = false; @ViewChild ('productsC') productsC: ProductsComponent; @ViewChild ('shoppingCartC') shoppingCartC: ShoppingCartComponent; @ViewChild ('ordersC') ordersC: OrdersComponent; toggleCollapsed (): void {this.collapsed =! this.collapsed; } finishOrder (orderFinished: boolean) {this.orderFinished = orderFinished; } reset () {this.orderFinished = false; this.productsC.reset (); this.shoppingCartC.reset (); this.ordersC.paid = false; }}

Zoals we kunnen zien, klikken op het Huis link zal onderliggende componenten resetten. We moeten toegang hebben tot methoden en een veld binnen de onderliggende componenten van de ouder, dus daarom bewaren we verwijzingen naar de kinderen en gebruiken we die in de resetten () methode.

3.5. De dienst

Om voor broers en zussen om met elkaar te communicerenen om gegevens van / naar onze API op te halen / te verzenden, moeten we een service maken:

@Injectable () exportklasse EcommerceService {private productsUrl = "/ api / products"; private ordersUrl = "/ api / orders"; privé productOrder: ProductOrder; privébestellingen: ProductOrders = new ProductOrders (); private productOrderSubject = nieuw onderwerp (); private ordersSubject = nieuw onderwerp (); private totalSubject = nieuw onderwerp (); privé totaal: aantal; ProductOrderChanged = this.productOrderSubject.asObservable (); OrdersChanged = this.ordersSubject.asObservable (); TotalChanged = this.totalSubject.asObservable (); constructor (privé http: HttpClient) {} getAllProducts () {retourneer this.http.get (this.productsUrl); } saveOrder (order: ProductOrders) {retourneer this.http.post (this.ordersUrl, order); } // getters en setters voor gedeelde velden}

Relatief eenvoudige dingen zitten hier in, zoals we konden opmerken. We doen een GET- en een POST-verzoek om met de API te communiceren. We maken ook gegevens die we tussen componenten moeten delen, waarneembaar, zodat we ons er later op kunnen abonneren.

Desalniettemin moeten we één ding opmerken met betrekking tot de communicatie met de API. Als we de applicatie nu uitvoeren, zouden we 404 ontvangen en geen gegevens ophalen. De reden hiervoor is dat, aangezien we relatieve URL's gebruiken, Angular standaard zal proberen te bellen naar // localhost: 4200 / api / products en onze backend-applicatie draait op localhost: 8080.

We kunnen de URL's hardcoderen naar localhost: 8080natuurlijk, maar dat willen we niet. In plaats daarvan, wanneer we met verschillende domeinen werken, moeten we een bestand maken met de naam proxy-conf.json in onze /voorkant map:

{"/ api": {"target": "// localhost: 8080", "secure": false}}

En dan moeten we Open package.json en veranderen scripts.start eigendom passend bij:

"scripts": {... "start": "ng serve --proxy-config proxy-conf.json", ...}

En nu zouden we dat gewoon moeten doen houd er rekening mee om de applicatie te starten met npm start in plaats daarvan ng serveren.

3.6. Producten

In onze ProductenComponent, we injecteren de service die we eerder hebben gemaakt en laden de productlijst vanuit de API en transformeren deze in de lijst met Productbestellingen aangezien we aan elk product een hoeveelheidsveld willen toevoegen:

exportklasse ProductsComponent implementeert OnInit {productOrders: ProductOrder [] = []; producten: Product [] = []; selectedProductOrder: ProductOrder; privé winkelenCartOrders: ProductOrders; abonnement: Abonnement; productSelected: boolean = false; constructor (privé ecommerceService: EcommerceService) {} ngOnInit () {this.productOrders = []; this.loadProducts (); this.loadOrders (); } loadProducts () {this.ecommerceService.getAllProducts () .subscribe ((products: any []) => {this.products = products; this.products.forEach (product => {this.productOrders.push (nieuwe ProductOrder ( product, 0));})}, (fout) => console.log (fout)); } loadOrders () {this.sub = this.ecommerceService.OrdersChanged.subscribe (() => {this.shoppingCartOrders = this.ecommerceService.ProductOrders;}); }}

We hebben ook een optie nodig om het product aan de winkelwagen toe te voegen of er een te verwijderen:

addToCart (bestelling: ProductOrder) {this.ecommerceService.SelectedProductOrder = bestelling; this.selectedProductOrder = this.ecommerceService.SelectedProductOrder; this.productSelected = true; } removeFromCart (productOrder: ProductOrder) {let index = this.getProductIndex (productOrder.product); if (index> -1) {this.shoppingCartOrders.productOrders.splice (this.getProductIndex (productOrder.product), 1); } this.ecommerceService.ProductOrders = this.shoppingCartOrders; this.shoppingCartOrders = this.ecommerceService.ProductOrders; this.productSelected = false; }

Ten slotte maken we een resetten() methode die we noemden in Sectie 3.4:

reset () {this.productOrders = []; this.loadProducts (); this.ecommerceService.ProductOrders.productOrders = []; this.loadOrders (); this.productSelected = false; }

We doorlopen de productlijst in ons HTML-bestand en tonen deze aan de gebruiker:

{{order.product.name}}

$ {{order.product.price}}

3.8. Bestellingen

We houden de dingen zo eenvoudig mogelijk en in het Bestellingen Component simuleer betalen door de eigenschap in te stellen op true en de bestelling op te slaan in de database. We kunnen controleren of de bestellingen zijn opgeslagen via h2-console of door te slaan // localhost: 8080 / api / orders.

We hebben de E-commerceService ook hier om de productlijst uit de winkelwagen en het totaalbedrag voor onze bestelling op te halen:

exportklasse OrdersComponent implementeert OnInit {orders: ProductOrders; totaal aantal; betaald: boolean; abonnement: Abonnement; constructor (privé ecommerceService: EcommerceService) {this.orders = this.ecommerceService.ProductOrders; } ngOnInit () {this.paid = false; this.sub = this.ecommerceService.OrdersChanged.subscribe (() => {this.orders = this.ecommerceService.ProductOrders;}); this.loadTotal (); } pay () {this.paid = true; this.ecommerceService.saveOrder (this.orders) .subscribe (); }}

En tot slot moeten we informatie aan de gebruiker weergeven:

BESTELLEN

  • {{order.product.name}} - $ {{order.product.price}} x {{order.quantity}} stuks.

Totaalbedrag: $ {{total}}

Betalen Gefeliciteerd! Je hebt de bestelling succesvol geplaatst.

4. Samenvoegen van Spring Boot- en Angular-toepassingen

We zijn klaar met de ontwikkeling van onze beide applicaties en het is waarschijnlijk gemakkelijker om deze afzonderlijk te ontwikkelen zoals wij deden. Maar tijdens de productie zou het veel handiger zijn om een ​​enkele applicatie te hebben, dus laten we die twee nu samenvoegen.

Wat we hier willen doen, is bouw de Angular-app die Webpack aanroept om alle activa te bundelen en in het / resources / static directory van de Spring Boot-app. Op die manier kunnen we gewoon de Spring Boot-applicatie uitvoeren en onze applicatie testen en dit alles inpakken en implementeren als één app.

Om dit mogelijk te maken, moeten we Open 'package.json‘Daarna weer enkele nieuwe scripts toevoegen scripts.bouwen:

"postbuild": "npm run deploy", "predeploy": "rimraf ../resources/static/ && mkdirp ../resources/static", "deploy": "copyfiles -f dist / ** ../resources/ statisch",

We gebruiken een aantal pakketten die we niet hebben geïnstalleerd, dus laten we ze installeren:

npm install --save-dev rimraf npm install --save-dev mkdirp npm install --save-dev copyfiles

De rimraf commando gaat naar de directory kijken en een nieuwe directory maken (eigenlijk opschonen), while copyfiles kopieert de bestanden van de distributiemap (waar Angular alles plaatst) naar onze statisch map.

Nu moeten we het gewoon doen rennen npm run build commando en dit zou al die commando's moeten uitvoeren en de uiteindelijke uitvoer zal onze verpakte applicatie in de statische map zijn.

Vervolgens draaien we onze Spring Boot-applicatie op poort 8080, openen deze daar en gebruiken de Angular-applicatie.

5. Conclusie

In dit artikel hebben we een eenvoudige e-commercetoepassing gemaakt. We hebben een API op de backend gemaakt met Spring Boot en deze vervolgens gebruikt in onze frontend-applicatie gemaakt in Angular. We hebben laten zien hoe we de componenten die we nodig hebben kunnen maken, ze met elkaar kunnen laten communiceren en data kunnen ophalen / verzenden van / naar de API.

Ten slotte hebben we laten zien hoe u beide applicaties kunt samenvoegen tot één, verpakte web-app in de statische map.

Zoals altijd is het volledige project dat we in dit artikel hebben beschreven, te vinden in het GitHub-project.