ㅅㅇ

Python _DB : pymysql 을 이용해 mysql 연동 본문

AI_STUDY/Python

Python _DB : pymysql 을 이용해 mysql 연동

SO__OS 2022. 6. 4. 01:29

플레이데이터 빅데이터캠프 공부 내용 _ 6/3

Python _DB  : pymysql을 이용해 mysql 연동

1.  pymysql

: 파이썬에서 MySQL, MariaDB DBMS와 연동하는 다양한 함수를 제공하는 모듈

   - Python DB API 2.0 표준을 따른다.

   db 마다 연결하는(전송하는) 방식이 다르다. 그러나, 연결한다라는 것은 즉, 하는 일은 동일하다.

   이는 Python [DB API 2.0] 표준에 따르기 때문이다.

   그렇기에, 모듈과 CONNECT 내 구문 등 달라질 수 도 있지만(DB연결방법에는 차이가 있으니),

   CONNECT, CURSOR, CLOSE ... 등 이는 표준에 따르기에 모든 DBMS 공통이다.

         - 오라클용 py 라이브러리는 xOracle. 이걸 다운받아 사용해야 하지만   

           기본적으로 pymysql이든 오라클용이든 사용법(함수...)은 동일한 것.

2. pymysql 설치(jupyter notebook 내)

쥬피터 노트북 환경에서의 사용을 위해 pymysql을 pip 설치 해준다.

 

- 조건
    - python version 3.6 이상
    - mysql version 5.6 이상
- 설치
    - `pip install PyMySQL`
    - `conda install -y -c conda-forge pymysql` #conda로 설치가 되지만, -y 붙어줘야함. conda가 더 느림.

 

- 쥬피터에서 pymysql 설치

# pymysql 설치는 느낌표없이도 깔리긴 하지만, 원래 넣는 게 맞음. 

! pip install pymysql

 

** 참고) 쥬피터 내 터미널 명령어 실행법

# ! 느낌표를 앞에두면 터미널 명령어를 실행 가능함.
! dir

3. 기본 작성 절차

 < 연결하기 >
1) Database 연결
- connect() 연결해주기

    - port 번호 기본값: 3306

- import 해줘야 한다.

import pymysql

# DB 연결 - connect() => 연결 실패 시 Exception 발생.

connection =  pymysql.connect(host="DBMS 서버 ip", 
                             port="port번호", 
                             user="계정명", 
                             password="비밀번호", 
                             db="연결할데이터베이스이름", 
                             charset='utf8')


 < 데이터주고받고 >
2) Connection을 이용해 Cursor 생성
    - Cursor(): 연결된 Database에 sql문을 전송하고 select결과 조회 기능을 제공하는 객체

# Cursor를 connection으로부터 얻어온다.
cursor = connection.cursor()


3) Cusror를 이용해 SQL문 전송

 execute : dms로 쿼리문을 전송하는 메소드
    cursor.execute("select문") 

 

- sql 문 실행함.

       - 만약, 쿼리문 실행 실패를 한다면 (sql문을 잘못 짠 경우. 제약조건 문제.. ) - > Exception 발생할 수 있다.

cursor.execute("sql문")

- mysql 쿼리 구문 그대로 " " 따옴표 안에 써주면 된다. 

 

 

 

<연결 닫기>  
4) 연결 닫기
    - cursor. connection 연결을 닫는다.
    - 보통, except-finally 이나 with문을 이용하여 구현.

cursor.close()
connection.close()

 

___ 구체적 예제로 설명 (사실 위 그대로 작성하는 것보다는 아래 with문 예제처럼 작성한다.)____

3.1 테이블 생성

- 참고) pymysql을 이용해서는 보통 DML(insert, update, delete, update)문을 실행한다.
            DDL도 할 수 있지만, 거의 실행하지 않는다. 직접 DBMS로 해주기

# 1 import
import pymysql

# 2 DB 연결 - connect() => 연결 실패 시 Exception 발생.
# 지금은 로컬 ip로 하자.
connection = pymysql.connect(host='127.0.0.1',
                            port = 3306,
                            user = '-',
                            password = '-',
                            db = 'testdb'
                            )
# 3 Cursor를 connection으로부터 얻어온다. 
cursor = connection.cursor()
# 4 sql 문은 string을 정의. execute() 메소드를 이용해서 db에 전송.
sql = """
create table test_user(
    id int auto_increment primary key,
    name varchar(30) not null,
    email varchar(100) not null unique,
    tall decimal(5,2),
    birthday date
)
"""
# 4 실행. sql문 보냄.
cursor.execute(sql) 
# 5 연결닫기 - cursor, connection close
cursor.close()
connection.close()

 

3.2 DML _ insert 예시

3.2.1 구문 _ 예외처리 & with문 & 함수 처리(매개변수 받아서)

1) 예외처리 try - except - finally 작성

 

import pymysql 


sql = "insert into test_user (name, email, tall, birthday) values ('홍길동','hong@a.com', 180.23, '2000-12-23')"
try:
    # 연결
    connection = pymysql.connect(host='127.0.0.1', port=3306, user='-', password='-', db='testdb', charset='utf8')
    # cursor 생성
    cursor = connection.cursor()
    # insert문 실행. 확인출력 메시지를 위해 결과를 cnt 변수에 넣어줌. 
    cnt = cursor.execute(sql)
    
    # 항상 commit을 안 하면 처리가 안 됨.
    connection.commit()
    
     # 결과를 반환
    print(f"{cnt} 행이 insert 되었습니다.")
    
except Exception as e:
    if connection: # exception 예외 발생 - rollback 원상복구
        connection.rollback() 
    print(e)
finally:
    # 연결 닫기 반드시 해야 함. 
    if cursor:  # != None : cursor가 None이 아니라면 연결을 닫아라.
        cursor.close()
    if connection: # != None : connection이 연결되어 있다면 close해라.
        connection.close()

 

 -  insert/delete/update 문 실행 후 commit 처리를 꼭 해야 한다.

    auto commit = False   =>  commit이 자동으로 안 되기에 우리가 처리해준다. 또, 자동이 아니기에 rollback 이 가능하다. 

 

 - exception 예외 발생 처리 : rollback 원상복구

       -  현구문에서 오류로 인해 commit 되기 전에 finally로 간다. 즉,  commit을 안 하기 때문에 dbms로 안 가긴 함.

          그리고 쿼리문이 하나라 지금은 rollback을 할 필요가 없지만, 습관적으로 이것도 작성해주자.

 

 - finally : 에러가 발생했어도 연결을 열었으면 연결 닫기가 필수적으로 이뤄져야 한다.

    보통, 이 처리를 위해 본 방법보다는 아래의  with문 방법을 이용해서 작성한다.

 

2) with문 작성

- with 연결함수 as 변수

sql = "insert into test_user (name, email, tall, birthday) values ('홍길동2','hong222@a.com', 150.23, '2003-12-23')"


with pymysql.connect(host='127.0.0.1',port=3306, user='-', password='-', db='testdb', charset='utf8') as connection:
    with connection.cursor() as cursor:
        cnt = cursor.execute(sql)
        print(f'{cnt} 행이 insert됨')
        connection.commit() # 항상 commit
        
# 여기서도 여러 쿼리문이라면 rollback 해줘야 함.

 

3) with문 _ 함수 처리  작성

 

def insert_user(name, email, tall, birthday):
    sql = f"insert into test_user (name, email, tall, birthday) values ('{name}','{email}', '{tall}', '{birthday}')"
                                                # values 데이터 값을 매개변수로 넣을 때' ' 로 묶어줘야 한다.

    with pymysql.connect(host='127.0.0.1',port=3306, user='-', password='-', db='testdb', charset='utf8') as connection:
        with connection.cursor() as cursor:
            cnt = cursor.execute(sql)
            print(f'{cnt} 행이 insert됨')
            connection.commit() # 항상 commit

insert_user('이순신','lee@a.com',190.20,'1955-11-11')​

 

- mysql연동 프로그램을 전역함수로 만들 수 없는 이유는 연결을 항상 끊는 작업이 필요해서이다.  
  그렇기에 함수로 만들어 재사용을 한다. 함수가 끝나면 무조건 연결끊기가 처리되도록 작성해야 한다. => with 문

 

- values ('{name}','{email}', '{tall}', '{birthday}')"
    =>  values 데이터 값을 매개변수로 넣을 때' ' 로 묶어줘야 한다.

          이게 매우 귀찮기 때문에 우린 보통 아래의 parameterized query 방법을 사용한다.

 

3.2.2 구문 _  Parameterized Query

:  SQL 문에서 컬럼 값이 들어가는 자리에 값대신 `%s` placeholder를 사용한뒤

    execute()에서 placeholder에 넣을 값을 tuple로 제공한다.

   - query문을 쉽게 작성할 수 있는 장점이 있다.  

     나중에 값 들어갈거야를 말해줌. 나중에 들어갈 때 '   ' 따옴표 알아서 들어가서 편하다. 위처럼 일일이 붙일 필요 없음.       - 모든 문에서 다 사용 가능.

 

1) 기본예제

 

( ex1 ) Parameterized Query을 이용해 데이터 삽입 _ with문

from datetime import date 

sql = 'insert into test_user(name, email, tall, birthday) values (%s, %s,%s,%s)'
with pymysql.connect(host='127.0.0.1',port=3306, user='-', password='-', db='testdb', charset='utf8') as connection:
    with connection.cursor() as cursor:
    	# 날짜 시간 데이터 - 문자열로 ' '
        cnt = cursor.execute(sql, ('유관순', 'yo331@a.com', 173.23, '2000-02-12')) # 튜플로 넣어줌. 타입에 맞춰 값만 넣어주면 됨. 
         # 날짜 시간 데이터 - date() 이용
        cnt2 = cursor.execute(sql, ('강감찬', 'dfdf@a.com', 180.3, date(1955,2,3)))
        print(f'{cnt+cnt2} 행이 insert됨')
        connection.commit() # 항상 commit을 안 하면 처리가 안 됨.

- 튜플로 넣어야 한다. 원소 하나 일 때    ,   넣어주기

   cursor.execute(delete_sql, (4, )) 

 

- sql 쿼리문 작성 방식

   - sql 쿼리문에서 문자열은 '  ' 작은 따옴표로 감싸줘야 한다. 

   -  date 데이터 타입

            - 문자열 방식  ' 2022-12-12'
            - date(2022,12,12) 이용

                       - 알아서 타입에 맞춰서 들어감.

                       - 그리고 select 조회 시 date 타입으로 받는다.

            - date.today()   # 현 날짜 반환

 

(참고)  datetime 모듈

          : 날짜 -date, 시간-time, 날짜시간 - datetime 등 날짜와 시간을 다루는 모듈

             -> DB의 날짜/시간 타입과 연동.

 

2) Parameterized Query를 이용해 여러개 행 insert

 

   ( ex1 ) for문 사용

- 0, 1, 2 인덱스 별로 데이터 정보 적어주고 for문 zip() 함수 사용

names = ['이름1', '이름2','이름3']
emails = ['e1@a.com', 'e2@a.com', 'e3@a.com']
talls = [192.3,149.2, 150.6]
birthdays = ['2000-10-10', date.today(), '1999-02-02']

sql = 'insert into test_user(name, email, tall, birthday) values (%s, %s,%s,%s)'
with pymysql.connect(host='127.0.0.1',port=3306, user='-', password='-', db='testdb', charset='utf8') as connection:
    with connection.cursor() as cursor:
        for data in zip(names, emails, talls, birthdays):
            cursor.execute(sql,data)
            
        connection.commit()

 

- 이 방법보다는 executemamy() 훨씬 편하다.

 

   ( ex2 )  executemany() 사용

- 아래 3.2 select에서 더 자세히 배울 것이다.

- insert할 값들을 가진 리스트를 넣어 한번에 여러 행을 insert한다.
- excute를 여러번 할 거야.

- 0, 1, 2 인덱스 별로 데이터 정보 적어주고 executemany(sql, datas)

datas = [
    ['name1', 'a31@a.com', 172.2, '2000-12-11'],
    ['name2', 'b13@a.com', 193.2, '1999-12-09'],
    ['name3', 'c31@a.com', 163.2, '1998-01-09'],    
    
]
sql = 'insert into test_user (name, email, tall, birthday) values (%s,%s,%s,%s)'
with pymysql.connect(host='127.0.0.1', port=3306, user='-', password='-', db='testdb', charset='utf8') as connection:
    with connection.cursor() as cursor:

        cursor.executemany(sql, datas) # 내부적으로 반복돌면서 알아서 넣어짐.
        connection.commit()

- datas에 리스트, 튜플 둘 다 넣을 수 있다.

 

3.2.3 기타 예제

ex) 'Null을 허용하는 컬럼, default값이 있는 컬럼'들을 '기본값이 있는 매개변수'로 선언

      insert 에서 아직 정보가 없는 또는 모르는 데이터에 해당하는 컬럼에 default None 을 설정했다.

      이렇게 기본값을 주면 일일이 함수 호출할 때 None 값을 안 써도 된다.

      - 주의. 디폴트값 있는 애들 뒤에 디폴트값 없는 애를 적을 수 없다. 

                 없는 애들 먼저 써주고, 있는 애들 써주기.

# 1개의 행을 emp 테이블에 insert하는 함수 - 호출하는 값 insert

def insert_emp(emp_id, emp_name, job, hire_date, mgr_id=None,  salary=None, comm_pct=None, dept_name=None):
    sql = "insert into emp values (%s, %s, %s, %s, %s, %s, %s,%s)"
    with pymysql.connect(host='127.0.0.1',port=3306, user='-', password='-', db ='hr', charset='utf8') as connection:
        with connection.cursor() as cursor:
            cnt = cursor.execute(sql,(emp_id, emp_name, job, mgr_id, hire_date, salary, comm_pct, dept_name)) # 주의. hr emp 테이블 틀 순서대로
            print(cnt)
            connection.commit()

# 넣어준 대로 들어감.
insert_emp(402, '홍길동', 'IT_PROG', '2010-12-12', 103, 3500, 0.2, '기획부')
# 안 넣어준 컬럼이 있다. 이때, 기본값 None 들어감.
insert_emp(401, '박영수', '구매', '2000-11-21') # 안 넣어준 컬럼에는 기본값 None 들어감.

3.3 DML _ update & delete 예시

- 코딩 절차는 insert 와 동일

 

ex1 ) update 데이터 수정 _ 특정 컬럼 데이터 수정

# tall 컬럼 데이터 모두 +10 , birthday 모두 변경 

update_sql = "update test_user set tall = tall +10, birthday = '2000-01-01'"
with pymysql.connect(host='127.0.0.1', port=3306, user='-', password='-', db='testdb', charset='utf8') as connection:
    with connection.cursor() as cursor:
        cnt = cursor.execute(update_sql) 
        connection.commit()
        print(f'{cnt}행이 update 됨')

 

ex2 ) update 데이터 수정 _ 함수_ 특정 id 의 user 정보를 변경( 해당 id와 바꿀 값들 모두 매개변수로 입력 받음.)

호출 : 안바뀌는 값, 바뀐 값 모두 argument로 전달.

   - 실제로도 where절 조건 데이터 값, 바꿀 값들 모두 인수로 받아 매개변수에 넣어진다.

   - 쿼리문 %s 으로 할 때 매개변수 잘 넣어주기.

# 특정 id 의 user 정보를 변경 

def update_user_by_id(id, name, email, tall, birthday):
    update_sql = 'update test_user set name=%s, email=%s, tall=%s, birthday = %s where id = %s'
    with pymysql.connect(host='127.0.0.1', port=3306, user='-', password='-', db='testdb', charset='utf8') as connection:
        with connection.cursor() as cursor:
            cnt = cursor.execute(update_sql, (name, email, tall, birthday, id)) # 순서 헷갈리지 말기
            connection.commit()
            return cnt
update_user_by_id(1, '홍길동', 'hhh@abd.com', 192.23, '2000-10-10')

 

ex3 ) delete 데이터 삭제 

- 삭제할 데이터가 이미 없다면 0 출력. 에러 발생하는 건 아니다.

def delete_user_by_id(id):
    delete_sql = 'delete from test_user where id = %s'
    with pymysql.connect(host='127.0.0.1', port=3306, user='-', password='-', db='testdb', charset='utf8') as connection:
        with connection.cursor() as cursor:
            cnt = cursor.execute(delete_sql, (id, )) # 튜플로 만들어야 하기에. 
            print(cnt) 
            connection.commit()
            return cnt
            
delete_user_by_id(2)

3.4 DQL _ Select 예시

: 조회결과 조회

- DQL(Data Query Language) : Select가 DML에 포함되긴 하지만 정확히 말하면 DQL이다.

- 데이터 없으면 None 반환

 

- execute : dms로 쿼리문을 전송하는 메소드
    - cursor.execute("select문") 실행 후 

      cursor의 결과 조회 메소드(fetch메소드)를 이용해 결과를 받는다.
    - 결과를 받는 것도 cursor로부터 들고 온다.


- fetch메소드 (cursor에 있는 메소드)
   (1) cursor.fetchall()
        - 조회한 모든 행을을 반환
   (2) cursor.fetchmany(size=개수)
        - 지정한 size개수 만큼 반환
    (3) cursor.fetchone()
        - 조회결과 중 첫번째 행만 반환(한 행만.)
        - 주로 pk 동등 조건으로 조회한 경우 사용


- 제공 방식
    - 튜플로 받는 방법 : 하나의 데이터 결과(한 행)를 tuple로
    - 딕셔너리로 받는 방법 : 하나의 데이터 결과(한 행)를 dict로

      조회 결과를 didtionary 로 반환하는 커서   (connect 문에 아래 구문 추가.)

           cursorclass=pymysql.cursors.DictCursor   # 안 써주면  tuple로 

    - 둘 다 많이 쓰임

 

 

3.4.1 fetchall()

 

(ex. 1)  fetchall()_ 튜플로 받는 방법

import pymysql

sql = 'select * from test_user'
with pymysql.connect(host='127.0.0.1', port=3306, user='-', password='-', db='testdb', charset='utf8') as connection:
    with connection.cursor() as cursor:
        print(type(cursor))
        # select 문 실행
        cursor.execute(sql)
        # select 결과 조회 (cursor.fetchXXX() 메소드 이용)
        result = cursor.fetchall()

 

- execute 후 cursor.fetachall() 조회로 받은 데이트를 result 변수에 담음.

print(type(result))           # <class 'tuple'> 
print(type(result[0]))        # <class 'tuple'>

 

- 전체 테이터도 튜플로 묶고, 데이터별도 튜플로 묶어서 보낸다.

   => fetchall() 의 결과는 튜플 안에 튜플로 묶인 한 개의 데이터로 나온다.
        그리고 한 데이터 (한 행) 안에는 속성(컬럼)이 여러 개이다.

        그렇기에 한 데이터 별(한 행별로)로 묶을 필요가 있다.

        n개에 대한 각 데이터별로 튜플로 묶고 쿼리문으로 조회받은 데이터 하나 자체도 튜플로 전체를 묶어준다.

((1, '홍길동', 'hhh@abd.com', Decimal('192.23'), datetime.date(2000, 10, 10)),
 (8, '유관순', 'yo11@a.com', Decimal('193.23'), datetime.date(2000, 1, 1)))

 

(ex. 2)  fetchall() _ 딕셔너리로 받는 방법

cursorclass = pymysql.cursors.DictCursor   추가.

   =>  조회 결과를 didtionary 로 반환하는 커서.

sql =select * from test_user
with pymysql.connect(host='127.0.0.1', 
                    port=3306, user='-', 
                    password='-', 
                    db='testdb', 
                    charset='utf8', 
                    cursorclass=pymysql.cursors.DictCursor) 
                    as connection:
                    
    with connection.cursor() as cursor:
        # select 문 실행
        cursor.execute(sql)
        result = cursor.fetchall()

 

 

- dict :   key 컬럼명, value 컬럼값.    # 별칭 안 넣어서 db table 컬럼명 그대로

- result 변수에 담아 원하는 정보 인덱스명으로 이용해 받기

print(type(result,), len(result))  # <class 'list'>    2
print(type(result[0]))             # <class 'dict'>

# result 변수에 담아 원하는 정보 인덱스를 이용해 받기
result[0]['id'], result[0]['email']

 

- 전체 데이터는 리스트, 리스트 내 데이터 별 원소는 dict 으로 묶는다.

[{'id': 1, 'name': '홍길동', 'email': 'hhh@abd.com', 'tall': Decimal('192.23'), 'birthday': datetime.date(2000, 10, 10)},
 {'id': 8, 'name': '유관순', 'email': 'yo11@a.com', 'tall': Decimal('193.23'), 'birthday': datetime.date(2000, 1, 1)}]

 

 

3.4.2 fetchone()

: 특정 데이터 특정 행 하나의 행을 보고 싶을 때

 

데이터가 n개일 때 튜플로 전체를 묶는 건데, 여기서는 한 데이터를 반환한다.
그렇다면 굳이 전체 튜플로 묶을 필요가 있는가.

받은 데이터 처리 시 중복리스트 인덱스 형식으로 값 처리해줘야 하고 불편할 것이다.

이렇게 한 데이터 한 행만을 볼 때 쓰는 게 fetchone()

 

ex)  fetchone()

# pk로 조회. - 조회결과 0행(없으면) 또는 1행(데이터 있으면)
# 조회 결과가 없으면 None을, 있으면 1개의 데이터를 반환

sql = 'select * from test_user where id = %s'
with pymysql.connect(host='127.0.0.1', port=3306, user='-', password='-', db='testdb', charset='utf8') as connection:
    with connection.cursor() as cursor:
        cursor.execute(sql, 1)
        result = cursor.fetchone()
        
print(len(result)) # 5   데이터 내 5개 속성

print(result)
(1, '홍길동', 'hhh@abd.com', Decimal('192.23'), datetime.date(2000, 10, 10))

- 만약 여러 행을 불러모아도 제일 첫 번째 행만을 반환한다.

 

3.4.3 fetchmany()

- fetchall로 하니 너무 많다. 갯수를 정해 그 행만 일부분만 가져오고싶을 때 사용.
- 많이 쓰지는 않음

   cursor.fetchmany(size =  원하는 크기정수로 )

 

ex)  fetchmany() 

sql = "select * from test_user"
with pymysql.connect(host='127.0.0.1', port=3306, user='-', password='-', db='testdb', charset='utf8') as connection:
    with connection.cursor() as cursor:
        cursor.execute(sql)
        result1 = cursor.fetchmany(size = 3)
print(result1)

- 정렬대로 3개 행 반환

 

3.4.4 기타 예제

 

ex)  'cursor 는 iterable 타입'

for문에 select 실행한 cursor를 사용하면 조회결과를 한 행씩 조회할 수 있다.

sql = "select * from test_user"
with pymysql.connect(host='127.0.0.1', port=3306, user='-', password='-', db='testdb', charset='utf8') as connection:
    with connection.cursor() as cursor:
        cursor.execute(sql)
        for data in cursor: # 한번 반복시 마다 조회결과를 한행씩 반환
            print(data)

 

ex) db 쿼리문 작성 문법과 동일하게 쥬피터에서도 작성해주면 된다.

# salary 범위를 받아서 그 범위의 salary를 받는 직원들을 조회한 결과를 반환하는 함수

def select_emp4(maxsalary,minsalary):
    sql = "select * from emp where salary between %s and %s"
    with pymysql.connect(host='127.0.0.1',port=3306, user='-', password='-',db='hr',charset='utf8') as connection:
        with connection.cursor() as cursor:
            cursor.execute(sql,(maxsalary,minsalary))
            result4 = cursor.fetchall()
            return result4
R = select_emp4(6000,8000)
for data in R:
    print(f"이름: {data[1]}, 월급 : {data[5]}")

 

ex) group by, 집계함수

# 부서별 급여 통계정보(합계, 평균, 최대, 최소)를 반환하는 함수

def select_emp5():
    sql = "select dept_name, sum(salary) 합계, avg(salary) 평균, max(salary) 최대급여, min(salary) 최소급여 from emp group by dept_name"
    with pymysql.connect(host='127.0.0.1',port=3306, user='-', password='-',db='hr',charset='utf8', cursorclass=pymysql.cursors.DictCursor) as connection:
        with connection.cursor() as cursor:
            cursor.execute(sql)
            result5 = cursor.fetchall()
            return result5
x = select_emp5()
print(x)