SQLAlchemy Classical Mappings

안녕하세요. 오늘은 SQLAlchemy의 Mapping에 대해서 이야기 해볼게요. 우리가 MVC로 API를 작성할때 Database에 접근하려면 보통 Model을 사용하지요. SQLAlchemy는 Model을 정의하고 해당 모델을 Table과 Mapping할수 있는 여러가지 방법을 제공하고 있습니다. 이번 시간에는 그중 mapper()함수를 이용한 Classical Mappings에 대해서 이야기해볼게요.

일단 mapper()함수를 사용하지 않고 Model을 만들려면 아래와같이 클래스안에 Table명과 Colum들을 상단에 우선 정의한 후에 해당 Model에서 추가적으로 처리하고 싶은 로직들을 클래스안에 구현하면 됩니다.

from sqlalchemy import Column, Integer, String

class User(Base):
    __tablename__ = 'user'

    id = Column(Integer, primary_key=True)
    name = Column(String)
    fullname = Column(String)
    password = Column(String)

    def __init__(self, name, fullname, password):
        self.name = name
        self.fullname = fullname
        self.password = password

    def __repr__(self):
        return "<User('%s', '%s', '%s')>" % (self.name, self.fullname, self.password)

물론 이렇게 해도 아무런 문제가 되지 않습니다만, Model에는 로지컬한 부분만 남기고 Database Table에 연결되는 항목들만 따로 Mapping을 시켜준다면 아무래도 코드도 간결해지고 보기에도 편해지겠죠.

그래서 제공하는것이 바로 mapper()함수를 이용하는 Classical Mappings방식입니다. 우선 가장먼저 해야할일은 Database의 Table을 코드에 기술하는 것입니다.

from sqlalchemy import Table, MetaData, Column, Integer, String, ForeignKey
metadata = MetaData()
user = Table('user', metadata,
            Column('id', Integer, primary_key=True),
            Column('name', String(50)),
            Column('fullname', String(50)),
            Column('nickname', String(12))
        )

Table을 기술하려면, sqlalchemy모듈에서 Table이라는 함수를 import하셔야합니다. 그리고 Table()함수의 가장 첫 인자로는 Database의 Table명을 적어주시고, 다음으로는 MetaData 클래스를 구현한 metadata객체를 넘겨줍니다. 그리고 그 뒤로 칼럼들을 선언하여 나열하면되는데 이때 칼럼들이 metadata객체의 일부로 저장되어 user에 들어가게 됩니다. Table의 정의가 완료되었다면 그 다음으로는 User Model을 아래와 같이 정의해주세요.

class User(object):
    def __init__(self, name, fullname, nickname):
        self.name = name
        self.fullname = fullname
        self.nickname = nickname

그리고 나서 두개를 아래와 같이 mapper()함수를 이용하여 Mapping해주면 끝입니다. 아래는 완성된 코드입니다.

from sqlalchemy import Table, MetaData, Column, Integer, String, ForeignKey
from sqlalchemy.orm import mapper

metadata = MetaData()

user = Table('user', metadata,
            Column('id', Integer, primary_key=True),
            Column('name', String(50)),
            Column('fullname', String(50)),
            Column('nickname', String(12))
        )

class User(object):
    def __init__(self, name, fullname, nickname):
        self.name = name
        self.fullname = fullname
        self.nickname = nickname

mapper(User, user)

이렇게 해주면 쿼리할때 User Model을 넘겨주면 user테이블과 mapping된 Column에 각각 값이 적용이 되게 되는것이지요. 그걸 전문용어로는 ORM이라고 합니다. 결국 mapper()를 호출하면 User Model클래스에 테이블명과 칼럼들이 자동으로 들어가는거에요. 마치 가장 처음에 mapper를 사용하지 않고 그냥 정의했을때처럼 말이죠. 그냥 mapper를 사용하지 않고 사용하는게 더 쉽고 편하다고요? ㅎㅎ 그래도 클래스하나에 다 때려넣는것보다 이렇게 나눠서 보니까 더 간결한것 같지 않나요? 조금이나마 도움이 되셨기를 바래요^^

Source: https://docs.sqlalchemy.org/en/13/orm/mapping_styles.html#classical-mapping

MySQL 기본명령어

DDL

Informative

SHOW CREATE TABLE 테이블명;

Management

ALTER TABLE 테이블명 MODIFY 칼럼명 데이터타입(사이즈) NULL;

DML

Insert

INSERT INTO 타겟테이블 칼럼1,칼럼2
SELECT 칼럼1,칼럼2 FROM 소스테이블
WHERE 조건;

Etc

내가 어떤 권한을 가지고 있는지 확인할때

show grants;

Export/Import MySQL in Docker Container

Export

Method #1: Export in the Container

# 컨테이너에 접속해서
docker exec -it 컨테이너ID sh

# Export를 실행하고
mysqldump -h127.0.0.1 -p비번 데이타베이스명 테이블명 > /tmp/테이블명.sql

# 저장된 파일 확인
ls -al /tmp

# 컨테이너 안의 파일 바깥으로 가지고 나오기
docker cp 컨테이너ID:/tmp/테이블명.sql /tmp

Method #2: All in One Command

아래의 명령어 하나로 위의 4개 명령어를 해결할수 있다.

docker exec -it 컨테이너ID mysqldump -h127.0.0.1 -p비번 데이타베이스명 테이블명 > /tmp/테이블명.sql

Import

# 컨테이너에 파일 복사하기
docker cp /tmp/테이블명.sql 컨테이너ID:/tmp

# 컨테이너에 접속해서
docker exec -it 컨테이너ID sh

# SQL스크립트 실행
mysql -h127.0.0.1 -p비번 데이타베이스명 < /tmp/테이블명.sql

# 에러가 나도 다음거 실행
mysql -h127.0.0.1 -p비번 -f -D데이타베이스명 < /tmp/테이블명.sql

Kafka command-line tool

reference: https://kafka.apache.org/quickstart

Download Kafka

download the Kafka client at https://www.apache.org/dyn/closer.cgi?path=/kafka/2.1.0/kafka_2.11-2.1.0.tgz and unzip the folder

cd kafka_2.11-2.1.0/bin

Run Kafka commands

Topics

All topics

$ ./kafka-topics.sh --bootstrap-server kafka-name.hostname.com:9092 --list
__consumer_offsets
_schemas
service
   service.database.table
...

topic detail

$ ./kafka-topics.sh --describe --bootstrap-server kafka-name.hostname.com:9092 --topic topicname
Topic:botr.botr.media PartitionCount:1 ReplicationFactor:3 Configs:min.compaction.lag.ms=604800000,min.insync.replicas=2,min.cleanable.dirty.ratio=0.05,delete.retention.ms=604800000,cleanup.policy=compact
Topic: botr.botr.media Partition: 0 Leader: 2 Replicas: 2,1,0 Isr: 1,0,2

Consumers

list of consumers

$ ./kafka-consumer-groups.sh --bootstrap-server kafka-name.hostname.com:9092 --list
consumer-0
consumer-1
consumer-2
...

Show consumer detail

./kafka-consumer-groups.sh --bootstrap-server kafka-name.hostname.com:9092 --describe --group kafka-group-name

Messages

Show messages from topic

./kafka-console-consumer.sh --bootstrap-server kafka-name.hostname.com:9092 --topic kafka.topic.name

show all messages

./kafka-console-consumer.sh --bootstrap-server kafka-name.hostname.com:9092 --topic kafka.topic.name --from-beginning

Command-Line for Mac

# 파일비교
diff file1 file2

# 파일라인개수
wc -l 파일명

# 파일쪼개기
split -l 10000 원본파일.csv PREFIX

# 단체로 extension붙이기
for f in PREFIX*; do echo mv "$f" "$f.csv"; done

# merge 2 files
cat file1 file2 > merged_file

# 파일찾기 - 여러파일안에서 text찾기 
find . -type f -exec grep -l "찾는 문자열" {} \;

# 특정파일안에 text찾아서 바꾸기
grep -rl 찾는단어 파일명 | xargs sed -i '' 's/찾는단어/바꿀단어/g'

# 특정파일에서 검색단어가 처음나오는 라인과 다음라인을 함께 보여주는데
# 결과 내용이 너무 기니까 해당 문자열을 구분자로 나누어 첫번째 필드만 보여줘라
grep -m 1 -C 1 "검색어" 파일명 | cut -d "구분자" -f 1

# 특정파일의 마지막 라인의 첫번째 필드만 보기
tail -n 1 파일명 | cut -d "구분자" -f 1

# 파일의 encoding 보기
file -b --mime-encoding test.txt

# 파일의 encoding 변경하기
iconv -f UTF-16 -t ASCII 34.txt > 34_a.txt
mv 34_a.txt 34.txt

Elasticsearch Template

아시다시피 엘라스틱서치는 미리 스키마를 정의할 필요없이 데이타를 입력함과 동시에 알아서 스키마를 자동으로 정의해줍니다. 하지만 자동으로 만들어진 스키마는 데이타에 따라서 달라질수 있고, 이 경우 검색결과의 consistency를 보장해주지 않습니다. 그래서 우리가 데이타를 입력하기 전에 인덱스의 mapping을 미리 정의해 주는데요, 만약에 만들어야하는 인덱스가 여러개라면 예를 들어 버젼별로 있거나 한다면, 인덱스를 사용하기 전에 매번 매핑을 만들어주는 번거로움이 있습니다. 그래서 이럴때 사용하라고 Template이라는게 있는데요. Template은 인덱스의 스키마를 미리 정의해놓고 나중에 데이타가 들어오면 자동으로 Template에 정의된 스키마를 가지고 인덱스를 생성한 후에 데이타를 입력하라고 정의해놓는거에요. 아래의 예를 보시면:

curl --location --request PUT 'http://localhost:9200/_template/blog' \
--header 'content-type: application/json; charset=UTF-8' \
--data-raw '{
  "template" : "blog*",
    "mappings": {
        "media": {
            "properties": {
                "id": {
                    "type": "keyword"
                },
                "title": {
                    "type": "text"
                },
                "description": {
                    "type": "text"
                }
            },
            "dynamic": "strict"
        }
    }
}'

위의 명령어는 blog라는 이름의 template을 만드는데 이 template은 인덱스 이름이 blog로 시작하는 존재하지 않는 인덱스에 데이타를 넣으려고 시도할때, 해당 template의 조건에 맞기때문에 스키마를 임의로 만들지 않고 template에 정의된 인덱스를 만든 후에 데이타를 입력합니다. 아래는 만들어진 template을 확인하는 명령어 입니다.

curl --location --request GET 'http://localhost:9200/_template/blog'

References:

  • https://oboki.net/workspace/bigdata/elasticsearch/elasticsearch-index-template/
  • https://www.elastic.co/guide/en/elasticsearch/guide/2.x/index-templates.html#index-templates
  • https://elasticsearch-dsl.readthedocs.io/en/latest/persistence.html

1. Two Sum

Given an array of integers, return indices of the two numbers such that they add up to a specific target.

You may assume that each input would have exactly one solution, and you may not use the same element twice.

Example:
Given nums = [2, 7, 11, 15], target = 9,

Because nums[0] + nums[1] = 2 + 7 = 9,
return [0, 1].

문제

정수가 담긴 하나의 배열이 있습니다. 그리고 두개의 정수를 합한 어떤 정수 하나가 주어졌어요. 주어진 정수는 배열 안의 숫자 두개를 더해서 나온 값이에요. 배열안에 오직 하나의 정답이 존재하고, 배열방의 값은 단 한번만 사용할수 있다고 가정할때, 어떤 방의 값들이 더해져서 주어진 정수가 나온건지 해당하는 배열방의 번호를 두개 반환하시오.

풀이

일단 문제가 나오면 서두르지 말고, 생각을 정리하세요. 문제를 정확하게 이해해야 풀이도 가능한거랍니다. 그리고 생각이 정리되면 수도코드로 코드를 대충 짜보세요. 이때는 코드의 정확성은 중요하지 않고, 알고리즘이 옳은 방향인지를 가늠해보는 단계이기 때문에 에러가 날것등은 신경쓰지 않고 일단 생각을 자유롭게 펼쳐봅니다.

그런데 오히려 맨땅에 헤딩하는 것보다 기본 템플릿을 깔아 놓고 시작하면 면접장에서 좀더 마음이 편안하고 막연할때 뭐라도 코드를 떼서 불안한 마음을 정돈하고 코딩에 집중할수 있는 바탕을 깔아놓습니다.

def main():

if __name__ == '__main__':
    main()

그뒤로 본격적인 코딩에 앞서 가장먼저 생각 해야할것은 클래스나 함수의 구조입니다. 큰 틀을 우선적으로 잡아놓고 그 안에서 디테일을 하나씩 쳐나가는 거죠. 이 문제의 경우는 검색할 배열과 주어진 정수를 인자로 받는 함수를 하나 선언합니다. 아직 안에 어떤 로직을 넣어야할지는 모르겠지만 우선 그런 함수를 만들겠다는 생각을 하고 있다는걸 보여주는거죠. 그리고 예제로 사용할 배열방과 정수값을 임의로 하나 만듭니다. 막연히 함수를 만드는것보다 예제값을 머리속에 대입해보면서 코딩을 하면 좀더 구체적으로 로직을 떠올릴수 있거든요.

def twoSum(nums, target):

def main():
  nums = [2, 7, 11, 15]
  target = 9
  twoSum(nums, target)

if __name__ == '__main__':
  main()

그럼 이제 본격적으로 알고리즘을 만들어 볼게요. 일단 무식하게 풀어보면 배열방을 돌면서 하나씩 다 더해보면 되지 않을까요? 주어진 배열과 정수를 변수에 담고, for문을 돌면서 확인합니다. 저는 간단하게 이렇게 시작해봤어요. 두개의 배열을 교차로 돌다가 더해보고 값이 나오면 출력한뒤 빠져나오는 거죠.

def twoSum(nums, target):
  for num1 as nums:
    for num2 as nums:
      if num1 + num2 == target:
        print(f'num1: {num1}, num2: {num2}')
        return

def main():
  nums = [2, 7, 11, 15]
  target = 9
  twoSum(nums, target)

if __name__ == '__main__':
    main()

그런데 문제에 배열방의 번호를 넘기라고 했으니까 for문을

다른 결과들

def twoSum(self, nums, target):
        """
        :type nums: List[int]
        :type target: int
        :rtype: List[int]
        """
        for i in range(len(nums)):
            mid = target - nums[i]
            for j in range(i+1,len(nums)):
                if nums[j] == mid:
                    return [i,j]
class Solution:
    def twoSum(self, nums, target):
        num_set = {}
        for num_index, num in enumerate(nums):
            if (target-num) in num_set:
                return [num_set[target-num], num_index]
            num_set[num] = num_index
def twoSum(self, nums: 'List[int]', target: 'int') -> 'List[int]':
    """
    finds two numbers which add to a given target number and returns their indices
    """
    # create dict to store reciprocal digits
    r = {}
    # loop through nums (keep index)
    for x in range(0, len(nums)):
        n = target - nums[x]
        # check dict for reciprocal of current number
        if n in r:
            return [r[n], x]
        # if not in dict, add current number to avoid doubles
        r[nums[x]] = x

참고자료

구간별 파일보기 with Cat, head, and tail

파일을 전체다 보고 싶을때

cat 파일명

파일의 맨 위에서 부터 몇번째 라인까지만 보고 싶을때

head -n 라인개수 파일명

파일의 맨 끝에서부터 몇번째 라인까지만 보고 싶을때

 tail -n 라인개수 파일명

파일의 특정구간을 보고 싶을때

cat 파일명 | head -n 20 | tail -n 5

위의 명령어는 파일을 보여주되, 위에서 20라인만 가져오고, 가져온 20개 결과의 맨끝의 5개만 보여준다. 즉 위의 명령어는 16번째 줄부터 20번째 줄까지의 결과를 반환한다.