Google Code Prettify

2019年9月8日 星期日

Python: ORM (many-to-many)

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import Column, String

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = "mysql+pymysql://root:passw0rd@localhost/test?charset=utf8"
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)

program = db.Table('program', db.Model.metadata,
    db.Column('year', db.Integer, primary_key=True),
    db.Column('teacher_id', db.Integer, db.ForeignKey('teacher.id')),
    db.Column('student_id', db.Integer, db.ForeignKey('student.id'))
)

class Teacher(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(30))
    course = db.Column(db.String(20))
    students = db.relationship('Student', secondary=program)

class Student(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(30))
    grade = db.Column(db.String(10))
    teachers = db.relationship('Teacher', secondary=program)

teacher1 = Teacher(name='Kent Beck', course='TDD')
teacher2 = Teacher(name='Martin Fowler', course='Design Patterns')
teacher3 = Teacher(name='Brendan Burns', course='Distributed Systems')

db.session.add_all([teacher1, teacher2, teacher3])
db.session.commit()

student1 = Student(name='Steven', grade='1')
student2 = Student(name='Candice', grade='1')
db.session.add_all([student1, student2])
db.session.commit()

statement = program.insert().values(year=2019, teacher_id=teacher1.id, student_id=student1.id)
db.session.execute(statement)
statement = program.insert().values(year=2019, teacher_id=teacher3.id, student_id=student1.id)
db.session.execute(statement)

statement = program.insert().values(year=2019, teacher_id=teacher1.id, student_id=student2.id)
db.session.execute(statement)
statement = program.insert().values(year=2019, teacher_id=teacher2.id, student_id=student2.id)
db.session.execute(statement)

db.session.commit()

print(student1.teachers)
print(student2.teachers)
前一篇「Python: ORM (one-to-many)」說明怎麼用 Flask-SQLAlchemy 建立一對多的關係,這篇說明怎麼建立多對多的關係。

如上程式碼,老師和學生存在著授課的關係,學生可以選擇多個課程,老師也可以有許多學生,program 這個 table 即是記錄學生與老師間的關係。
  • line 20、26: 注意裡面的 secondary 屬性,這兩行除了使用
    relationship 建立起學生對老師的一對多關係、老師對學生的一對多關係,還用這個屬性指出另一個關係。 
  • line 12、37: 前一篇有用到 add 一次加入一個 table,add_all 則是允許一次多個。

2019年9月7日 星期六

Python: ORM (one-to-many)

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import Column, String

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = "mysql+pymysql://root:password@localhost/test?charset=utf8"
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)

class Sales(db.Model):
    emno = db.Column(db.String(6), primary_key=True)
    name = db.Column(db.String(20))
    customers = db.relationship('Customer')

class Customer(db.Model):
    cid = db.Column(db.String(10), primary_key=True)
    name = db.Column(db.String(20))
    emno = db.Column(db.String(6), db.ForeignKey(Sales.emno))

sales1 = Sales(emno='001234')
sales1.name = 'Steven'

db.session.add(sales1)

c1 = Customer(cid='L222149000')
c1.name = 'Shelley'
c1.emno = '001234'

c2 = Customer(cid='A123456789')
c2.name = 'Martin'
c2.emno = '001234'

db.session.add(c1)
db.session.add(c2)

db.session.commit()

print(sales1.customers)

前一篇「Python: ORM using Flask」說明如何用 Flask-SQLAlchemy 連線到資料庫進行查詢,既然是 ORM package,這篇來說明一下怎麼建立關係。

如上的程式碼,有一個 table Sales 儲存業務員的資料,另一個 table Customer 儲存客戶資料,每個客戶會分配一個業務員,所以 Sales 和 Customer 間是一對多的關係。
  • line 13: 這是「一」方指向「多」方的方式,在 relationship 間的屬性名稱可任意取名,和資料庫沒有關係,只是方便程式員辨識。
  • line 18: 在「多」方建立一個 foreign key 指向「一」方的 primary key。
  • line 38: 可以印出如下結果,確認這些類別真的和資料庫的 table 產生實質的連繫。
  • [<customer a123456789>, <customer l222149000>]

2019年8月25日 星期日

Python: ORM using Flask

from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
from sqlalchemy import Column, Integer, String
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

class Contacts(Base):
    __tablename__ = 'contacts'
    name = Column(String(100), primary_key=True, nullable=False)
    phone_number = Column(String(32))

# 連線到資料庫
engine = create_engine("mysql+pymysql://root:password@localhost/test?charset=utf8")

# 建立 transaction
session = sessionmaker(engine)
mySession = session()

# 查詢
result = mySession.query(Contacts).first()
print(result.name)

# 新增資料
mySession.add_all([Contacts(name='Steven', phone_number='0963210089')])
mySession.commit()

Flask 是和 Django 一樣,是 python 非常流行的 web framework ,上面是以 ORM 方式存取資料庫的程式,說明如下:
  • 資料庫
我使用 MariaDB,使用時可以將它視為 MySQL,我在裡面建了一個命名為 test 的資料庫,裡面有個名為 Contacts 的 table,如下:
  • 安裝套件
我使用 PyCharm 開發,要先安裝相關套件,安裝的方式是在「File > Settings > Project: flask > Project Interpreter」中,按右方的「+」,可以搜尋要安裝的 package,這裡請先安裝 (1) Flask (2) SQLAlchemy (3) PyMySQL。

PyMySQL 是連接資料庫的程式庫,SQLAlchemy 則是 ORM 程式庫。
  • 程式說明
既然是 ORM,首先就要將程式對映到 table。
    • line 2: 取得SQLAlchemy 的基礎類別
    • line 7~10: 將程式對映到 table Contacts
    • line 13: 連線到資料庫,連線字串前的 mysql+pymysql 指出了要使用 pymysql API 連線到 MySQL。
    • line 16~17: session 就如 Java 中的 transaction
    • line 20~21: 查詢並取得第一列的結果印出來
    • line 24~25: 新增一筆資料

除了上面的方法,還可再安裝 Flask-SQLAlchemy package,程式如下:
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import Column, String

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = "mysql+pymysql://root:password@localhost/test?charset=utf8"
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)

class Contacts(db.Model):
    __tablename__ = 'contacts'
    name = Column(String(100), primary_key=True, nullable=False)
    phone_number = Column(String(32))

item1 = Contacts.query.first()
print(item1.name)
print(item1.phone_number)
  • 程式說明:
    • line 6: Flask-SQLAlchemy 套件,資料庫的 URI 要放在 SQLALEMY_DATABASE_URI 程式設定裡。
    • line 7: 這一行可以不需要,將 SQLALCHEMY_TRACK_MODIFICATIONS 設定為 False,可以讓警告訊息不要出現。
    • line 8: 經由這個方式,db 這個類別可以使用 Flask-SQLAlchemy 提供的所有功能。

2019年7月20日 星期六

install Apache Spark (CentOS 7.x)

在測試環境 Spark 可以只安裝一台,但是在正式環境,在 HA 的原則下,會由多台伺服器組成叢集,其中一台是 master,其餘是 slave,這裡整理的會以一台 master、兩台 slave 為例,我是在 VMware Workslation Player 安裝了三台 CentOS 7.x。

首先下載如下的軟體:
Apache Spark 是由 scala 寫的,scala 是執行於 JVM 上的程式語言,所以要下載 java,雖然目前最新的 Java LTS 版本是 Java 11,但是 Spark 最新版只能在 Java 8 上運行。 (Oracle 現在對 Java 用於商業用途都要收取高昂的費用,建議下載 Open JDK。) 下載後,如下開始安裝。
  • 修改 hostname
為了方便辨識,以 root 身分將三台 server 的  hostname 改成 master、slave1、slave2,指令如下:
hostnamectl set-hostname 新的hostname
並於三台 server 的 /etc/hosts 中加入如下的設定:
192.168.0.103 master
192.168.0.107 slave1
192.168.0.106 slave2
IP 當然要視你自己的電腦而改變。
  • 環境配置
三台 server 都建立一個名為 spark 的使用者,建立的指令可以參考『使用者與群組』,後續以 spark 安裝軟體時,會使用到 sudo 指令,所以要在 /etc/sudoers 中加入如下設定:
spark   ALL=(ALL)       ALL
並且在三台 server 都建立如下目錄:
    • /app/soft
    • /app/spark
    • /home/spark/work
並將上面下載的軟體放入 /home/spark/work 目錄裡,然後在根目錄下 chown -R spark:spark /app 將這些目錄的 owner 改成 spark,之後的動作沒有特別說明,都用 spark 這個使用者來做。
  • 設定 ssh 免密碼認證
在叢集需要 ssh 免密碼登入,以 root 身分編輯 /etc/ssh/sshd_config 設定,如下:
RSAAuthentication yes
PubkeyAuthentication yes
AuthorizedKeysFile      .ssh/authorized_keys
開放使用 RSA 加密演算法公私鑰方式認證,key 放置於 ~/.ssh/authorized_keys 目錄裡,修改好執行 service sshd restart 重啟 SSH 服務,接下來都以 spark 使用者登入,並如下指令,產生公私鑰,並將公鑰提供給另兩台 server,匯入 ~/.ssh/authorized_keys 檔案裡。
ssh-keygen -t rsa
cd .ssh

# master server
mv id_rsa.pub authorized_keys_master.pub
# slave1 server
mv id_rsa.pub authorized_keys_slave1.pub
# slave2 server
mv id_rsa.pub authorized_keys_slave2.pub

# master server 將公鑰複製到另兩台機器 (三台都這麼做)
scp authorized_keys_master.pub spark@slave1:/home/spark/.ssh
scp authorized_keys_master.pub spark@slave2:/home/spark/.ssh

cat authorized_keys_master.pub >> authorized_keys
cat authorized_keys_slave1.pub >> authorized_keys
cat authorized_keys_slave2.pub >> authorized_keys
chmod 400 authorized_keys
  • 安裝 JDK
指令如下:
cd /home/spark/work
tar xzvf jdk_ri-8u40-b25-linux-x64-10_feb_2015.tar.gz
mv java-se-8u40-ri /app/soft
以 root 身分編輯 /etc/profile,如下:
#java
JAVA_HOME=/app/soft/java-se-8u40-ri
CLASSPATH=.:$JAVA_HOME/lib:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar
PATH=$JAVA_HOME/bin:$PATH
export PATH CLASSPATH JAVA_HOME
編輯完下如下指令讓設定生效。
source /etc/profile
  • 安裝 Hadoop & Spark
指令如下:
cd /home/spark/work
tar xvzf hadoop-3.1.2.tar.gz
mv hadoop-3.1.2 /app/soft
tar xvzf spark-2.4.3-bin-hadoop2.7.tgz
mv spark-2.4.3 /app/spark
使用 root 編輯 /etc/profile,如下:
# spark
export HADOOP_HOME=/usr/local/hadoop-3.1.2
export SPARK_HOME=/app/spark/spark-2.4.3
export PATH=$PATH:$HADOOP_HOME/bin:$HADOOP_HOME/sbin:$SPARK_HOME/bin:$SPARK_HOME/sbin
編輯完下如下指令讓設定生效。
source /etc/profile
  • 設定 Spark
進入 /app/spark/spark-2.4.3/conf 將 spark-env.sh.template 複製一份成 spark-env.sh,並於 spark-env.sh 中加入如下內容: (三台 server 都要設定)
export SPARK_MASTER_IP=master
export SPARK_MASTER_PORT=7077
export SPARK_EXECUTOR_INSTANCES=1
export SPARK_WORKER_INSTANCES=1
export SPARK_WORKER_CORES=1
export SPARK_WORKER_MEMORY=1024M
export SPARK_MASTER_WEBUI_PORT=8080
export SPARK_CONF_DIR=/app/spark/spark-2.4.3/conf
  • 啟動 Spark
在 master 上執行如下指令:
cd /app/spark/spark-2.4.3/sbin
./start-all.sh
在兩台 slave 上執行如下指令:
cd /app/spark/spark-2.4.3/sbin
./start-slave.sh spark://master:7077
怎麼驗證呢? 在 master 上開啟瀏覽器,輸入網址 http://localhost:8080 這是 Web UI 管理介面,可以看到 master 正常啟動,且 slave1、slave2 連到 master。

2019年4月5日 星期五

部署 RESTful service 到 docker

我用 spring boot 寫了一個 hello 的 RESTful service,如下,傳入名稱,服務就會回覆 Hello $name。
@RestController
public class HelloController {

@RequestMapping("/hello/{name}")
public String hello(@PathVariable String name) {
return "Hello " + name + " !";
}
}
這個 RESTful service 我打包成 jar 檔,要部署到 docker 上執行。打包好的 jar 檔名為 Hello-1.0.0.jar,在 docker 中要安裝 open JDK 11 (openjdk-11_linux-x64_bin.tar.gz),Dockerfile 是 docker 提供的自動化部署方式,繼續往下看之前,可以先看一下這一篇 -- 使用 Dockerfile 創建 docker 鏡像
  • Dockerfile
FROM centos:latest
MAINTAINER "Steven Shi"<hi.steven@gmail.com>
ADD openjdk-11_linux-x64_bin.tar.gz /usr/local
COPY Hello-1.0.0.jar /home
RUN ln -s /usr/local/jdk-11 /usr/bin/java
ENV JAVA_HOME /usr/local/jdk-11
ENV PATH $JAVA_HOME/bin:$PATH
EXPOSE 9080
CMD java -jar /home/Hello-1.0.0.jar
    • FROM centos:latest: 指定這個 image 的基礎鏡像為 centos 的最新版。
    • ADD openjdk-11_linux-x64_bin.tar.gz /usr/local: 把 jdk 部署到 /usr/local 目錄下,ADD 指令不只會 copy 指定的檔案到指定目錄,還具有解壓縮的功能。
    • COPY Hello-1.0.0.jar /home: 把 RESTful service 的 jar 檔部署到 /home 目錄下。
    • EXPOSE 9080: 這個 container 對外開放出 9080 這個 port。
    • CMD java -jar /home/Hello-1.0.0.jar: 在 container 啟動時,讓 hello 服務啟動。
  • 部署服務到 docker
docker build -t steven/hello .
在上面那三個檔案所在目錄執行上述指令,就會在 docker 產生鏡像,名稱為 steven/hello,注意,最後面有個句點,指出是在目前目錄。
  • 啟動 container
docker run -p 9080:9080 --name hello-container -d steven/hello
    • -p 9080:9080 : container 外要連入 container 的 port 也設為 9080 和 container 開放給外界的 port 相連通 (port 號不需要一樣)。
    • --name : container 啟動後的名稱。
    • -d : 啟動為 daemon 狀態,後面接的是鏡像名稱。
  • 測試
curl localhost:9080/hello/Steven
用 curl 發一個 http request 如上,會傳回 Hello Steven,確認服務部署成功。

2019年3月23日 星期六

安裝 Minikube

最簡單可以執行 Kubernetes 的方式,大概是使用 Minikube (只能用在開發環境,不能用在正式環境!),這裡說明一下怎麼在 CentOS 7 上安裝 Minikube。
  • 開啟 BIOS 中的 VT-x/EPT 設定
首先要先確定自己電腦的 BIOS 中的 VT-x/EPT 或 AMD-V/RVI 有沒有打開? 我是在 VMware workstation 上執行 CentOS,所以就如下圖先把該選項打勾後,再啟動 CentOS。

  • 安裝 VisualBox
minikube 要能執行,需要有 VisualBox,所以先安裝 VisualBox,如下:
yum install VirtualBox-6.0
  • 安裝 kubectl 套件
curl -LO https://storage.googleapis.com/kubernetes-release/release/`curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt`/bin/linux/amd64/kubectl
kubectl 是 kubernetes 的命令列介面,用來操作 kubernetes 用的,上述指令下載套件後,再下如下兩個指令,給予 kubectl 執行權限,並移到 PATH 路徑下。
chmod +x ./kubectl
mv ./kubectl /usr/local/bin/kubectl
  • 安裝 minikube
curl -Lo minikube https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64 && chmod +x minikube

cp minikube /usr/local/bin && rm minikube
如上,兩行指令即可安裝好 minikube。
  • 啟動 minikube
minikube start
啟動 minikube 會出現如下畫面,表示成功安裝了!

啟動後,在 home 目錄下,會多出一個 .kube 的資料夾,裡面有一些 minikube 的設定,可以用 cat 指令看 ~/.kube/config 裡的內容。

如果要查看 minikube 的狀態,可以使用 minikube status 指令:

如果要指定使用什麼 vm,可以用如下指令:
minikube start --vm-driver=virtualbox
應該可以得到如下的結果:
  • 啟動時帶入環境設定
minikube 啟動時,會啟動一個 VisualBox,並預設分配 2GB 的 RAM,如果想要大一點? 可以如下:
minikube start --cpus=4 --memory=4096 --kubernetes-version=v1.7.2
可以指定 cpu 個數,RAM 的大小,甚至 kubernetes 版本。
  • 在 minikube 上執行測試用的 app
kubectl run hello-minikube --image=gcr.io/google_containers/echoserver:1.8 --port=8080

kubectl expose deployment hello-minikube --type=NodePort

minikube service hello-minikube --url
如上,安裝一個 google 提供的 hello 程式,三個指令說明如下:
    • kubectl run: 在 minikube 上執行一個 docker image
    • kubectl expose: 把 hello-minikube 這個服務開啟出來給外界使用
    • minikube service: 查詢一下 hello-minikube 這網址
接下來因為我是在命令列工作,如果有瀏覽器,可以直接瀏覽上面查詢到的網址,我則用 curl 命令。
curl 192.168.99.100:32714
得到如下的結果:

2019年2月17日 星期日

k8s troubleshooting (1): Pod 無法啟動

在 Centos 7 上,依著「Kubernetes權威指南」第1章 p.6 的第一個範例安裝一個 demo 的 ReplicationController,安裝好,Pod 沒有執行,於是利用以下指令查一下是什麼原因:
kubectl describe replicationcontrollers/mysql
出現了如下錯誤訊息:
Error creating: No API token found for service account "default", retry after the token is automatically created and added to the service account
在網路 google 了一下,得到的解答如下:
  • disable KUBE_ADMISSION_CONTROL
使用 vi 打開 /etc/kubernetes/apiserver,找到 KUBE_ADMISSION_CONTROL 把原來的設定用 # 註解掉,然後加上 KUBE_ADMISSION_CONTROL="" 後存檔。
  • 重啟 kube-apiserver
在命令列執行:
systemctl restart kube-apiserver
這樣解決了 RC 無法啟動的問題。

接下來我用如下指令:
kubectl get pods
得到如下的錯誤訊息:
NAME READY STATUS RESTARTS AGE
mysql-14w74 0/1 ContainerCreating 0 3h

Pod 一直在 ContainerCreating 的狀態,我再用這個指令查一下原因:
kubectl describe pods mysql-14w74
看到如下錯誤訊息:
1m 34s 3 {kubelet 127.0.0.1} Warning FailedSync Error syncing pod, skipping: failed to “StartContainer” for “POD” with ErrImagePull: “image pull failed for registry.access.redhat.com/rhel7/pod-infrastructure:latest, this may be because there are no credentials on this request. details: (open /etc/docker/certs.d/registry.access.redhat.com/redhat-ca.crt: no such file or directory)”

大意是有些鏡像拉不下來 … 解決辦法是先安裝 rhsm ...
yum install *rhsm*

再把相關的鏡像手動拉下來 …
docker pull registry.access.redhat.com/rhel7/pod-infrastructure:latest

刪除原有不正常的 rc 和 pods 並重新 create 就可以了,如下:
kubectl delete pods mysql-14w74
kubectl delete rc mysql
kubectl create -f mysql-rc.yaml

2019年1月24日 星期四

Spring Security: AuthenticationManager

前一篇 (Spring Security: 資料庫認證、授權) 說明了怎麼使用資料庫認證,但是,看完可能會有點混淆,似乎 Spring Security 的認證、授權,就一定要使用到 WebSecurityConfigurerAdapter,Spring Security 一定要在 Web 中使用? 當然不是! 這裡以官方文件 Spring Security Reference 中的範例來說明。
  • AuthenticationExample
public class AuthenticationExample {

  private static AuthenticationManager am = new SampleAuthenticationManager();

  public static void main(String[] args) throws Exception {
    BufferedReader in = new BufferedReader(new InputStreamReader(System.in));

    while (true) {
      System.out.println("Please enter your username:");
      String name = in.readLine();
      System.out.println("Please enter your password:");
      String password = in.readLine();
      try {
        Authentication request = new UsernamePasswordAuthenticationToken(name, password);
        Authentication result = am.authenticate(request);
        SecurityContextHolder.getContext().setAuthentication(result);
        break;
      } catch(AuthenticationException e) {
        System.out.println("Authentication failed: " + e.getMessage());
      }
    }
  
    System.out.println("Successfully authenticated. Security context contains: " +
                        SecurityContextHolder.getContext().getAuthentication());
  }
}
  • SampleAuthenticationManager
public class SampleAuthenticationManager implements AuthenticationManager {
  static final List<GrantedAuthority> AUTHORITIES = new ArrayList<GrantedAuthority>();

  static {
    AUTHORITIES.add(new SimpleGrantedAuthority("ROLE_USER"));
  }

  public Authentication authenticate(Authentication auth)
      throws AuthenticationException {
    if (auth.getName().equals(auth.getCredentials())) {
      return new UsernamePasswordAuthenticationToken(
                 auth.getName(), auth.getCredentials(), AUTHORITIES
             );
    }

    throw new BadCredentialsException("Bad Credentials");
  }
}
  • 執行結果
Please enter your username:
bob
Please enter your password:
password
Authentication failed: Bad Credentials
Please enter your username:
bob
Please enter your password:
bob
Successfully authenticated. Security context contains: org.springframework.security.authentication.UsernamePasswordAuthenticationToken@441d0230: Principal: bob; Credentials: [PROTECTED]; Authenticated: true; Details: null; Granted Authorities: ROLE_USER
程式簡單的要使用者從命令列輸入帳號、密碼,然後檢查帳密是否合法,這裡把帳密當成同一字串,所以第二次輸入 bob / bob 時,認證才成功,現在來說明一下程式。 SampleAuthenticationManager 這個類別實作 AuthenticationManager 介面,這個介面很簡單,只有一個 method - authenticate(Authentication authentication),只需要傳入一個實作 Authentication 介面的類別物件就行,範例傳入的是 UsernamePasswordAuthenticationToken 類別的物件。 UsernamePasswordAuthenticationToken 這個類別非常簡單的,它實作 Authentication,用來放使用者的帳號、密碼 (如果必要還可以放角色,範例放入了預設的角色 USER。),然後在 authenticate method 裡,就可以看到,我們取出帳號、密碼來比對,相同就回傳 token,失敗拋出 exception。 回到主程式,可以看到取得 authenticate 的回傳值後,範例程式將它放入 SecurityContextHolder 的 context 的 authentication 裡,為什麼?
Authentication result = am.authenticate(request);
SecurityContextHolder.getContext().setAuthentication(result);
這是 spring security 的要求,在認證成功後,要將使用者的 token 放入 SecurityContext 裡,未來 spring security 需要使用者的權限時,會自行取用。

2019年1月10日 星期四

Spring Batch: multiple datasource

在 spring boot 環境中,使用 spring batch,如果需要連線兩個不同資料庫,要怎麼做呢?
  • 在 application.properties 定義多個 dataSource
batch.datasource.driver-class-name=oracle.jdbc.OracleDriver
batch.datasource.url=jdbc:oracle:thin:@192.168.50.12:1521:testDB1
batch.datasource.username=dbuser1
batch.datasource.password=p@ssword

db1.datasource.driver-class-name=oracle.jdbc.OracleDriver
db1.datasource.url=jdbc:oracle:thin:@192.168.50.12:1521:testDB1
db1.datasource.username=dbuser1
db1.datasource.password=p@ssword

db2.datasource.driver-class-name=oracle.jdbc.OracleDriver
db2.datasource.url=jdbc:oracle:thin:@192.168.51.168:1521:testDB2
db2.datasource.username=dbuser2
db2.datasource.password=p@ssword
如上,db1、db2 分別連到不同的兩個資料庫,但是,除此之外,還要設定一個給 spring batch 使用,所以有 batch (第一個) 的 dataSource 設定,雖然 spring batch 與 db1 使用同一個資料庫,還是要另外設定 dataSource,否則在執行到 spring batch 的 tasklet 時,如果裡面有存取到 db1、db2 的資料庫時,會抓不到 transaction。
  • 設定多個 dataSource
@Configuration
public class MultipleDataSourceConfig {
    @Autowired
    private Environment env;
 
    @Bean(name = "batchDatasource")
    @ConfigurationProperties(prefix = "batch.datasource")
    public DataSource batchDataSource() {
        return DataSourceBuilder.create()
           .driverClassName(env.getProperty("batch.datasource.driver-class-name"))
           .url(env.getProperty("batch.datasource.url"))
           .username(env.getProperty("batch.datasource.username"))
           .password(env.getProperty("batch.datasource.password"))
           .build();
    }

    @Bean(name = "db1Datasource")
    @Primary
    @ConfigurationProperties(prefix = "db1.datasource")
    public DataSource db1DataSource() {
        return DataSourceBuilder.create()
           .driverClassName(env.getProperty("db1.datasource.driver-class-name"))
           .url(env.getProperty("db1.datasource.url"))
           .username(env.getProperty("db1.datasource.username"))
           .password(env.getProperty("db1.datasource.password"))
           .build();
    }
 
    @Bean(name = "db2Datasource")
    @ConfigurationProperties(prefix = "db2.datasource")
    public DataSource db2DataSource() {
        return DataSourceBuilder.create()
           .driverClassName(env.getProperty("db2.datasource.driver-class-name"))
           .url(env.getProperty("db2.datasource.url"))
           .username(env.getProperty("db2.datasource.username"))
           .password(env.getProperty("db2.datasource.password"))
           .build();
    }
}
  • 為每個 dataSource 設定相關的 transactionManager
    • BatchDbConfig.java
@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
   entityManagerFactoryRef = "batchEntityManagerFactory",
   basePackages = { "idv.steven.batch.dao" },
   transactionManagerRef = "batchTransactionManager"
)
public class BatchDbConfig {
 @Autowired
 @Qualifier("batchDatasource")
 private DataSource datasource;
 
    private JpaVendorAdapter jpaVendorAdapter() {
        HibernateJpaVendorAdapter adapter = new HibernateJpaVendorAdapter();

        adapter.setDatabase(Database.ORACLE);
        adapter.setShowSql(true);
        adapter.setGenerateDdl(false);
        adapter.setDatabasePlatform("org.hibernate.dialect.Oracle12cDialect");

        return adapter;
    }
    
    /**
     * 載入 Entity
     * @return
     */
    @Bean
    public LocalContainerEntityManagerFactoryBean batchEntityManagerFactory() {
        LocalContainerEntityManagerFactoryBean emf = new LocalContainerEntityManagerFactoryBean();
        emf.setDataSource(datasource);
        emf.setPackagesToScan(new String[] { "idv.steven.batch.entity" });
        emf.setJpaVendorAdapter(this.jpaVendorAdapter());
        emf.setSharedCacheMode(SharedCacheMode.NONE);

        return emf;
    }
    
    @Bean(name = "batchTransactionManager")
    public PlatformTransactionManager batchTransactionManager() {
        JpaTransactionManager tm = new JpaTransactionManager();
        tm.setEntityManagerFactory(this.batchEntityManagerFactory().getObject());
        
        return tm;
    }
}
    • DB1DbConfig.java
@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
   entityManagerFactoryRef = "db1EntityManagerFactory",
   basePackages = { "idv.steven.database.db1.dao" },
   transactionManagerRef = "db1TransactionManager"
)
public class DB1DbConfig {
 @Autowired
 @Qualifier("db1Datasource")
 private DataSource datasource;
 
    private JpaVendorAdapter jpaVendorAdapter() {
        HibernateJpaVendorAdapter adapter = new HibernateJpaVendorAdapter();

        adapter.setDatabase(Database.ORACLE);
        adapter.setShowSql(true);
        adapter.setGenerateDdl(false);
        adapter.setDatabasePlatform("org.hibernate.dialect.Oracle12cDialect");

        return adapter;
    }
    
    /**
     * 載入 Entity
     * @return
     */
    @Primary
    @Bean
    public LocalContainerEntityManagerFactoryBean db1EntityManagerFactory() {
        LocalContainerEntityManagerFactoryBean emf = new LocalContainerEntityManagerFactoryBean();
        emf.setDataSource(datasource);
        emf.setPackagesToScan(new String[] { "idv.steven.database.db1.entity" });
        emf.setJpaVendorAdapter(this.jpaVendorAdapter());
        emf.setSharedCacheMode(SharedCacheMode.NONE);

        return emf;
    }
    
    @Primary
    @Bean(name = "db1TransactionManager")
    public PlatformTransactionManager db1TransactionManager() {
        JpaTransactionManager tm = new JpaTransactionManager();
        tm.setEntityManagerFactory(this.db1EntityManagerFactory().getObject());
        
        return tm;
    }
}
    • DB2DbConfig.java
@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
   entityManagerFactoryRef = "db2EntityManagerFactory",
   basePackages = { "idv.steven.database.db2.dao" },
   transactionManagerRef = "db2TransactionManager"
)
public class DB2DbConfig {
 @Autowired
 @Qualifier("db2Datasource")
 private DataSource datasource;
 
    private JpaVendorAdapter jpaVendorAdapter() {
        HibernateJpaVendorAdapter adapter = new HibernateJpaVendorAdapter();

        adapter.setDatabase(Database.ORACLE);
        adapter.setShowSql(true);
        adapter.setGenerateDdl(false);
        adapter.setDatabasePlatform("org.hibernate.dialect.Oracle12cDialect");

        return adapter;
    }
    
    /**
     * 載入 Entity
     * @return
     */
    @Bean
    public LocalContainerEntityManagerFactoryBean db2EntityManagerFactory() {
        LocalContainerEntityManagerFactoryBean emf = new LocalContainerEntityManagerFactoryBean();
        emf.setDataSource(datasource);
        emf.setPackagesToScan(new String[] { "idv.steven.database.db2.entity" });
        emf.setJpaVendorAdapter(this.jpaVendorAdapter());
        emf.setSharedCacheMode(SharedCacheMode.NONE);

        return emf;
    }
    
    @Bean(name = "db2TransactionManager")
    public PlatformTransactionManager db2TransactionManager() {
        JpaTransactionManager tm = new JpaTransactionManager();
        tm.setEntityManagerFactory(this.db2EntityManagerFactory().getObject());
        
        return tm;
    }
}
三個 transaction manager 需有一個設定為 primary,萬一有任何的 dao 沒有指明用那一個,系統才知道預設是那個。
  • 指定 spring batch 使用那一個 dataSource
@Configuration
@EnableBatchProcessing
public class BatchConfig {
    @Bean
    BatchConfigurer configurer(@Qualifier("batchDatasource") DataSource dataSource){
  return new DefaultBatchConfigurer(dataSource);
    }
}

在有 @EnableBatchProcessing 的類別裡,建立一個 BatchConfigurer 的 bean,指定 spring batch 要使用的 dataSource。