Simple CRUD - シナリオ編

本チュートリアルは、単一のデータモデルを定義し、データモデルに対してCRUDするためのAPIを作成します。 Modelでデータベーススキーマを定義生成して、生成されたデータモデルをScenarioからCRUD操作するアプリケーションを作成しましょう。

Step1. データモデルを作成する

データモデルは、jsonschemaによって定義します。 ここでは、Employmentという名前のデータモデルを作成しています。

category: Tutorial
scenario_auto_generation_mode: false
model:
  name: Employment
  schema:
    type: object
    properties:
      entryNumber:
        type: string
      firstName:
        type: string
      lastName:
        type: string
      email:
        type: string
      salaryRequirements:
        type: integer
    required:
      - entryNumber
      - firstName
      - lastName
      - email
  constraints:
    primary_key: entryNumber
    unique_keys:
        - email


データモデルを登録したら、REPLでモデルオブジェクトやデータベーステーブルが生成されていることを確認しましょう。

>>> models↵
Tutorial Employment
>>> desc Employment;↵
| Field | Type | Null | Key | Default | Extra |
entryNumber varchar(255) NO PRI NULL 
firstName varchar(255) NO  NULL 
lastName varchar(255) NO  NULL 
email varchar(255) NO UNI NULL 
salaryRequirements int YES  NULL 
>>>


Step2. カウンタを作成する

Employmentの主キーは、entryNumberです。新規登録時は、entryNumberを払い出す必要があります。ここでは、entryNumberは、E + 数字4桁の文字列とします。
Counterサービスを利用してユニークなentryNumberを払い出せるようにしましょう。
カウンタの定義は以下のようになります。

counter_name: entryNumber
counter_type: number
counter_format: E$
counter_script: null
max_num: 9999
min_num: 1
padding: true


カウンタを作成したら、entryNumberが払い出せることを確認しましょう。

>>> n = await Counter.allocate("entryNumber")↵
... print(n)↵
... ↵
↵
E0001
>>>


Step3. 新規登録シナリオを作成する

それでは、新規登録シナリオを作成しましょう。 以下の定義でシナリオを作成してください。

category: Tutorial
name: createEmployment
uri: /tutorlals/employments
method: POST
routing_auto_generation_mode: true
spec:
  response:
    normal:
      codes:
        - 200
  request:
    headers:
      type: object
      properties:
        Content-Type:
          type: string
          default: application/json
      required:
        - Content-Type
    body:
      type: object
      properties:
        firstName:
          type: string
          pattern: '[a-zA-Z]'
        lastName:
          type: string
          pattern: '[a-zA-Z]'
        email:
          type: string
          format: email
        salaryRequirements:
          type: integer
          minimum: 0
          maximum: 99999999
      required:
        - firstName
        - lastName
        - email
        - salaryRequirements
commands:
  - command: script
    kwargs:
      code: |-
        entryNumber = await Counter.allocate("entryNumber")
        async with model.aiodb() as conn:
            cursor = await conn.execute(model.Employment.select().where(model.Employment.c.email==context.request.body.email))
            if cursor.rowcount != 0:
                raise Error(400, reason="E-mail address is already registered")

            await conn.execute(model.Employment.insert().values(entryNumber=entryNumber,
                                                                firstName=context.request.body.firstName,
                                                                lastName=context.request.body.lastName,
                                                                email=context.request.body.email,
                                                                salaryRequirements=context.request.body.salaryRequirements))
        context.session.finish(dict(entryNumber=entryNumber))
request_timeout: 60
connect_timeout: 60

このシナリオを保存すると、POST /tutorlals/employmentsで待ち受けるAPIが生成されます。この例のようにspecでAPI入力仕様を記述すると、リクエストバリデーションが有効になります。specに記述されているスキーマ情報は、API Gatewayに生成されるAPIルーティング情報に保存され、API Gatewayでリクエストが受信されたときにバリデーションされます。バリデーションがNGの場合、400 BadRequestを返却します。シナリオ側でもリクエスト受信時にバリデーションする場合は、request_validationコマンドを使用してください。
シナリオの動作としては、最初にカウンタサービスを利用してentryNumberを払い出します。 次にデータベースに接続して、emailが重複しているデータが存在しないかをチェックします。重複している場合は、400 BadRequestを返却します。 重複がない場合は、データを登録します。最後にentryNumber200 Success応答します。


シナリオを作成したら、Scenario.runで実行してみましょう。実行前後でEmploymentテーブルのレコードを検索して状況を確認してみてください。
尚、Scenario組込みオブジェクトの詳細は、Docs » リファレンス » ビルトインオブジェクトを参照してください。

>>> select * from Employment;↵
Empty set
>>> r = await Scenario.run("createEmployment", firstName="Ray", lastName="Amuro", email="amuroray@uc.com", salaryRequirements=10000000)↵
... print(r.body)↵
... ↵
↵
b'{"entryNumber":"E0003"}'
>>> select * from Employment;↵
| entryNumber | firstName | lastName | email | salaryRequirements |
E0003 Ray Amuro amuroray@uc.com 10000000
>>>


Step4. 検索シナリオを作成する

検索シナリオを作成しましょう。以下の定義でシナリオを作成してください。

category: Tutorial
name: getEmployment
uri: /tutorials/employments
additional_paths:
  - '/tutorials/employments/{entryNumber}'
method: GET
routing_auto_generation_mode: true
spec:
  response:
    normal:
      codes:
        - 200
  request:
    params:
      type: object
      properties:
        entryNumber:
          type: array
          items:
            type: string
        firstName:
          type: array
          items:
            type: string
            pattern: '[a-zA-Z]'
        lastName:
          type: array
          items:
            type: string
            pattern: '[a-zA-Z]'
        email:
          type: array
          items:
            type: string
            format: email
        salaryRequirements:
          type: array
          items:
            type: integer
            minimum: 0
            maximum: 99999999
    resources:
      type: object
      properties:
        entryNumber:
          type: string
commands:
  - command: script
    kwargs:
      code: |-
        async with model.aiodb() as conn:
            if context.request.resources.entryNumber:
                cursor = await conn.execute(model.Employment.select().where(model.Employment.c.entryNumber==context.request.resources.entryNumber))
                if cursor.rowcount == 0:
                    raise Error(404, reason="Could not found employment")

                employment = await cursor.fetchone()
                context.session.finish(rowtodict(employment))
                return
            logger.info(context.request.params.dictionary)
            cursor = await conn.execute(model.Employment.select().where(where_statement(model.Employment, context.request.params.dictionary)))
            employments = await cursor.fetchall()
            context.session.finish([rowtodict(employment) for employment in employments])
request_timeout: 60
connect_timeout: 60


このAPIは、GET /tutorials/employmentsを介してアクセスするとEmploymentリストを返します。GET /tutorials/employments/{entryNumber}でアクセスすると、対応するEmploymentが返されます。GET /tutorials/employee?lastName=Amuroのようなクエリ検索もできます。クエリ検索に対応するためにSQLのWHERE句を組み立てる必要がありますが、この例ではwhere_statement組込み関数にクエリパラメータの辞書を渡して自動生成しています。


シナリオを作成したら、Scenario.runで実行してみましょう。

>>> r = await Scenario.run("getEmployment")↵
... print(json.dumps(r.body, indent=4))↵
... ↵
[
    {
        "entryNumber": "E0011",
        "firstName": "Ray",
        "lastName": "Amuro",
        "email": "amuroray@uc.com",
        "salaryRequirements": 10000000
    }
]↵
↵
>>> r = await Scenario.run("getEmployment", lastName="Amuro")↵
... print(json.dumps(r.body, indent=4))↵
... ↵
[
    {
        "entryNumber": "E0011",
        "firstName": "Ray",
        "lastName": "Amuro",
        "email": "amuroray@uc.com",
        "salaryRequirements": 10000000
    }
]↵
↵
>>> r = await Scenario.run("getEmployment", lastName="Hoge")↵
... print(r.body)↵
... ↵
[]↵
↵
>>> r = await Scenario.run("getEmployment", entryNumber="E0001")↵
... print(r.body)↵
... ↵
{'errorCode': 404, 'errorMessage': 'Could not found employment', 'moreInfo': None}↵
↵
>>> r = await Scenario.run("getEmployment", entryNumber="E0011")↵
... print(json.dumps(r.body, indent=4))↵
... ↵
{
    "entryNumber": "E0011",
    "firstName": "Ray",
    "lastName": "Amuro",
    "email": "amuroray@uc.com",
    "salaryRequirements": 10000000
}↵
↵


Step5. 更新シナリオを作成する

更新シナリオを作成しましょう。以下の定義でシナリオを作成してください。

category: Tutorial
name: updateEmployment
uri: '/tutorials/employments/{entryNumber}'
method: PUT
routing_auto_generation_mode: true
request_timeout: 60
connect_timeout: 60
spec:
  response:
    normal:
      codes:
        - 200
  request:
    headers:
      type: object
      properties:
        Content-Type:
          type: string
          default: application/json
      required:
        - Content-Type
    body:
      type: object
      properties:
        firstName:
          type: string
          pattern: '[a-zA-Z]'
        lastName:
          type: string
          pattern: '[a-zA-Z]'
        email:
          type: string
          format: email
        salaryRequirements:
          type: integer
          minimum: 0
          maximum: 99999999
    resources:
      type: object
      properties:
        entryNumber:
          type: string
      required:
        - entryNumber
commands:
  - command: script
    kwargs:
      code: |-
        async with model.aiodb() as conn:
            if not context.request.resources.entryNumber:
                raise Error(400, reason="entryNumber is not specified")

            cursor = await conn.execute(model.Employment.select().where(model.Employment.c.entryNumber==context.request.resources.entryNumber))
            if cursor.rowcount == 0:
                raise Error(404, reason="Could not found employment")

            row = await cursor.fetchone()
            employment = rowtodict(row)
            employment.update(context.request.body.dictionary)
            await conn.execute(model.Employment.update().where(model.Employment.c.entryNumber==context.request.resources.entryNumber).values(**employment))
            context.session.finish({"entryNumber": context.request.resources.entryNumber})


シナリオを作成したら、Scenario.runで実行してみましょう。

>>> r = await Scenario.run("updateEmployment", entryNumber="E0011", email="updated@gmail.com")↵
... print(json.dumps(r.body, indent=4))↵
... r = await Scenario.run("getEmployment")↵
... print(json.dumps(r.body, indent=4))↵
... ↵
{
    "entryNumber": "E0011"
}↵
↵
[
    {
        "entryNumber": "E0011",
        "firstName": "Ray",
        "lastName": "Amuro",
        "email": "updated@gmail.com",
        "salaryRequirements": 10000000
    }
]↵
↵


Step6. 削除シナリオを作成する

削除シナリオを作成しましょう。以下の定義でシナリオを作成してください。

category: Tutorial
name: deleteEmployment
uri: '/tutorials/employments/{entryNumber}'
method: DELETE
routing_auto_generation_mode: true
spec:
  response:
    normal:
      codes:
        - 200
  request:
    resources:
      type: object
      properties:
        entryNumber:
          type: string
      required:
        - entryNumber
commands:
  - command: script
    kwargs:
      code: |-
        async with model.aiodb() as conn:
            if not context.request.resources.entryNumber:
                raise Error(400, reason="entryNumber is not specified")

            cursor = await conn.execute(model.Employment.select().where(model.Employment.c.entryNumber==context.request.resources.entryNumber))
            if cursor.rowcount == 0:
                raise Error(404, reason="Could not found employment")

            await conn.execute(model.Employment.delete().where(model.Employment.c.entryNumber==context.request.resources.entryNumber))
            context.session.set_status(204)
            context.session.finish()
request_timeout: 60
connect_timeout: 60


シナリオを作成したら、Scenario.runで実行してみましょう。

>>> r = await Scenario.run("deleteEmployment", entryNumber="E0012")↵
... print(r.code)↵
... ↵
204↵
↵
>>> r = await Scenario.run("getEmployment")↵
... print(json.dumps(r.body, indent=4))↵
... ↵
[]↵


以上でSimple CRUD - シナリオ編のチュートリアルは完了です。ここでは作成したシナリオの動作確認はREPL上で行いましたが時間に余裕のある方は、curlコマンドやcallout組込みオブジェクトなどHTTPクライアントを用いて、API Gateway経由でAPIを呼び出してみてください。