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