상태표시줄에 표시된 날짜/시간 부분을 클릭하면 데이터를 조회할 날짜를 선택하기 위한 달력 위젯이 표시되도록 하기 위해 먼저 TimeLabel의 mouseReleaseEvent 를 오버라이드하여 clicked 시그널을 구현해준다.
3. MainWindow 수정
# 시간 레이블 추가
time_label = TimeLabel()
time_label.clicked.connect(lambda: actions.date_query(self, self.central_widget.get_selected_tab()))
status_bar.addPermanentWidget(time_label)
MainWindow의 time_label을 상태표시줄에 추가하는 부분에서 time_label의 clicked 시그널에 date_query를 호출하는 람다함수를 연결해준다. 이 때, central_widget의 get_selected_tab 을 호출하여 현재 선택된 탭 정보를 받아와 매개변수로 넘겨준다.
4. CentralWidget 수정
# 중앙 위젯
class CentralWidget(QWidget):
def __init__(self, parent):
super().__init__(parent)
self.doc_tab = DocTab()
self.doc_tab.setParent(self)
self.init_ui()
def init_ui(self):
self.setStyleSheet("background-color: #FFFFFF")
# 그리드 레이아웃
grid = QGridLayout()
# 탭 위젯 추가
grid.addWidget(self.doc_tab, 0, 0)
# 중앙 위젯에 그리드 레이아웃 적용
self.setLayout(grid)
def get_selected_tab(self):
cur = self.doc_tab.currentWidget()
return 0 if cur == self.doc_tab.tab1 else 1 if self.doc_tab.tab2 else 2
CentralWidget에 선택된 탭이 몇번 탭인지 반환하는 get_selected_tab 메소드를 구현한다
tab1이라면 0, tab2라면 1, tab3라면 2를 반환한다.
5. date_query 함수 작성
# 날짜로 데이터 조회
def date_query(parent, tab):
date_select = DateSelect(parent)
date_select.show_modal()
if not date_select.canceled:
if tab == 0:
today = date_select.calendar.selectedDate().toString('yyyy-MM-dd')
result = DayCalQueryResult(parent, get_daycal_owner_values(today), get_daycal_other_values(today), get_daycal_result(today), today)
result.show()
호출시에 매개변수로 전달받은 parent 를 부모 위젯으로 하여 DataSelect 다이얼로그를 생성, modal하게 표시한다 => DataSelect 는 달력 위젯을 사용하여 유저가 선택한 날짜를 가져오기 위한 위젯이다.
DataSelect 다이얼로그가 정상적으로 제출되었을 경우 받아온 날짜를 기준으로 DayCalOwnerValues, DayCalOtherValues, DayCalResult 정보를 DB에서 읽어온다.
읽어온 정보를 기반으로 DayCalQueryResult 위젯을 생성하여 표시한다. => DayCalQueryResult 는 조회된 데이터를 테이블로 나타내주기 위한 위젯이다.
테이블 모델에서의 참조를 용이하게 하기 위해서 위 처럼 인덱스를 사용한 get, set을 구현
db액션 수정
# 화주 명단 가져오기
def get_daycal_owner_list():
return session.query(DayCalOwner).order_by(DayCalOwner.id).all()
# 화주별 데이터 가져오기
def get_daycal_owner_values(today = None):
if today:
return session.query(DayCalOwnerValues).filter(DayCalOwnerValues.date == today).order_by(DayCalOwnerValues.owner_id).all()
today = date.today()
values = []
owner_list = get_daycal_owner_list()
for owner in owner_list:
id = owner.get(0)
name = owner.get(1)
value = session.query(DayCalOwnerValues).filter(and_(DayCalOwnerValues.owner_id == id, DayCalOwnerValues.date == today)).first()
if not value:
value = DayCalOwnerValues(today, id, name)
session.add(value)
session.commit()
values.append(value)
return values
# 기타 데이터 가져오기
def get_daycal_other_values(today = date.today()):
value = session.query(DayCalOtherValues).filter(DayCalOtherValues.date == today).first()
if not value:
value = DayCalOtherValues(today)
session.add(value)
session.commit()
return value
# 결과 데이터 가져오기
def get_daycal_result(today = date.today()):
value = session.query(DayCalResult).filter(DayCalResult.date == today).first()
if not value:
value = DayCalResult(today)
session.add(value)
session.commit()
return value
DB에서 쿼리를 통해 가져온 객체를 to_list를 호출하여 컬럼값이 들어있는 리스트의 형태로 반환하는 기존의 방식에서 객체 자체를 반환하는 방식으로 변경.
테이블 모델 수정
def data(self, index, role):
if index.isValid():
if role == Qt.DisplayRole:
value = self.table_data[index.column()].get(index.row())
return format(value, ',')
elif role == Qt.EditRole:
value = self.table_data[index.column()].get(index.row())
return value
elif role == Qt.TextAlignmentRole:
return int(Qt.AlignRight | Qt.AlignVCenter)
def headerData(self, section: int, orientation: Qt.Orientation, role: int = ...):
if role == Qt.DisplayRole:
if orientation == Qt.Horizontal:
return self.owner_list[section].get(1)
else:
return self.vertical_header[section]
return None
def setData(self, index: QModelIndex, value: int, role):
if role == Qt.EditRole:
r, c = index.row(), index.column()
self.changed(r, c, self.table_data[c].get(r), value)
self.table_data[c].set(r, value)
return True
return False
기존에 2차원 리스트 형태였던 데이터들이 객체의 배열로 변경되면서 참조 방식도 getter를 사용하도록 수정
이렇게 객체를 직접 수정하는 방식을 사용하는 것으로 모델을 사용하는 의미도 퇴색되지 않으며 저장기능의 구현도 훨씬 편해진다.
저장 액션 구현
def save():
session.commit()
테이블에서 쿼리로 가져온 객체를 직접 수정하도록 변경한 덕분에 단순히 session.commit()을 호출하는 것으로 모든 변경사항이 DB에 반영된다.
저장 액션 메뉴와 툴바에 추가
# 저장 액션 추가
save_data = QAction(QIcon('src/img/save_icon.png'), '저장하기', self)
save_data.setShortcut('Ctrl+S')
save_data.setStatusTip('화주 이름 변경')
save_data.triggered.connect(actions.save)
file_menu.addAction(save_data)
tool_bar.addAction(save_data)
화주가 추가되고 그에따라 화주별 데이터도 추가되어 데이터베이스에 반영되지만 데이터베이스에서 데이터를 읽어오는 것은 프로그램 실행시에만 수행하는 작업이며 반영된 데이터를 굳이 데이터베이스에서 다시 읽어오는것도 비효율적이기에 모델의 메소드를 호출하여 즉각 반영하도록 하였다.
added 와 removed 의 경우 테이블 자체가 변경되는 작업이기에 (열의 추가/삭제) beginInsertColumns, beginRemoveColumns 등 테이블의 변경작업이 시작됨을 알리는 메소드를 호출한 뒤 작업을 수행하고 마지막에 endInsertColumns, endRemoveColumns 등 테이블의 변경작업이 끝났음을 알리는 메소드를 호출해줘야 변경사항이 정상적으로 반영된다.
modified 의 경우 단순히 값을 수정하는 것 뿐이기에 header 리스트를 수정해주는 것만으로 간단하게 반영된다.
3. other_table 테이블 모델 작성
import operator
from PySide6.QtCore import QAbstractTableModel, Qt, SIGNAL, QModelIndex
from PySide6.QtGui import *
''' 생략 '''
class DayCalOthersTableModel(QAbstractTableModel):
def __init__(self, parent, data, *args):
QAbstractTableModel.__init__(self, parent, *args)
self.setParent(parent)
self.table_data = [data]
self.vertical_header = ['경매 사무실 입금', '가라경매 강동 입금', '직접 지출', '우리 경매', '강동 사입']
self.row_count = len(self.vertical_header)
self.column_count = 1
def rowCount(self, parent):
return self.row_count
def columnCount(self, parent):
return self.column_count
def data(self, index, role):
if index.isValid():
if role == Qt.DisplayRole or role == Qt.EditRole:
value = self.table_data[index.column()][index.row()]
return str(value)
def headerData(self, section: int, orientation: Qt.Orientation, role: int = ...):
if role == Qt.DisplayRole:
if orientation == Qt.Vertical:
return self.vertical_header[section]
return None
def flags(self, index):
return Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable
def setData(self, index, value, role):
if role == Qt.EditRole:
self.table_data[index.column()][index.row()] = value
return True
return False
''' 생략 '''
other_table 의 테이블 모델도 기본적으로 input_table의 그것과 거의 동일하게 구현
horizontal header가 필요하지 않기 때문에 vertical header만을 설정
모두 사용자가 입력하는 데이터로만 이루어졌기에 특정 셀의 수정 불가등의 설정을 하지 않음
4. result_table 테이블 모델 작성
import operator
from PySide6.QtCore import QAbstractTableModel, Qt, SIGNAL, QModelIndex
from PySide6.QtGui import *
''' 생략 '''
class DayCalResultTableModel(QAbstractTableModel):
def __init__(self, parent, result_data, *args):
QAbstractTableModel.__init__(self, parent, *args)
self.setParent(parent)
self.horizontal_header = ['계']
self.vertical_header = ['강동총금액 합계', '강동운임 합계', '강동하차비 합계', '강동수수료 4% 합계', '공제후금액 합계',
'중매수수료 5% 합계', '화주운임 합계', '화주하차비 합계', '상장수수료 4% 합계', '경매확인',
'경매 차액', '중개수수료 5%', '경매 차익']
self.row_count = len(self.vertical_header)
self.column_count = 1
self.table_data = [result_data]
def rowCount(self, parent):
return self.row_count
def columnCount(self, parent):
return self.column_count
def data(self, index, role):
if index.isValid():
if role == Qt.DisplayRole or role == Qt.EditRole:
value = self.table_data[index.column()][index.row()]
return str(value)
def headerData(self, section: int, orientation: Qt.Orientation, role: int = ...):
if role == Qt.DisplayRole:
if orientation == Qt.Vertical:
return self.vertical_header[section]
else:
return self.horizontal_header[section]
return None
def flags(self, index):
return Qt.ItemIsSelectable | Qt.ItemIsEnabled
마찬가지로 input_table과 거의 동일하게 구현
해당 테이블은 모든 셀을 수정 불가능하도록 설정
5. 테이블 뷰에 모델 세팅
from PySide6.QtWidgets import QTabWidget, QWidget, QGridLayout, QTableView
from controller import actions
from models.table_models import DayCalTableModel, DayCalOthersTableModel, DayCalResultTableModel
# 일일 정산서 계산서 위젯
class DayCal(QWidget):
# 생성자
def __init__(self):
super().__init__()
self.input_table = QTableView()
self.other_table = QTableView()
self.result_table = QTableView()
self.init_ui()
# ui 초기화
def init_ui(self):
# 화주별 데이터 입력테이블
self.data_model = DayCalTableModel(self, actions.get_daycal_owner_list(), actions.get_daycal_owner_values())
self.input_table.setModel(self.data_model)
# 기타 데이터 입력 테이블
self.other_data_model = DayCalOthersTableModel(self, actions.get_daycal_other_values())
self.other_table.setModel(self.other_data_model)
# 결과 테이블
self.result_data_model = DayCalResultTableModel(self, actions.get_daycal_result())
self.result_table.setModel(self.result_data_model)
# 그리드 레이아웃
grid = QGridLayout()
# 테이블위젯 추가
grid.addWidget(self.input_table, 0, 0)
grid.addWidget(self.other_table, 1, 0)
grid.addWidget(self.result_table, 0, 1, 2, 1)
grid.setRowStretch(0, 5)
grid.setColumnStretch(0, 5)
# 레이아웃 세팅
self.setLayout(grid)
# 화주 추가 반영
def owner_added(self, added_user):
self.data_model.owner_added(added_user)
# 화주 삭제 반영
def owner_removed(self, removed_name):
self.data_model.owner_removed(removed_name)
# 화주 이름 변경 반영
def owner_modified(self, org_name, chg_name):
self.data_model.owner_modified(org_name, chg_name)
''' 생략 '''
작성한 모델의 인스턴스를 생성(이 때, DB에서 데이터를 읽어와서 인자로 넘겨준다.)
테이블 뷰에 각각 해당하는 모델의 인스턴스를 세팅
레이아웃에 추가한 뒤 위젯에 레이아웃을 세팅한다.
DayCal 위젯은 owner_added, owner_removed, owner_modified 메소드를 각각 구현하여 화주 상태변경을 인식하고 각 테이블의 데이터모델의 메소드를 호출하여 이를 반영한다.
6. DB 작업을 위한 함수 작성
# 화주 명단 가져오기
def get_daycal_owner_list():
return [q.name for q in session.query(DayCalOwner).order_by(DayCalOwner.id)]
# 화주별 데이터 가져오기
def get_daycal_owner_values():
values = []
today = date.today()
for owner_id in session.query(DayCalOwner.id).order_by(DayCalOwner.id):
id = owner_id[0]
value = session.query(DayCalOwnerValues).filter(and_(DayCalOwnerValues.owner_id == id, DayCalOwnerValues.date == today)).first()
if not value:
value = DayCalOwnerValues(today, id)
session.add(value)
session.commit()
values.append(value.to_list())
return values
# 기타 데이터 가져오기
def get_daycal_other_values():
today = date.today()
value = session.query(DayCalOtherValues).filter(DayCalOtherValues.date == today).first()
if not value:
value = DayCalOtherValues(today)
session.add(value)
session.commit()
return value.to_list()
# 결과 데이터 가져오기
def get_daycal_result():
today = date.today()
value = session.query(DayCalResult).filter(DayCalResult.date == today).first()
if not value:
value = DayCalResult(today)
session.add(value)
session.commit()
return value.to_list()
데이터 모델의 인스턴스 생성에 필요한 데이터를 DB에서 가져오기 위한 함수 구현
get_daycal_owner_list
화주 이름 명단을 반환
화주 id를 기준으로 오름차순 정렬
get_daycal_owner_values
화주별 오늘의 데이터를 리스트 형태로 반환
화주 id를 기준으로 오름차순 정렬
오늘 날짜의 데이터가 존재하지 않을 경우 디폴트로 모든 값이 0인 기본 데이터를 생성하여 데이터베이스에 추가하고 해당 데이터를 반환한다.
get_daycal_other_values
오늘의 기타 데이터를 리스트 형태로 반환
오늘 날짜의 데이터가 존재하지 않을 경우 디폴트로 모든 값이 0인 기본 데이터를 생성하여 데이터베이스에 추가하고 해당 데이터를 반환한다.
get_daycal_result
오늘의 결과 데이터를 리스트 형태로 반환
오늘 날짜의 데이터가 존재하지 않을 경우 디폴트로 모든 값이 0인 기본 데이터를 생성하여 데이터베이스에 추가하고 해당 데이터를 반환한다.
화주 이름 리스트에서 화주의 인덱스를 찾아 해당 인덱스의 열을 삭제하고 화주 이름 리스트에서도 삭제
4. 화주 이름 변경
# 화주 이름 변경
def modify_owner(main_window, table_widget, name_data=None):
# 다이얼로그 위젯 생성
modify_owner_dialog = Dialog()
modify_owner_dialog.setWindowTitle('화주 이름 변경')
modify_owner_dialog.setGeometry(500, 500, 300, 50)
grid = QGridLayout()
# 화주 이름을 미리 입력받지 않은 경우(메뉴바나 툴바에서 변경액션에 접근한 경우)
org_name = QLineEdit()
if not name_data:
# 화주 이름을 입력받기 위한 다이얼로그 ui 세팅
org_name.setPlaceholderText('화주 이름')
grid.addWidget(QLabel('화주 이름: '), 0, 0, 1, 1)
grid.addWidget(org_name, 0, 1, 1, 2)
chg_name = QLineEdit()
chg_name.setPlaceholderText('바꿀 이름')
chg_name.returnPressed.connect(modify_owner_dialog.success)
submit = QPushButton('변경')
submit.clicked.connect(modify_owner_dialog.success)
grid.addWidget(QLabel('바꿀 이름: '), 1, 0, 1, 1)
grid.addWidget(chg_name, 1, 1, 1, 2)
grid.addWidget(submit, 2, 0, 1, 3)
modify_owner_dialog.setLayout(grid)
# 다이얼로그를 modal 하게 표시
modify_owner_dialog.show_modal()
# 입력이 취소된경우 (다이얼로그를 그냥 종료한 경우) 삭제절차 종료
if modify_owner_dialog.canceled:
return
# 대상 화주이름 name 과 바꿀 이름 changed 를 설정
name = name_data if name_data else org_name.text()
changed = chg_name.text()
# 화주 이름 변경
try:
target = session.query(DayCalOwner).filter(DayCalOwner.name == name).first()
target.name = changed
session.commit()
table_widget.owner_modified(name, changed)
except AttributeError:
main_window.statusBar().showMessage('>> 등록되지 않은 화주입니다.')
except DatabaseError:
main_window.statusBar().showMessage('>> 이미 등록된 화주입니다.')
session.rollback()
Dialog를 통해 변경할 화주의 이름(name)과 바꿀 이름(changed)을 입력받음
데이터베이스에서 name에 해당하는 데이터를 가져와 이름을 변경한 뒤 commit
이름 변경을 즉각 반영하기 위해 table_widget.owner_modified 실행
AttributeError 발생시 status bar에 이름을 바꾸려는 화주가 등록되지 않은 화주임을 표시
DatabaseError 발생시 status bar에 바꿀 이름이 이미 존재하는 다른 화주의 이름임을 표시
5. 화주 이름 변경 반영
# 화주 이름 변경 반영
def owner_modified(self, org_name, chg_name):
idx = self.owners.index(org_name)
self.owners[idx] = chg_name
self.input_table.setHorizontalHeaderItem(idx, QTableWidgetItem(chg_name))
# 화주 모델
class DayCalOwner(Base):
__tablename__ = 'daycal_owner'
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String, unique=True)
def __init__(self, name):
self.name = name
autoincrement 가 설정된 id 속성을 추가
primary key를 name에서 id로 변경
name을 primaryk 중복 불가능한 값으로 하기 위해 unique 설정
2. actions.py
from PySide6.QtWidgets import QLabel, QGridLayout, QPushButton, QLineEdit
from sqlalchemy.exc import DatabaseError
from sqlalchemy.orm.exc import UnmappedInstanceError
from controller.db_manager import session
from models.models import DayCalOwner
from widgets.simple import Dialog
# 화주 추가
def create_owner(main_window, table_widget, name_data=None):
# 입력받은 이름이 이미 있는 경우
if name_data:
name = name_data
# 이름을 입력받아야 하는 경우
else:
# 다이얼로그 위젯 생성
create_owner_dialog = Dialog()
create_owner_dialog.setWindowTitle('화주 추가')
create_owner_dialog.setGeometry(500, 500, 300, 50)
# 화주 이름을 입력받기 위한 다이얼로그 ui 세팅
grid = QGridLayout()
input_name = QLineEdit()
input_name.setPlaceholderText('화주 이름')
input_name.returnPressed.connect(create_owner_dialog.success)
submit = QPushButton('추가')
submit.clicked.connect(create_owner_dialog.success)
grid.addWidget(QLabel('화주 이름: '), 0, 0, 1, 1)
grid.addWidget(input_name, 0, 1, 1, 2)
grid.addWidget(submit, 1, 0, 1, 3)
create_owner_dialog.setLayout(grid)
# 다이얼로그를 modal 하게 표시
create_owner_dialog.show_modal()
# 입력이 취소된경우 (다이얼로그를 그냥 종료한 경우) 추가절차 종료
if create_owner_dialog.canceled:
return
# 다이얼로그에서 입력이 완료되면 입력받은 이름을 name 으로 설정
name = input_name.text()
# 새 화주 추가
new_owner = DayCalOwner(name)
try:
session.add(new_owner)
session.commit()
table_widget.owner_added(name)
except DatabaseError:
main_window.statusBar().showMessage('>> 이미 등록된 화주입니다.')
session.rollback()
main_window 에 구현했던 화주 추가 액션을 controller 디렉토리의 actions.py 로 이동
self 대신 메인윈도우의 인스턴스와 화주 추가 결과를 반영할 table이 위치한 위젯의 인스턴스를 인자로 받음
dialog를 통하지않고 이름을 입력받을 상황을 대비하여 name_data 인자를 추가
name_data가 입력되었다면 다이얼로그로 이름을 입력받지 않고 name_data를 name으로 설정
중복된 이름일 경우 status bar에 이미 등록된 화주임을 표시
name이 더이상 primary key가 아닌 unique key이기 때문에 이름이 중복될 경우 add 가 아닌 commit단계에서 예외가 발생한다. 그러므로 예외 발생시 이미 적용된 add 를 취소하기 위해 rollback을 호출해줘야한다.
from controller import actions
``` 생략 ```
# 화주추가 액션 추가
create_owner = QAction(QIcon('src/img/create_owner_icon.png'), '화주 추가', self)
create_owner.setShortcut('Ctrl+Shift+A')
create_owner.setStatusTip('화주 추가')
create_owner.triggered.connect(lambda: actions.create_owner(self, self.central_widget.doc_tab.tab1))
file_menu.addAction(create_owner)
tool_bar.addAction(create_owner)
``` 생략 ```
contoller.actions를 import 한 뒤 create_owner 액션의 triggered 시그널에 lambda 함수의 형태로 연결해준다.
3. simple.py
from PySide6.QtCore import QDateTime, QTimer
from PySide6.QtWidgets import QLabel, QDialog
# 시간레이블 클래스(QLabel 상속)
# 1초마다 현재 날짜/시간을 갱신하여 표시하는 레이블
class TimeLabel(QLabel):
def __init__(self):
super().__init__()
self.setText(QDateTime.currentDateTime().toString('yyyy년 MM월 dd일 ddd hh:mm:ss'))
self.timer = QTimer(self)
self.timer.timeout.connect(self.timeout)
self.timer.start(100)
def timeout(self):
self.setText(QDateTime.currentDateTime().toString('yyyy년 MM월 dd일 ddd hh:mm:ss'))
# 다이얼로그(QDialog 상속)
class Dialog(QDialog):
def __init__(self):
super().__init__()
self.canceled = True
def show_modal(self):
return super().exec_()
def success(self):
self.canceled = False
self.close()
Dialog, TimeLabel 등의 구조가 간단한 위젯들을 widgets/simple.py 에 분리
4. DayCal 위젯
# 일일 정산서 계산서 위젯
class DayCal(QWidget):
# 생성자
def __init__(self):
super().__init__()
self.owners = [q.name for q in session.query(DayCalOwner).order_by(DayCalOwner.id)]
self.num_of_owners = len(self.owners)
self.input_table = QTableWidget()
self.input_table.setParent(self)
self.init_ui()
``` 생략 ```
화주 데이터의 id를 기반으로 순서를 유지하도록 쿼리시에 order_by 를 통해 id를 기준으로 오름차순 정렬
5. create_owner 수정
# 화주 추가
def create_owner(main_window, table_widget, name_data=None):
# 입력받은 이름이 이미 있는 경우
if name_data:
name = name_data
# 이름을 입력받아야 하는 경우
else:
# 다이얼로그 위젯 생성
create_owner_dialog = Dialog()
create_owner_dialog.setWindowTitle('화주 추가')
create_owner_dialog.setGeometry(500, 500, 300, 50)
# 화주 이름을 입력받기 위한 다이얼로그 ui 세팅
grid = QGridLayout()
input_name = QLineEdit()
input_name.setPlaceholderText('화주 이름')
input_name.returnPressed.connect(create_owner_dialog.success)
submit = QPushButton('추가')
submit.clicked.connect(create_owner_dialog.success)
grid.addWidget(QLabel('화주 이름: '), 0, 0, 1, 1)
grid.addWidget(input_name, 0, 1, 1, 2)
grid.addWidget(submit, 1, 0, 1, 3)
create_owner_dialog.setLayout(grid)
# 다이얼로그를 modal 하게 표시
create_owner_dialog.show_modal()
# 입력이 취소된경우 (다이얼로그를 그냥 종료한 경우) 추가절차 종료
if create_owner_dialog.canceled:
return
# 다이얼로그에서 입력이 완료되면 입력받은 이름을 name 으로 설정
name = input_name.text()
# 새 화주 추가
new_owner = DayCalOwner(name)
try:
session.add(new_owner)
session.commit()
table_widget.owner_added(name)
except DatabaseError:
main_window.statusBar().showMessage('>> 이미 등록된 화주입니다.')
session.rollback()
이름을 입력하기 위한 위젯을 QPlainText 에서 QLineEdit 으로 변경
returnPressed 시그널을 사용하여 엔터키에도 반응하도록 수정
다이얼로그에 canceled 속성을 추가하여 단순 종료와 데이터를 정상적으로 제출한 종료를 구분
PostgreSQL을 설치했으니 이제 데이터를 저장할 DB 서버를 생성해야한다. 위의 설치과정에서 자동으로
설치됐을 PostgreSQL의 GUI툴인 pgAdmin 을 실행하여 위와 같이 server 를 우클릭하여 새 서버를 생성한다.
pgAdmin 실행시 요구하는 암호는 PostgreSQL 설치시에 입력한 슈퍼유저 계정의 암호를 입력해주면 된다.
먼저 General 탭의 Name 에는 서버의 이름을 입력해준다. 무엇으로 하든 상관없으니 원하는 이름을 입력한다.
Connection 탭에서는 호스트 이름 혹은 주소, 포트번호, 유저명, 패스워드를 입력한다. 로컬 DB로 사용할 것이기
때문에 호스트는 localhost로 해주고 포트번호는 PostgreSQL의 기본 포트번호인 5432를 그대로 사용한다.
유저명과 패스워드는 DB를 사용할 계정의 것을 입력해야한다. 여기서는 슈퍼유저 계정인 postgres를 사용한다.
모두 입력한 뒤 save를 누르면 서버가 생성된 것을 볼 수 있다.
3. sqlAlchemy 설치
sqlAlchemy는 Python에서 데이터베이스를 다루기 위한 SQL 툴킷이며 ORM(Object Relational Mapper)이다.
ORM은 데이터베이스의 데이터들을 객체의 형태로 매핑해주는 것으로 사용자가 SQL문에 의존하지 않고
객체지향적인 코드로 DB를 다룰 수 있게 해준다. sqlAlchemy 는 현재 Python 웹 프레임워크인 Flask에서도
사용되고있으며 마찬가지로 Python 웹 프레임워크인 Django 의 ORM과 함께 Python에서 가장 널리 사용되는
ORM 툴이다. 여기서는 sqlAlchemy 를 사용하여 애플리케이션을 DB와 연결해본다.
pip install sqlalchemy
설치는 매우 간단하다. 현재 개발환경인 Pycharm의 Terminal은 기본적으로 가상환경에 진입한 상태이기 때문에
터미널상에서 바로 pip 명령어를 사용하여 설치해주면 된다.
4. db_manager.py
import json
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import scoped_session, sessionmaker
# 데이터베이스 패스워드를 저장한 secrete_file.json 을 읽는다
with open('secrete_file.json') as f:
secretes = json.loads(f.read())
# postgresql 의 형식에 따라 엔진 생성
engine = create_engine('postgresql://{username}:{db_password}@{host}:{port}/{db_name}'.format(
username='postgres',
db_password=secretes['db_password'],
host='localhost',
port='5432',
db_name='postgres'
))
# DB 세션 생성
session = scoped_session(sessionmaker(autocommit=False, autoflush=False, bind=engine))
# 모델 매핑
Base = declarative_base()
Base.query = session.query_property()
# db 초기화
def init_db():
Base.metadata.create_all(engine)
controller 디렉토리에 db_manager.py 를 위와 같이 작성
secrete_file.json 에서 데이터베이스의 패스워드를 읽음
create_engine 에 postgresql의 형식에 맞춰 정보를 입력해주는 것으로 엔진을 생성
sessionmaker와 scoped_session 을 사용하여 엔진과 연결된 세션을 생성
autocommit과 autoflush를 False 로 하는 것은 여러개의 쿼리문을 적용해야할 때 한번에 commit을 실행하기 위해서이다.(flush가 발생하면 db에 실제로 삽입, 삭제 등의 연산을 수행한 상태가 된다. 다만 commit까지는 가지 않는다. commit은 내부적으로 flush를 먼저 수행한다.)
declarative_base 를 사용하여 Base를 상속하여 선언한 클래스(모델)들을 매핑해준다.
init_db를 호출하면 연결된 db에 선언한 모델들을 기반으로 테이블을 생성한다.
5. 모델 선언
from sqlalchemy import Column, Integer, String, DateTime
from controller.db_manager import Base
# 화주 모델
class DayCalOwner(Base):
__tablename__ = 'daycal_owner'
name = Column(String, primary_key=True)
def __init__(self, name):
self.name = name
# 화주별 일일정산 데이터 모델
class DayCalOwnerValues(Base):
__tablename__ = 'daycal_owner_values'
datetime = Column(DateTime, primary_key=True)
owner_name = Column(String, primary_key=True)
kd_total = Column(Integer)
kd_fare = Column(Integer)
kd_drop = Column(Integer)
match_fee5 = Column(Integer)
owner_fare = Column(Integer)
owner_drop = Column(Integer)
listing_fee4 = Column(Integer)
kd_pre = Column(Integer)
def __init__(self, datetime, owner):
self.datetime = datetime
self.owner = owner
# 일일정산서에 입력될 기타 데이터 모델
class DayCalOtherValues(Base):
__tablename__ = 'daycal_other_values'
datetime = Column(DateTime, primary_key=True)
office_deposit = Column(Integer)
kd_deposit = Column(Integer)
direct_exp = Column(Integer)
our_auc = Column(Integer)
kd_buy = Column(Integer)
def __init__(self, datetime):
self.datetime = datetime