2022/05/16

GraphQL with SpringBoot & MySQL

這是使用 SpringBoot 並將資料放在 MySQL 的 GraphQL sample

建立專案

Spring Initializer 產生 maven project,在 Dependencies 的部分增加

  • Lombok
  • MySQL Driver
  • Spring Data JPA
  • Spring Web

取得 project 後,再增加 Graphql dependencies

              <dependency>
            <groupId>com.graphql-java</groupId>
            <artifactId>graphql-spring-boot-starter</artifactId>
            <version>5.0.2</version>
        </dependency>
        <dependency>
            <groupId>com.graphql-java</groupId>
            <artifactId>graphql-java-tools</artifactId>
            <version>5.2.4</version>
        </dependency>

Data Model

因應 MySQL 的 Table,需要先產生 ORM 的 Data Model

Post.java

package com.example.writeup.model;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import javax.persistence.*;
import java.util.Date;

@Getter
@Setter
@NoArgsConstructor
@Entity
@Table(name = "USER")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "USER_ID")
    private Integer userId;

    @Column(name = "FIRST_NAME")
    private String firstName;

    @Column(name = "LAST_NAME")
    private String lastName;

    @Column(name = "DOB")
    private Date dob;

    @Column(name = "ADDRESS")
    private String address;

    @Column(name = "POST_ID")
    private Integer postId;

    public User(String firstName, String lastName, Date dob, String address, Integer postId) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.dob = dob;
        this.address = address;
        this.postId = postId;
    }
}

User.java

package com.example.writeup.model;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import javax.persistence.*;
import java.util.Date;

@Getter
@Setter
@NoArgsConstructor
@Entity
@Table(name = "USER")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "USER_ID")
    private Integer userId;

    @Column(name = "FIRST_NAME")
    private String firstName;

    @Column(name = "LAST_NAME")
    private String lastName;

    @Column(name = "DOB")
    private Date dob;

    @Column(name = "ADDRESS")
    private String address;

    @Column(name = "POST_ID")
    private Integer postId;

    public User(String firstName, String lastName, Date dob, String address, Integer postId) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.dob = dob;
        this.address = address;
        this.postId = postId;
    }
}

Repository

這是用來跟資料庫建立跟剛剛的 Data Model 的關聯

PostRepository.java

package com.example.writeup.repository;

import com.example.writeup.model.Post;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface PostRepository extends JpaRepository<Post,Integer> {
}

UserRepository.java

package com.example.writeup.repository;

import com.example.writeup.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

@Repository
public interface UserRepository extends JpaRepository<User, Integer> {

    @Transactional
    @Modifying
    @Query(value = "UPDATE user SET address = ?1 WHERE user_id = ?2 ", nativeQuery = true)
    int updateUserAddress(String address, Integer user_id);

}

Note: 原文說可以改為繼承 CrudRepository,但 find 會回傳一般的 List,但 JpaRepository 會回傳 iterable list ref: What is difference between CrudRepository and JpaRepository interfaces in Spring Data JPA?

DataLoader

用在 init project 時,會自動產生 MySQL table 及測試資料

DataLoader.java

package com.example.writeup.service;

import com.example.writeup.model.Post;
import com.example.writeup.model.User;
import com.example.writeup.repository.PostRepository;
import com.example.writeup.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.util.Calendar;
import java.util.Date;
import java.util.concurrent.ThreadLocalRandom;

@Service
public class DataLoader {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private PostRepository postRepository;

    @PostConstruct
    public void loadData(){

        User user1 = new User("Yasas" ,"Sandeepa",DataLoader.getRandomDate(),"Mount Pleasant Estate Galle",1);
        User user2 = new User("Sahan" ,"Rambukkna",DataLoader.getRandomDate(),"Delkanda Nugegoda",2);
        User user3 = new User("Ranuk" ,"Silva",DataLoader.getRandomDate(),"Yalawatta gampaha",3);

        Post post1 = new Post("Graphql with SpringBoot",DataLoader.getRandomDate());
        Post post2 = new Post("Flutter with Firebase",DataLoader.getRandomDate());
        Post post3 = new Post("Nodejs Authentication with JWT",DataLoader.getRandomDate());

        postRepository.save(post1);
        postRepository.save(post2);
        postRepository.save(post3);

        userRepository.save(user1);
        userRepository.save(user2);
        userRepository.save(user3);
    }

    public static Date getRandomDate(){
        Calendar calendar = Calendar.getInstance();
        calendar.set(Calendar.YEAR, 1990);
        calendar.set(Calendar.MONTH, 1);
        calendar.set(Calendar.DATE, 2);
        Date date1 = calendar.getTime();
        calendar.set(Calendar.YEAR, 1996);
        Date date2 = calendar.getTime();
        long startMillis = date1.getTime();
        long endMillis = date2.getTime();
        long randomMillisSinceEpoch = ThreadLocalRandom
                .current()
                .nextLong(startMillis, endMillis);

        return new Date(randomMillisSinceEpoch);
    }
}

application.properties

產生設定檔

server.port=7000

#mysql properties
spring.jpa.generate-ddl=true
spring.datasource.url=jdbc:mysql://localhost/writeup
spring.datasource.username=user
spring.datasource.password=password
spring.datasource.driverClassName=com.mysql.cj.jdbc.Driver
spring.jpa.hibernate.ddl-auto=create

#graphql properties
graphql.servlet.corsEnabled=true
graphql.servlet.mapping=/graphql
graphql.servlet.enabled=true

GraphQL

  • 一開始的 pom.xml 已增加了相關 dependencies
  • 剛剛的 application.properties 增加 graphql.servlet 的設定
  • 建立 GraphQL schema

schema.graphqls

schema {
    query: Query,
    mutation: Mutation,
}

type Query{
    # Fetch All Users
    getAllUsers:[User]

}

type Mutation {
    # Update the user address
    updateUserAddress(userId:Int,address:String): User
}

type User {
    userId : ID!,
    firstName :String,
    lastName :String,
    dob:String,
    address:String,
    postId : Int,
}
  • 產生 Service

UserService.java

package com.example.writeup.service;

import com.coxautodev.graphql.tools.GraphQLMutationResolver;
import com.coxautodev.graphql.tools.GraphQLQueryResolver;
import com.example.writeup.model.User;
import com.example.writeup.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class UserService implements GraphQLQueryResolver, GraphQLMutationResolver {

    @Autowired
    private UserRepository userRepository;

    public List<User> getAllUsers() {
        return userRepository.findAll();
    }

    public User updateUserAddress(Integer userId, String address) throws Exception {
        try {
            userRepository.updateUserAddress(address, userId);
            User user = userRepository.findById(userId).get();
            return user;
        } catch (Exception e) {
            throw new Exception(e);
        }
    }
}

WriteupApplication

package com.example.writeup;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class WriteupApplication {

    public static void main(String[] args) {
        SpringApplication.run(WriteupApplication.class, args);
    }

}

Altair

這是 GraphQL 的測試工具,這邊安裝了 Chrome Extension 版本

網址為 http://localhost:7000/graphql/ ,點 Docs 可取得 Query 及 Mutation 兩個介面

References

[譯]使用 SpringBoot 和 MySQL 構建 GraphQL 服務端應用程式

Build a GraphQL Server With Spring Boot and MySQL

WriteUp github

Altair Altair GraphQL Client helps you debug GraphQL queries and implementations

2022/05/09

原生 vs 跨平台

現今軟體應用程式有四種使用者介面:終端機(Command Line Interface)、網頁、Mobile APP、Desktop APP。在為應用程式考量要提供哪一種使用者介面時,如果面對一般使用者,通常會以 網頁 -> Mobile -> Dekstop -> CLI 這樣的順序考慮。但每一種類型的使用者介面,又通常因為運作的 OS 平台不同而有不同的差異,導致開發的方法跟經驗,無法跨出熟悉的領域。

為了節省開發耗費的資源,跨平台成了開發者的首要考量,就如同 Java 一開始的宏願:Write Once, Run Everywhere!,這個目標,Java 並沒有取得全面的成功,但 Java 至少統一了過去數十年一部分的 Application 的開發。Java 在嵌入式環境、Mobile 行動平台、Desktop、網頁 Applet 直到現在都已經確認是失敗的。

失敗的重要原因在於效能及耗費的系統資源量,因為跨平台的要求,會需要將平台相關的功能包裝起來,再用另一個標準提供開發者介面,但這個封裝可能會造成系統資源的耗費,畢竟原生的函式庫會越貼近作業系統,資源的使用會比較少,效能也比較好。瀏覽器之間的差異相較於作業系統,差異是很小的,因此 Web 會是使用者介面的首選。

但為什麼還是會有跨平台的需求?畢竟不是每一個提供服務的廠商,都有足夠的工程師配額,能夠處理不同平台的開發。只要能夠節省成本,就永遠都會有跨平台的需求。

Web -> Mobile -> Dekstop -> CLI

一般使用者都能夠使用圖形介面 GUI 的應用程式,但終端機 Command Line Interface 就不同了,沒有經過 Linux OS 洗禮的使用者,通常無法適應一個字一個字敲打的指令列,通常也只有開發者才會去接觸 CLI 的程式。

在這幾種使用介面中,以網頁是最符合跨平台的開發方法,因為網頁的標準規格,各家瀏覽器都要遵循這些規格去開發,再加上有眾多框架與函式庫,解決了不同瀏覽器之間的些微差異問題,因此為應用提供網頁使用者介面,基本上是最常見的做法。

但實際上還是有很多應用程式,是沒辦法放在網頁裡面運作的,尤其是使用到 OS 原生的資源的應用。另外由於手機這種手持裝置的流行,人手一機,在面對一般使用者提供服務時,首先就會考慮提供手機 Mobile APP。

失敗經驗

過去曾經有使用過 Titanium 開發 iOS, Android 的失敗經驗,記得失敗的原因是 APP 只能做比較簡單的畫面,太複雜客製,非標準的 UI 元件,無法自由地透過 Titanium 產生,也無法產生具有不同平台特性的 UI 元件。

因此目前在 Mobile APP 比較常見的作法,是分開實作的,業界也分別需要 iOS 與 Android 的開發人員。也許當時我們是使用了一個封裝得不好的 Library 去開發,才導致失敗的結果。

不代表 iOS, Android 就一定要用原生開發,像 Game 的領域中,Unity 作為 Game Engine,也跨平台的 Game 開發做得很好。

Microsoft 也曾經想要統一 Desktop 與 Mobile 的使用者體驗,用 Tiles 做了 Windows Phone 及 Windows 10。現在大家已經知道,Windows Phone 已經完全在市場消失。而 Windows 11 的設計方式也已經向競爭對手看齊。

跨平台

是永久的需求及目標,成功與失敗的案例都有,但也都要經過嘗試,才能在一段時間後知道結果如何。

如果公司能夠負擔足夠的開發成本,或是專案運作的目標限制在某些平台上,當然就在不同平台去開發。要使用跨平台,如果能先用小的專案去測試,並先了解問題點,做先行測試,會是比較安全的作法。跨平台的效能跟資源耗費,必須要在可接受的範圍內。

另外也要考慮平台升級的問題,一但使用了跨平台的函式庫,就表示當核心升級後,會需要等待一段時間,才能取得跨平台函式庫的更新,這一段時間會是空窗期,遇到問題是完全無法解決的,也只能靜靜等待更新與修正。

References

2021 年加速開發的 8 個最佳跨平台框架

放弃坚持 15 年的原生开发,1Password 用 Electron 重写了全部代码

Top 10 Best Cross Platform App Development Frameworks in 2021

2022/04/25

電商營收數據指標

商品成交金額 GMV

商品成交金額(Gross Merchandise Volume,簡稱GMV

  • GMV = (來客數) 流量 × 購買轉換率 × 平均客單價

來客數、流量

可再區分裝置 (PC/Mobile/APP)、通路(自然流量/付費流量)

  • Unique Visitor

    不重複的來客數,實際上有多少訪客

  • Page View

    每一個頁面的瀏覽數量

  • Session

    使用者進入網站的次數,同一個使用者可能連接很多 Page。

  • MAU/WAU

    每月/週活躍用戶,檢視吸引用戶的能力

  • 下載量

    APP 下載數量

轉換率

從來店人流數量,轉換為真正購買的來客數量

  • CVR, Conversion Rate

    各頁面的轉換率

    從進入平台到結帳前,分析客戶是從哪個步驟跳開,分析產品的「訊息流」「任務流」

    訊息流 是從商品供給角度提供的商品內容與資訊,例如商品規格、評價、導購文章等

    任務流 是從用戶需求的角度去搜尋並找到他所需要的商品,例如搜尋篩選器、熱銷排行榜、推薦商品等

  • Bounce Rate

    CVR 的相反,了解是從哪裡跳開的

平均客單價 ABS、AOV

ABS (Average Basket Size) AOV (Average Order Value)

當客戶流量降低時,提升 ABS 是提升毛利的方法,常見的方法有:免運、折扣、跨銷、綁售

電商營運指標

流量指標

  • Session
  • Unique Visitor
  • Page View

轉化指標

  • CVR

用戶指標

  • 客單價 AOV

  • 用戶黏性

    • DAU (Daily Activited Users)

      日活躍用戶數

    • MAU (Monthly Activited Users)

      自統計之日算起一個月內登錄過APP的使用者總量

  • 用戶留存 Retention 回購

商品指標

  • 商品總數

    • SKU (Stock Keeping Unit)

      單品項管理、最小存貨單位

    • 庫存

  • 商品優勢

    • 個別商品轉化率&收入佔比、商品最低價比例

風險管控指標

  • 評價、投訴率、退貨率

拆解營收

  • 營收 = 來客數 * 購買轉換率 * 客單價

  • 營收 = 新客數 * 新客轉化率 * 新客的單價 + 舊客數 * 舊客回訪率 * 舊客轉換率 * 舊客單價

    區分新舊客戶數量

  • 營收 = 某某 channel 導流數 * 各自channel 轉化率 * 客單價

    區分網路流量來源,ex: EDM、LINE 官方帳號、搜尋流量、直接流量、網紅流量

  • 營收 = 品類一 * 銷售量 * 單價 + 品類二 * 銷售量 * 單價

    區分商品品項

  • 營收 = 通路一 * 銷售量 * 單價 + 通路二 * 銷售量 * 單價

    區分通路

  • 營收 = Campiagn 時期流量 * 轉化率 * 客單價 + 平常時期流量 * 轉化率 * 客單價

    區分週年慶時期

  • 營收 = 流量池導流數 * 轉化率 * 客單價 + 付費流量 * 轉化率 * 客單價 + 自然流量 * 轉化率 * 客單價

    LINE 官方帳號、APP 用戶,都被歸類在流量池

  • 營收 = 獲客數 * 回訪率 * 付費轉換率 * 付費頻率 * 客單價

    ex: 免費手遊

References

電商營收哪裡來?拆解各項重要數據指標

八種拆解營收的方法

電商 PM 都應了解的 5 大數據運營指標 -【數據乾貨大全】

電商人必備!68個常見電商專有名詞