Springを使った簡単なEコマースの実装

Springを使用した簡単なeコマースの実装

1. Eコマースアプリケーションの概要

このチュートリアルでは、簡単なeコマースアプリケーションを実装します。 Spring Bootを使用してAPIを開発し、Angularを使用してAPIを使用するクライアントアプリケーションを開発します。

基本的に、ユーザーは製品リストから製品をショッピングカートに追加したり、ショッピングカートから削除したり、注文したりすることができます。

2. バックエンド部

APIを開発するには、最新バージョンのSpringBootを使用します。 また、永続化のためにJPAおよびH2データベースを使用します。

To learn more about Spring Boot,you could check out our Spring Boot series of articlesおよびto get familiar with building a REST API, please check out another seriesが必要な場合。

2.1. Mavenの依存関係

プロジェクトを準備し、必要な依存関係をpom.xmlにインポートしましょう。

いくつかのコアSpring Boot dependenciesが必要です。


    org.springframework.boot
    spring-boot-starter-data-jpa
    2.0.4.RELEASE


    org.springframework.boot
    spring-boot-starter-web
    2.0.4.RELEASE

次に、H2 database


    com.h2database
    h2
    1.4.197
    runtime

そして最後に–Jackson library


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

Spring Initializrを使用して、必要な依存関係を持つプロジェクトをすばやく設定しました。

2.2. データベースのセットアップ

Spring BootでそのままインメモリH2データベースを使用することもできますが、APIの開発を開始する前に、いくつかの調整を行います。

application.propertiesファイルso we can actually check the state of our database and see if everything is going as we’d expectenable H2 consoleを入れます。

また、開発中にコンソールにSQLクエリを記録すると便利です。

spring.datasource.name=ecommercedb
spring.jpa.show-sql=true

#H2 settings
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console

これらの設定を追加すると、jdbc:h2:mem:ecommercedb as JDBC URLとユーザーsa を使用して、パスワードなしでhttp://localhost:8080/h2-consoleでデータベースにアクセスできるようになります。

2.3. プロジェクト構造

プロジェクトはいくつかの標準パッケージに編成され、Angularアプリケーションはフロントエンドフォルダーに配置されます。

├───pom.xml
├───src
    ├───main
    │   ├───frontend
    │   ├───java
    │   │   └───com
    │   │       └───example
    │   │           └───ecommerce
    │   │               │   EcommerceApplication.java
    │   │               ├───controller
    │   │               ├───dto
    │   │               ├───exception
    │   │               ├───model
    │   │               ├───repository
    │   │               └───service
    │   │
    │   └───resources
    │       │   application.properties
    │       ├───static
    │       └───templates
    └───test
        └───java
            └───com
                └───example
                    └───ecommerce
                            EcommerceApplicationIntegrationTest.java

リポジトリパッケージ内のすべてのインターフェースは単純であり、Spring DataのCrudRepositoryを拡張するため、ここではそれらの表示を省略していることに注意してください。

2.4. 例外処理

最終的な例外を適切に処理するには、APIの例外ハンドラが必要です。

You can find more details about the topic in our Error Handling for REST with Spring and Custom Error Message Handling for REST API articles

ここでは、ConstraintViolationExceptionとカスタムのResourceNotFoundExceptionに焦点を当てています。

@RestControllerAdvice
public class ApiExceptionHandler {

    @SuppressWarnings("rawtypes")
    @ExceptionHandler(ConstraintViolationException.class)
    public ResponseEntity handle(ConstraintViolationException e) {
        ErrorResponse errors = new ErrorResponse();
        for (ConstraintViolation violation : e.getConstraintViolations()) {
            ErrorItem error = new ErrorItem();
            error.setCode(violation.getMessageTemplate());
            error.setMessage(violation.getMessage());
            errors.addError(error);
        }
        return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST);
    }

    @SuppressWarnings("rawtypes")
    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity handle(ResourceNotFoundException e) {
        ErrorItem error = new ErrorItem();
        error.setMessage(e.getMessage());

        return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
    }
}

2.5. 製品情報

If you need more knowledge about persistence in Spring, there is a lot of useful articles in Spring Persistence series

このアプリケーションはonly reading products from the databaseをサポートするため、最初にいくつか追加する必要があります。

単純なProductクラスを作成しましょう。

@Entity
public class Product {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NotNull(message = "Product name is required.")
    @Basic(optional = false)
    private String name;

    private Double price;

    private String pictureUrl;

    // all arguments contructor
    // standard getters and setters
}

ユーザーがアプリケーションから商品を追加する機会はありませんが、商品リストに事前入力するために、データベースに商品を保存することをサポートします。

シンプルなサービスで十分です:

@Service
@Transactional
public class ProductServiceImpl implements ProductService {

    // productRepository constructor injection

    @Override
    public Iterable getAllProducts() {
        return productRepository.findAll();
    }

    @Override
    public Product getProduct(long id) {
        return productRepository
          .findById(id)
          .orElseThrow(() -> new ResourceNotFoundException("Product not found"));
    }

    @Override
    public Product save(Product product) {
        return productRepository.save(product);
    }
}

シンプルなコントローラーは、製品のリストを取得する要求を処理します。

@RestController
@RequestMapping("/api/products")
public class ProductController {

    // productService constructor injection

    @GetMapping(value = { "", "/" })
    public @NotNull Iterable getProducts() {
        return productService.getAllProducts();
    }
}

製品リストをユーザーに公開するために必要なのは、実際にデータベースにいくつかの製品を入れることだけです。 したがって、CommandLineRunnerクラスを使用して、メインアプリケーションクラスでBeanを作成します。

このようにして、アプリケーションの起動時に製品をデータベースに挿入します。

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

ここでアプリケーションを起動すると、http://localhost:8080/api/products.を介して製品リストを取得できます。また、http://localhost:8080/h2-consoleに移動してログインすると、PRODUCTという名前のテーブルがあります。追加したばかりの製品。

2.6. ご注文

API側では、POST要求を有効にして、エンドユーザーが行う注文を保存する必要があります。

まず、モデルを作成しましょう。

@Entity
@Table(name = "orders")
public class Order {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @JsonFormat(pattern = "dd/MM/yyyy")
    private LocalDate dateCreated;

    private String status;

    @JsonManagedReference
    @OneToMany(mappedBy = "pk.order")
    @Valid
    private List orderProducts = new ArrayList<>();

    @Transient
    public Double getTotalOrderPrice() {
        double sum = 0D;
        List orderProducts = getOrderProducts();
        for (OrderProduct op : orderProducts) {
            sum += op.getTotalPrice();
        }
        return sum;
    }

    @Transient
    public int getNumberOfProducts() {
        return this.orderProducts.size();
    }

    // standard getters and setters
}

ここでいくつかの点に注意する必要があります。 確かに、最も注目すべきことの1つは、remember to change the default name of our tableです。 クラスにOrderという名前を付けたため、デフォルトではORDERという名前のテーブルを作成する必要があります。 ただし、これは予約済みのSQLワードであるため、競合を避けるために@Table(name = “orders”)を追加しました。

さらに、2つの@Transient methods that will return a total amount for that order and the number of products in itがあります。 どちらも計算されたデータを表しているため、データベースに保存する必要はありません。

最後に、@OneToMany relation representing the order’s detailsがあります。 そのためには、別のエンティティクラスが必要です。

@Entity
public class OrderProduct {

    @EmbeddedId
    @JsonIgnore
    private OrderProductPK pk;

    @Column(nullable = false)
    private Integer quantity;

    // default constructor

    public OrderProduct(Order order, Product product, Integer quantity) {
        pk = new OrderProductPK();
        pk.setOrder(order);
        pk.setProduct(product);
        this.quantity = quantity;
    }

    @Transient
    public Product getProduct() {
        return this.pk.getProduct();
    }

    @Transient
    public Double getTotalPrice() {
        return getProduct().getPrice() * getQuantity();
    }

    // standard getters and setters

    // hashcode() and equals() methods
}

We have a composite primary keyhere

@Embeddable
public class OrderProductPK implements Serializable {

    @JsonBackReference
    @ManyToOne(optional = false, fetch = FetchType.LAZY)
    @JoinColumn(name = "order_id")
    private Order order;

    @ManyToOne(optional = false, fetch = FetchType.LAZY)
    @JoinColumn(name = "product_id")
    private Product product;

    // standard getters and setters

    // hashcode() and equals() methods
}

これらのクラスはそれほど複雑ではありませんが、OrderProductクラスでは、主キーに@JsonIgnoreを配置していることに注意してください。 これは、主キーのOrderの部分が冗長になるため、シリアル化したくないためです。

Productをユーザーに表示するだけでよいので、一時的なgetProduct()メソッドを使用します。

次に必要なのは、単純なサービスの実装です。

@Service
@Transactional
public class OrderServiceImpl implements OrderService {

    // orderRepository constructor injection

    @Override
    public Iterable getAllOrders() {
        return this.orderRepository.findAll();
    }

    @Override
    public Order create(Order order) {
        order.setDateCreated(LocalDate.now());
        return this.orderRepository.save(order);
    }

    @Override
    public void update(Order order) {
        this.orderRepository.save(order);
    }
}

そして、Orderリクエストを処理するために/api/ordersにマップされたコントローラー。

最も重要なのはcreate()メソッドです。

@PostMapping
public ResponseEntity create(@RequestBody OrderForm form) {
    List formDtos = form.getProductOrders();
    validateProductsExistence(formDtos);
    // create order logic
    // populate order with products

    order.setOrderProducts(orderProducts);
    this.orderService.update(order);

    String uri = ServletUriComponentsBuilder
      .fromCurrentServletMapping()
      .path("/orders/{id}")
      .buildAndExpand(order.getId())
      .toString();
    HttpHeaders headers = new HttpHeaders();
    headers.add("Location", uri);

    return new ResponseEntity<>(order, headers, HttpStatus.CREATED);
}

まず、we accept a list of products with their corresponding quantitiesです。 その後、データベース内のwe check if all products existthen create and save a new order。 注文の詳細を追加できるように、新しく作成されたオブジェクトへの参照を保持しています。

最後に、we create a “Location” header

詳細な実装はリポジトリにあります。リンクはこの記事の最後に記載されています。

3. フロントエンド

Spring Bootアプリケーションが構築されたので、次はthe Angular part of the projectを移動します。 そのためには、最初にNPMでNode.jsをインストールし、その後、AngularのコマンドラインインターフェースであるAngular CLIをインストールする必要があります。

公式ドキュメントに記載されているように、両方をインストールするのは本当に簡単です。

3.1. Angularプロジェクトのセットアップ

前述したように、Angular CLIを使用してアプリケーションを作成します。 物事をシンプルに保ち、すべてを1か所にまとめるために、Angularアプリケーションを/src/main/frontendフォルダー内に保持します。

作成するには、/src/mainフォルダーにあるターミナル(またはコマンドプロンプト)を開いて、次のコマンドを実行する必要があります。

ng new frontend

これにより、Angularアプリケーションに必要なすべてのファイルとフォルダーが作成されます。 ファイルpakage.jsonで、依存関係のどのバージョンがインストールされているかを確認できます。 このチュートリアルはAngularv6.0.3に基づいていますが、古いバージョン、少なくともバージョン4.3以降(ここで使用するHttpClientはAngular 4.3で導入されました)で十分です。

特に明記しない限り、we’ll run all our commands from the /frontend folderに注意する必要があります。

この設定は、ng serveコマンドを実行してAngularアプリケーションを起動するのに十分です。 デフォルトでは、http://localhost:4200で実行されます。ここに移動すると、ベースのAngularアプリケーションが読み込まれていることがわかります。

3.2. ブートストラップの追加

独自のコンポーネントの作成に進む前に、まずプロジェクトにBootstrapを追加して、ページの見栄えを良くしましょう。

これを実現するために必要なものはほんのわずかです。 First, we need torun a command to install it

npm install --save bootstrap

およびthen to say to Angular to actually use it。 このためには、ファイルsrc/main/frontend/angular.jsonを開き、“styles”プロパティの下にnode_modules/bootstrap/dist/css/bootstrap.min.css を追加する必要があります。 以上です。

3.3. コンポーネントとモデル

アプリケーションのコンポーネントの作成を開始する前に、まずアプリが実際にどのように表示されるかを確認しましょう。

image

次に、ecommerceという名前の基本コンポーネントを作成します。

ng g c ecommerce

これにより、/frontend/src/appフォルダー内にコンポーネントが作成されます。 To load it at application startup, we’llinclude itinto the app.component.html

次に、この基本コンポーネント内に他のコンポーネントを作成します。

ng g c /ecommerce/products
ng g c /ecommerce/orders
ng g c /ecommerce/shopping-cart

確かに、必要に応じてこれらすべてのフォルダとファイルを手動で作成することもできますが、その場合は、remember to register those components in our AppModuleを作成する必要があります。

データを簡単に操作するためのモデルもいくつか必要です。

export class Product {
    id: number;
    name: string;
    price: number;
    pictureUrl: string;

    // all arguments constructor
}
export class ProductOrder {
    product: Product;
    quantity: number;

    // all arguments constructor
}
export class ProductOrders {
    productOrders: ProductOrder[] = [];
}

前述の最後のモデルは、バックエンドのOrderFormと一致します。

3.4. 基本コンポーネント

ecommerceコンポーネントの上部に、右側にホームリンクがあるナビゲーションバーを配置します。

ここから他のコンポーネントもロードします。

コンポーネントのコンテンツを表示するには、navbarクラスを使用しているため、app.component.cssにCSSを追加する必要があることに注意してください。

.container {
    padding-top: 65px;
}

最も重要な部分にコメントする前に、.tsファイルを確認しましょう。

@Component({
    selector: 'app-ecommerce',
    templateUrl: './ecommerce.component.html',
    styleUrls: ['./ecommerce.component.css']
})
export class EcommerceComponent implements 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;
    }
}

ご覧のとおり、Homeリンクをクリックすると、子コンポーネントがリセットされます。 親から子コンポーネント内のメソッドとフィールドにアクセスする必要があるため、子への参照を保持し、reset()メソッド内でそれらを使用します。

3.5. サービス

siblings components to communicate with each otherand to retrieve/send data from/to our APIを作成するには、サービスを作成する必要があります。

@Injectable()
export class EcommerceService {
    private productsUrl = "/api/products";
    private ordersUrl = "/api/orders";

    private productOrder: ProductOrder;
    private orders: ProductOrders = new ProductOrders();

    private productOrderSubject = new Subject();
    private ordersSubject = new Subject();
    private totalSubject = new Subject();

    private total: number;

    ProductOrderChanged = this.productOrderSubject.asObservable();
    OrdersChanged = this.ordersSubject.asObservable();
    TotalChanged = this.totalSubject.asObservable();

    constructor(private http: HttpClient) {
    }

    getAllProducts() {
        return this.http.get(this.productsUrl);
    }

    saveOrder(order: ProductOrders) {
        return this.http.post(this.ordersUrl, order);
    }

    // getters and setters for shared fields
}

ここにあるように、比較的単純なものがここにあります。 APIと通信するためにGETリクエストとPOSTリクエストを作成しています。 また、後でサブスクライブできるように、コンポーネント間で共有する必要があるデータを観察可能にします。

それでも、APIとの通信に関して1つのことを指摘する必要があります。 ここでアプリケーションを実行すると、404を受け取り、データを取得しません。 これは、相対URLを使用しているため、Angularはデフォルトでhttp://localhost:4200/api/productsを呼び出そうとし、バックエンドアプリケーションはlocalhost:8080で実行されているためです。

もちろん、URLをlocalhost:8080にハードコーディングすることもできますが、それは私たちがやりたいことではありません。 代わりに、when working with different domains, we should create a file named proxy-conf.json in our /frontend folder

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

そして、一致するようにopen package.json and change scripts.start propertyする必要があります。

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

そして今、私たちはkeep in mind to start the application with npm start instead ng serveをすべきです。

3.6. 製品情報

ProductsComponentで、前に作成したサービスを挿入し、APIから商品リストを読み込んで、すべての商品に数量フィールドを追加するため、ProductOrdersのリストに変換します。

export class ProductsComponent implements OnInit {
    productOrders: ProductOrder[] = [];
    products: Product[] = [];
    selectedProductOrder: ProductOrder;
    private shoppingCartOrders: ProductOrders;
    sub: Subscription;
    productSelected: boolean = false;

    constructor(private 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(new ProductOrder(product, 0));
                    })
                },
                (error) => console.log(error)
            );
    }

    loadOrders() {
        this.sub = this.ecommerceService.OrdersChanged.subscribe(() => {
            this.shoppingCartOrders = this.ecommerceService.ProductOrders;
        });
    }
}

また、製品をショッピングカートに追加するか、ショッピングカートから削除するオプションも必要です。

addToCart(order: ProductOrder) {
    this.ecommerceService.SelectedProductOrder = order;
    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;
}

最後に、セクション3.4で説明したreset()メソッドを作成します。

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

HTMLファイルの製品リストを繰り返し処理して、ユーザーに表示します。

{{order.product.name}}

${{order.product.price}}

また、対応するCSSファイルに単純なクラスを追加して、すべてがうまく収まるようにします。

.padding-0 {
    padding-right: 0;
    padding-left: 1;
}

3.7. ショッピングカート

ShoppingCartコンポーネントでは、サービスも注入します。 これを使用して、ProductsComponentの変更をサブスクライブし(商品がショッピングカートに入れるように選択されたことを通知します)、カートの内容を更新し、それに応じて合計コストを再計算します。

export class ShoppingCartComponent implements OnInit, OnDestroy {
    orderFinished: boolean;
    orders: ProductOrders;
    total: number;
    sub: Subscription;

    @Output() onOrderFinished: EventEmitter;

    constructor(private ecommerceService: EcommerceService) {
        this.total = 0;
        this.orderFinished = false;
        this.onOrderFinished = new EventEmitter();
    }

    ngOnInit() {
        this.orders = new ProductOrders();
        this.loadCart();
        this.loadTotal();
    }

    loadTotal() {
        this.sub = this.ecommerceService.OrdersChanged.subscribe(() => {
            this.total = this.calculateTotal(this.orders.productOrders);
        });
    }

    loadCart() {
        this.sub = this.ecommerceService.ProductOrderChanged.subscribe(() => {
            let productOrder = this.ecommerceService.SelectedProductOrder;
            if (productOrder) {
                this.orders.productOrders.push(new ProductOrder(
                    productOrder.product, productOrder.quantity));
            }
            this.ecommerceService.ProductOrders = this.orders;
            this.orders = this.ecommerceService.ProductOrders;
            this.total = this.calculateTotal(this.orders.productOrders);
        });
    }

    ngOnDestroy() {
        this.sub.unsubscribe();
    }
}

注文が完了したら、ここからイベントを親コンポーネントに送信します。チェックアウトに進む必要があります。 ここにもreset()メソッドがあります。

finishOrder() {
    this.orderFinished = true;
    this.ecommerceService.Total = this.total;
    this.onOrderFinished.emit(this.orderFinished);
}

reset() {
    this.orderFinished = false;
    this.orders = new ProductOrders();
    this.orders.productOrders = []
    this.loadTotal();
    this.total = 0;
}

HTMLファイルは単純です:

Shopping Cart
Total: ${{total}}

Items bought:
  • {{ order.product.name }} - {{ order.quantity}} pcs.

3.8. ご注文

プロパティをtrueに設定し、データベースに注文を保存することで、できる限りシンプルにし、OrdersComponentで支払いをシミュレートします。 注文が保存されていることを確認するには、h2-consoleを使用するか、http://localhost:8080/api/orders.を押します。

ショッピングカートから商品リストと注文の合計金額を取得するには、ここでもEcommerceServiceが必要です。

export class OrdersComponent implements OnInit {
    orders: ProductOrders;
    total: number;
    paid: boolean;
    sub: Subscription;

    constructor(private 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();
    }
}

最後に、ユーザーに情報を表示する必要があります。

ORDER

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

Total amount: ${{ total }}

4. Spring BootとAngularアプリケーションの統合

両方のアプリケーションの開発を終了しましたが、おそらく私たちが行ったように個別に開発する方が簡単でしょう。 ただし、本番環境では、単一のアプリケーションを使用する方がはるかに便利なので、これら2つをマージしてみましょう。

ここでやりたいのはbuild the Angular app which calls Webpack to bundle up all the assets and push them into the /resources/static directory of the Spring Boot appです。 そうすれば、Spring Bootアプリケーションを実行してアプリケーションをテストし、これをすべてパックして1つのアプリとしてデプロイできます。

これを可能にするには、open ‘package.json‘ again add some new scripts after scripts.buildを実行する必要があります。

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

インストールされていないパッケージを使用しているので、それらをインストールしましょう。

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

rimrafコマンドはディレクトリを調べて新しいディレクトリを作成し(実際にクリーンアップします)、copyfilesはファイルを配布フォルダ(Angularがすべてを配置する場所)からstaticにコピーしますsフォルダ。

ここで、run npm run build command and this should run all those commands and the ultimate output will be our packaged application in the static folderを実行する必要があります。

次に、ポート8080でSpring Bootアプリケーションを実行し、そこにアクセスして、Angularアプリケーションを使用します。

5. 結論

この記事では、簡単な電子商取引アプリケーションを作成しました。 Spring Bootを使用してバックエンドでAPIを作成し、Angularで作成したフロントエンドアプリケーションで使用しました。 必要なコンポーネントを作成し、相互に通信させ、APIからデータを取得/送信する方法を示しました。

最後に、両方のアプリケーションを静的フォルダー内の1つのパッケージ化されたWebアプリにマージする方法を示しました。

いつものように、この記事で説明した完全なプロジェクトは、GitHub project.にあります。