Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

DjangoRestFrameworkのTips

バージョン情報

module version
Python 3.7.7
django-environ 0.4.5
Django 2.2.11
djangorestframework 3.11.0
djangorestframework-jwt 1.11.0
django-rest-swagger 2.2.0
django-filter 2.2.0
mysqlclient 1.4.6

Swaggerを使用したい

django-rest-swaggerをpipインストール

pip3 install django-rest-swagger==2.2.0

settings.pyを修正

backend/src/config/settings/settings.py
.
..
...
REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': (
        'rest_framework.permissions.IsAuthenticated',
    ),
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
        'rest_framework.authentication.SessionAuthentication',
        'rest_framework.authentication.BasicAuthentication',
    )
}

development.pyを修正

backend/src/config/settings/developement.py
.
..
...
INSTALLED_APPS += [
    'rest_framework_swagger',
]

REST_FRAMEWORK['DEFAULT_SCHEMA_CLASS'] = 'rest_framework.schemas.coreapi.AutoSchema'

urls.pyを修正

backend/src/config/urls.py
from django.contrib import admin
from django.urls import path
from django.conf import settings  # 追記
from rest_framework_jwt.views import obtain_jwt_token  # 追記

urlpatterns = [
    path('admin/', admin.site.urls),
    url('api/v1/login/', obtain_jwt_token),  # なんか1つ以上APIがないとSwagger-UIを開けないみたいなので、とりあえずログインのAPIを登録
]

# 以下全て追記
if settings.DEBUG:  # DEBUG=True(開発時)の場合のみ使用
    from rest_framework.schemas import get_schema_view
    from rest_framework_swagger import renderers
    schema_view = get_schema_view(
        title='API一覧',
        public=True,
        renderer_classes=[renderers.OpenAPIRenderer, renderers.SwaggerUIRenderer])
    urlpatterns += [
        url(API_ROOT + 'api-auth/', include('rest_framework.urls')),
        url('swagger-ui/', schema_view),
    ]

Swagger-uiにアクセス

http://0.0.0.0:8000/swagger-ui/にアクセスして下記のような画面が出たら右上のLog inを押下してログインする
image.png

image.png

image.png

ViewSets編

とりあえずモデルに対するGET, POST, PUT, DELETE...のAPIを公開したい

みんな大好きModelViewSetsが使えます。

backend/src/api/users/views.py
from api.users.serializers import UserSerializer
from common.models import User
from rest_framework.viewsets import ModelViewSet  # コレ


class UserViewSets(ModelViewSet):
    queryset = User.objects.all()
    serializer_class = UserSerializer

出来上がり(swagger-ui使ってます)
image.png

公開するHTTPリクエストメソッドは絞りたい

方法1: GenericViewSetと各種mixinsを組み合わせる

後でリクエストメソッドを増やしたいといった場合は対応するmixinsをつけてあげればいい

backend/src/api/users/views.py
from api.users.serializers import UserSerializer
from common.models import User
from rest_framework.viewsets import GenericViewSet  # コレ
from rest_framework.mixins import CreateModelMixin  # コレ


class UserViewSets(GenericViewSet, CreateModelMixin):
    queryset = User.objects.all()
    serializer_class = UserSerializer

image.png

方法2: genericsを使用する

後でリクエストメソッドを増やしたいといった場合は対応するAPIViewをつけてあげればいい

backend/src/api/users/views.py
from api.users.serializers import UserSerializer
from common.models import User
from rest_framework.generics import CreateAPIView  # コレ


class UserViewSets(CreateAPIView):
    queryset = User.objects.all()
    serializer_class = UserSerializer

genericsの場合、urls.pyの指定方法が異なる

backend/src/config/urls.py
# =========== viewsets ===========
router = DefaultRouter()
router.register('user', user_views.UserViewSets)  # viewsetsの場合はrouterが使える

urlpatterns = [
    path('admin/', admin.site.urls),
    url('api/v1/login/', obtain_jwt_token),
    url('', include(router.urls)),
]
backend/src/config/urls.py
# =========== generics ===========
urlpatterns = [
    path('admin/', admin.site.urls),
    url('api/v1/login/', obtain_jwt_token),
    url('user/', user_views.UserViewSets.as_view()),  # urlpatternsにas_views()で追加する
]

結果は方法1と同じ
image.png

ModelViewSetを使いたい、でもHTTPリクエストメソッドは絞りたい(照)

そんなワガママさんにはhttp_method_namesじゃ。
実装はしたけどまだ公開したくない、みたいな場面で使うのかな・・・。

backend/src/api/users/views.py
from api.users.serializers import UserSerializer
from common.models import User
from rest_framework.viewsets import ModelViewSet


class UserViewSets(ModelViewSet):
    queryset = User.objects.all()
    serializer_class = UserSerializer
    http_method_names = ['get', 'post']  # HTTPリクエストメソッドは大文字じゃなくて、小文字(重要)

image.png

APIをリクエストできるのは認証済みのユーザーだけに絞りたい

permission_classにIsAuthenticatedを指定しましょう。

まずsettings.pyを修正します。

backend/src/config/settings/settings.py
.
..
...
REST_FRAMEWORK = {
    # 追加
    'DEFAULT_PERMISSION_CLASSES': (
        'rest_framework.permissions.IsAuthenticated',
        # 'common.permissions.IsSuperuser',  # 自作のpermissionを作成した場合は、ここに追加。
    ),
    ...
    ..
    .
}

未認証状態でAPIをリクエストすると、403が返ってきます。

backend/src/api/users/views.py
from api.users.serializers import UserSerializer
from common.models import User
from rest_framework.permissions import IsAuthenticated  # 追加
from rest_framework.viewsets import ModelViewSet


class UserViewSets(ModelViewSet):
    permission_classes = [IsAuthenticated,]  # 追加
    queryset = User.objects.all()
    serializer_class = UserSerializer

ルーティング可能なアドホックメソッドを追加したい(detail=False)

そんな時は@actionデコレータじゃ。
(DRFバージョンが古いと、@list_route@detail_routeデコレータがこれにあたります。@actionデコレータに統合されたようですね。)

backend/src/api/users/views.py
from api.users.serializers import UserSerializer
from common.models import User
from rest_framework.decorators import action  # 追加
from rest_framework.viewsets import ModelViewSet


class UserViewSets(ModelViewSet):
    queryset = User.objects.all()
    serializer_class = UserSerializer

    @action(detail=False, methods=['get'], url_path='get_user', url_name='get_user')
    def get_user(self, request, *kwargs):
        """ ログインユーザー情報を返す
        """
        return UserSerializer(self.request.user).data

image.png

ルーティング可能なアドホックメソッドを追加したい(detail=True)

backend/src/api/users/views.py
from api.users.serializers import UserSerializer
from common.models import User
from rest_framework import status
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet


class UserViewSets(ModelViewSet):
    queryset = User.objects.all()
    serializer_class = UserSerializer

    @action(detail=True, methods=['get'], url_path='get_user', url_name='get_user')
    def get_user(self, request, pk=None, **kwargs):
        """ ログインユーザー情報を返す
        """
        # 処理はdefail=Falseと同じです。すみません(. _ .)
        return Response(
            status=status.HTTP_200_OK,
            data=UserSerializer(self.request.user).data)

api_root/{pk}/url_name/といったAPIになります。
image.png

ルーティング可能なアドホックメソッドを追加したい。URLに主キー含めたいけど、@actionデコレータのdetailはFalseがいい(照)

そんなワガママさんは@actionデコレーターのurl_pathじゃ。

backend/src/api/users/views.py
from api.users.serializers import UserSerializer
from common.models import User
from rest_framework import status
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet


class UserViewSets(ModelViewSet):
    queryset = User.objects.all()
    serializer_class = UserSerializer

    @action(detail=False, methods=['get'], url_path='get_user/(?P<user_id>[0-9]+)', url_name='get_user')
    def get_user(self, request, user_id=None):
        """ ログインユーザー情報を返す
        """
        return Response(
            status=status.HTTP_200_OK,
            data=UserSerializer(self.request.user).data)

detail=Trueで生成されるURLが気に食わない人にはおすすめかもしれません笑

image.png

クエリパラメータでの検索を可能にしたい

django-filterをpipインストール

pip3 install django-filter==2.2.0

filters.py(ファイル名は任意)を作成して、filterを定義する

backend/src/api/users/filters.py
from common.models import User
from django_filters import FilterSet


class UserSearchFilter(FilterSet):
    class Meta:
        model = User
        fields = '__all__'

        # クエリパラメータに指定されたくないフィールドはexcludeに追加してあげると良い
        # exclude = [
        #     'password',
        #     'date_created',
        #     'date_updated',
        # ]

定義したfilterをviewsで使用する

backend/src/api/users/views.py
from api.users.serializers import UserSerializer
from common.models import User
from django_filters.rest_framework import DjangoFilterBackend  # 追加
from api.users.filters import UserSearchFilter  # 追加
from rest_framework.viewsets import ModelViewSet


class UserViewSets(ModelViewSet):
    filter_backends = [DjangoFilterBackend,]  # 追加
    filter_class = UserSearchFilter  # 追加
    queryset = User.objects.all()
    serializer_class = UserSerializer

クエリパラメータを指定してGETできるようになります。

image.png

Serializer編

みんな大好きModelSerializer

backend/src/api/users/serializers.py
from rest_framework.serializers import ModelSerializer
from common.models import User


class UserSerializer(ModelSerializer):
    """ ユーザーシリアライザー
    """
    class Meta:
        model = User
        fields = '__all__'

パラメータを絞りたい

groupsとかuser_permissionsとかをパラメータで投げられたくない

image.png

そんな時はextra_kwargsに指定します。

backend/src/api/users/serializers.py
from rest_framework.serializers import ModelSerializer
from common.models import User


class UserSerializer(ModelSerializer):
    """ ユーザーシリアライザー
    """
    class Meta:
        model = User
        fields = '__all__'
        extra_kwargs = {
            'is_superuser': {'read_only': True},
            'date_created': {'read_only': True},
            'date_deleted': {'read_only': True},
            'is_staff': {'read_only': True},
            'is_active': {'read_only': True},
            'groups': {'read_only': True},
            'user_permissions': {'read_only': True},
        }

無事、絞れました↓
image.png

読み取り専用(read_only)にしただけなので、取得(GET)時はextra_kwargsに指定したものも含めてくれます。
image.png

関数/メソッドの結果をフィールドの値として返したい

例えば、リレーション先の値とかは主キーだけが入った状態です。
※下記画像school
image.png

こんな時はSerializerMethodFieldが使えます。
フィールドを定義したら、get_フィールド名の関数を用意してあげます。

backend/src/api/users/serializers.py
import json  # 追加
from django.core.serializers import serialize  # 追加
from rest_framework.serializers import ModelSerializer, SerializerMethodField  # 追加
from common.models import User


class UserSerializer(ModelSerializer):
    """ ユーザーシリアライザー
    """
    school = SerializerMethodField()  # フィールド追加

    # get_フィールド名の関数追加
    def get_school(self, obj):
        """ schoolオブジェクトをJSONシリアライズしたオブジェクトを返す

        Args:
            obj (User): Userオブジェクト

        Returns:
            dict: UserオブジェクトをJSON形式にシリアライズしたオブジェクト
        """
        if obj.school is not None:
            data = serialize('json', [obj.school,])
            objs = json.loads(data)
            return json.dumps(objs[0]['fields'])
        return None

    class Meta:
        ...

さっき主キーだったschoolに、schoolオブジェクトが展開されました(. _ .)
SerializerMethodFieldを使えば割と任意の値を一緒に返せるので便利です。

image.png

リレーション先のオブジェクトを返したい。でもSerializerMethodFieldよくわからん(照)

そんなワガママさんはdepthオプションじゃ。

backend/src/api/users/serializers.py
from common.models import User


class UserSerializer(ModelSerializer):
    """ ユーザーシリアライザー
    """
    class Meta:
        model = User
        fields = '__all__'
        extra_kwargs = {
            'is_superuser': {'read_only': True},
            'date_created': {'read_only': True},
            'date_deleted': {'read_only': True},
            'is_staff': {'read_only': True},
            'is_active': {'read_only': True},
            'groups': {'read_only': True},
            'user_permissions': {'read_only': True},
        }
        depth = 1  # デフォルトは0なので、0を指定しても意味ない

image.png

depth = 2とかすると、さらに先のリレーションまで取得してくれます。(この例だと、schoolがさらにリレーションを先を持っている場合、そのオブジェクトも返してくれる)

※ただ、POSTで新規登録する場合めんどくさくなるのでやっぱりSerializerMethodFieldがオススメです。GETでしか使わない場合とかには有効かもしれない。

djangorestframework-jwt編

obtain_jwt_tokenのレスポンスにtokenの他にユーザー情報も含めたい

デフォルトだとtokenしか返してくれません。
image.png

ここにtokenの他にユーザー情報を含めたい場合はjwt_response_payload_handlerを自作します。
jwt_utils.pyを作成(ファイル名は任意)

backend/src/common/jwt_utils.py
def jwt_response_payload_handler(token, user=None, request=None):
    """ JWT認証のカスタムレスポンス
    """
    return {
        'token': token,
        'first_login': user.first_login,
        'id': user.id,
    }

settings.pyを修正します。

backend/src/config/settings/settings.py
.
..
...
# JWT_AUTHを追加
JWT_AUTH = {
    'JWT_RESPONSE_PAYLOAD_HANDLER': 'common.jwt_utils.jwt_response_payload_handler',  # JWT_RESPONSE_PAYLOAD_HANDLERに自作のjwt_response_payload_handlerを指定する
}

レスポンスを増やすことができました(. _ .)

image.png

Djangoプロジェクト, DjangoRestFrameworkの導入編

django-environ, Djangoとdjangorestframeworkをpipインストール

pip3 install django-environ==0.4.5 Django==2.2.11 djangorestframework==3.11.0 mysqlclient==1.4.6

プロジェクトルートとバックエンドのsrcディレクトリを作成

mkdir -p project_root/backend/src

 
下記の様なディレクトリ構成になる

project_root # プロジェクトルート
└── backend
    └── src

 

Djangoプロジェクト作成

cd project_root/backend/src
django-admin startproject confing .

下記の様なディレクトリ構成になる(configに設定ファイルをまとめられる)

project_root # プロジェクトルート
└── backend
     └── src
         ├── config
         │    ├── __init__.py
         │    ├── settings.py
         │    ├── urls.py
         │    └── wsgi.py
         │
         └── manage.py

設定ファイルを開発用と本番用とで分割する

project_root # プロジェクトルート
└── backend
     └── src
         ├── config
         │    ├── __init__.py
         │    ├── settings
         │    │    ├── settings.py
         │    │    ├── development.py  # 開発用
         │    │    └── production.py  # 本番用
         │    │
         │    ├── urls.py
         │    └── wsgi.py
         └── manage.py

 

設定ファイル分割に伴う修正

settings.pyを修正
(BASE_DIRがsrcディレクトリになるよう修正)

backend/src/config/settings/settings.py
.
..
...
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))  # >> /src/confing/settings
 │
 ↓
BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))  # >> /src
...
..
.

manage.pyを修正

backend/src/manage.py
.
..
...
def main():
    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'confing.settings')
     
     
    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'confing.settings.development')  # developmentを読み込む様にする
...
..
.

 
デプロイを考慮してwsgi.pyを修正

backend/src/config/wsgi.py
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'confing.settings')
 │
 ↓
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'confing.settings.production')

development.pyを修正

backend/src/config/settings/development.py
from confing.settings.settings import *

ALLOWED_HOSTS = ['*']

production.pyを修正

backend/src/config/settings/development.py
from confing.settings.settings import *

DRFを使えるようにする

settings.pyを修正

backend/src/config/settings.settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',  # 追記
]

Dockerでwebとdbコンテナを立ち上げる

docker-compose.ymlとDockerfileを用意する

project_root # プロジェクトルート
├── backend
│    ├── src
│    │   ├── config
│    │   │    ├── __init__.py
│    │   │    ├── settings
│    │   │    │    ├── settings.py
│    │   │    │    ├── development.py
│    │   │    │    └── production.py
│    │   │    │
│    │   │    ├── urls.py
│    │   │    └── wsgi.py
│    │   |
│    │   └── manage.py
│    │
│    └── Dockerfile  # 追加
│
└── docker-compose.yml  # 追加
docker-compose.yml
version: '3'
services:
  web:
    build:
      context: ./
      dockerfile: ./backend/Dockerfile
    container_name: drf_web
    volumes:
      - './backend/src:/src'
    environment:
      - LC_ALL=ja_JP.UTF-8
    ports:
      - '8000:8000'
    depends_on:
      - db
    command: python3 manage.py runserver 0.0.0.0:8000
    restart: always
    tty: true

  db:
    image: mariadb:latest
    container_name: drf_db
    command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_general_ci
    environment:
      - MYSQL_ROOT_USER=root
      - MYSQL_ROOT_PASSWORD=root
      - MYSQL_DATABASE=db_drf
      - MYSQL_USER=user
      - MYSQL_PASSWORD=user
    volumes:
      - db_data:/var/lib/mysql
    ports:
      - '3306:3306'

volumes:
  db_data:
    driver: local
FROM ubuntu:18.04
RUN apt update && apt install -y locales python3-pip python3.7 python3-dev libssl-dev libffi-dev libgeos-dev libmysqlclient-dev
RUN apt-get update && apt-get install -y mysql-client python3-gdal
RUN mkdir /src \
    && rm -rf /var/lib/apt/lists/* \
    && echo "ja_JP UTF-8" > /etc/locale.gen \
    && locale-gen
WORKDIR /src
ADD ./backend/src /src/
RUN LC_ALL=ja_JP.UTF-8 pip3 install -r requirements.txt

Dockerコンテナ起動

docker-compose up -d

※もしかしたらwebコンテナがDB接続エラーで立ち上げ失敗してしまうかもしれませんが、その時はdocker-compose stopしてからdocker-compose up -dしてください。その対応が微妙だという方はwait-for-it.shの導入を検討してください。

Djangoのスタートアップページにアクセスする

この画面が出れば成功
image.png

.env.developmentファイルを作成

cd backend/src/
touch .env.development

.env.developmentファイルの内容

backend/src/.env.development
DEBUG=True
DATABASE_URL=mysql://user:user@db:3306/db_drf

development.pyを修正

backend/src/config/settings/development.py
import environ

ENV_FILE = os.path.join(BASE_DIR, '.env.development')
ENV = environ.Env()
ENV.read_env(ENV_FILE)

DEBUG = ENV.get_value('DEBUG', cast=bool)
DATABASES['default'] = ENV.db()  # DB接続情報読み込み(.env.developmentのDATABASE_URLを読み込んでくれる)
...
..
.

ベースモデルの実装

baseアプリケーションを作成

django-admin startapp base

settings.pyのINSTALLED_APPSに追記

INSTALLED_APPS = [
    .
    ..
    ...
    'base',
]

BaseModelを記述

backend/src/base/models.py
from django.db import models
from django.utils import timezone

# Create your models here.
from django.db import models
from django.utils import timezone

# Create your models here.
class BaseModel(models.Model):
    """ ベースモデル
    """
    date_created = models.DateTimeField('作成日時', default=timezone.now)
    date_updated = models.DateTimeField('最終更新日時', auto_now_add=True)
    date_deleted = models.DateTimeField('削除日時', null=True)

    class Meta:
        abstract = True  # ← 必須

カスタムユーザーモデルの実装

commonアプリケーションを作成

django-admin startapp common

settings.pyのINSTALLED_APPSに追記

INSTALLED_APPS = [
    .
    ..
    ...
    'base',
    'common',
]

まずユーザーマネージャーを実装

manager.pyを作成

cd backend/src/base
touch manager.py
backend/src/base/manager.py
from django.contrib.auth.models import UserManager


class UserManager(UserManager):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

    def _create_user(self, username, email, password, **extra_fields):
        """
        Create and save a user with the given username, email, and password.
        """
        if not username:
            raise ValueError('The given username must be set')
        email = self.normalize_email(email)
        username = self.model.normalize_username(username)
        user = self.model(username=username, email=email, **extra_fields)
        user.set_password(password)
        user.save(using=self._db)
        return user

    def create_user(self, username, email=None, password=None, **extra_fields):
        extra_fields.setdefault('is_staff', True)
        extra_fields.setdefault('is_superuser', False)
        return self._create_user(username, email, password, **extra_fields)

    def create_superuser(self, username, email, password, **extra_fields):
        extra_fields.setdefault('is_staff', True)
        extra_fields.setdefault('is_superuser', True)

        if extra_fields.get('is_staff') is not True:
            raise ValueError('Superuser must have is_staff=True.')
        if extra_fields.get('is_superuser') is not True:
            raise ValueError('Superuser must have is_superuser=True.')

        return self._create_user(username, email, password, **extra_fields)

カスタムユーザーモデル実装

公式がカスタムユーザーモデルの実装を推奨しているので従いましょう(. _ .)

backend/src/common/models.py
from django.db import models
from django.contrib.auth.base_user import AbstractBaseUser
from django.contrib.auth.models import PermissionsMixin
from base.manager import UserManager
from base.models import BaseModel


class User(AbstractBaseUser, PermissionsMixin, BaseModel):
    email = models.EmailField('メールアドレス', blank=True, null=True)
    username = models.CharField('ユーザー名', max_length=150, unique=True)
    display_name = models.CharField('画面表示名', max_length=30, blank=True, null=True)
    last_name = models.CharField('姓', max_length=150, blank=True, null=True)
    first_name = models.CharField('名', max_length=30, blank=True, null=True)
    is_staff = models.BooleanField('スタッフフラグ', default=False)
    is_active = models.BooleanField('有効フラグ', default=True)
    first_login = models.BooleanField('初回ログイン', default=True)

    objects = UserManager()

    EMAIL_FIELD = 'email'
    USERNAME_FIELD = 'username'
    REQUIRED_FIELDS = ['email']

    class Meta:
        verbose_name = verbose_name_plural = 'users'
        db_table = 'user'

settings.pyを修正する

backend/src/config/settings/settings.py
.
..
...
AUTH_USER_MODEL = 'common.User'  # 認証に使用するユーザーモデルに、カスタムユーザーモデルを指定。

AUTH_USER_MODELの指定を忘れると、以下のようなエラーで悩まされます。

python3 manage.py makemigrations
SystemCheckError: System check identified some issues:

ERRORS:
auth.User.groups: (fields.E304) Reverse accessor for 'User.groups' clashes with reverse accessor for 'User.groups'.
    HINT: Add or change a related_name argument to the definition for 'User.groups' or 'User.groups'.
auth.User.user_permissions: (fields.E304) Reverse accessor for 'User.user_permissions' clashes with reverse accessor for 'User.user_permissions'.
    HINT: Add or change a related_name argument to the definition for 'User.user_permissions' or 'User.user_permissions'.
common.User.groups: (fields.E304) Reverse accessor for 'User.groups' clashes with reverse accessor for 'User.groups'.
    HINT: Add or change a related_name argument to the definition for 'User.groups' or 'User.groups'.
common.User.user_permissions: (fields.E304) Reverse accessor for 'User.user_permissions' clashes with reverse accessor for 'User.user_permissions'.
    HINT: Add or change a related_name argument to the definition for 'User.user_permissions' or 'User.user_permissions'.

マイグレーションファイルを生成

python3 manage.py makemigrations

マイグレーション実行

python3 manage.py migrate
Butterthon
いろんなことに興味あります。 最近のマイブームはPython, Django, Vue, Ruby(今更) スキル:C#, Java(Seasar2), Python(Django), Ruby(Rails), JavaScript, jQuery, Vue.js, AWS
https://twitter.com/Butterthon
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away