DBのパフォーマンスを考慮して複雑なREST APIを設計するにはどうすればよいですか?


8

REST APIの設計方法に関するいくつかのチュートリアルに従ってきましたが、まだいくつかの大きな疑問符があります。これらのチュートリアルはすべて、比較的単純な階層のリソースを示しています。これらのチュートリアルで使用されている原則が、より複雑な階層にどのように適用されるかを知りたいのです。さらに、彼らは非常に高い/建築レベルにとどまります。永続層は言うまでもなく、関連するコードはほとんど表示されません。Gavin Kingが言ったように、私は特にデータベースのロード/パフォーマンスについて心配しています:

開発のすべての段階でデータベースに注意を払えば、労力を節約できます

アプリケーションがのトレーニングを提供するとしますCompaniesCompanies持っDepartmentsていOfficesます。Departments持っていEmployeesます。Employees持っているSkillsCoursesし、特定のLevel特定のスキルのは、いくつかのコースのために署名することができるように要求されています。階層は次のとおりですが、

-Companies
  -Departments
    -Employees
      -PersonalInformation
        -Address
      -Skills (quasi-static data)
        -Levels (quasi-static data)
      -Courses
        -Address
  -Offices
    -Address

パスは次のようになります。

companies/1/departments/1/employees/1/courses/1
companies/1/offices/1/employees/1/courses/1

リソースを取得する

したがって、会社を返すときに、階層全体を返すことはできませんcompanies/1/departments/1/employees/1/courses/1+ companies/1/offices/../。部門または展開された部門へのリンクのリストを返す可能性があり、このレベルでも同じ決定を行う必要があります。部門の従業員または展開された従業員へのリンクのリストを返しますか?それは部門や従業員などの数に依存します。

質問1:私の考えは正しいですか。「階層をどこで切るか」は、私がしなければならない典型的なエンジニアリング上の決定ですか

ここで、尋ねられたときGET companies/idに、部署のコレクションへのリンクのリストと展開されたオフィス情報を返すことにします。私の会社には多くのオフィスがないので、テーブルOfficesと一緒に参加するAddressesことは大したことではありません。応答の例:

GET /companies/1

200 OK
{
  "_links":{
    "self" : {
      "href":"http://trainingprovider.com:8080/companies/1"
      },
      "offices": [
            { "href": "http://trainingprovider.com:8080/companies/1/offices/1"},
            { "href": "http://trainingprovider.com:8080/companies/1/offices/2"},
            { "href": "http://trainingprovider.com:8080/companies/1/offices/3"}
      ],
      "departments": [
            { "href": "http://trainingprovider.com:8080/companies/1/departments/1"},
            { "href": "http://trainingprovider.com:8080/companies/1/departments/2"},
            { "href": "http://trainingprovider.com:8080/companies/1/departments/3"}
      ]
  }
  "name":"Acme",
  "industry":"Manufacturing",
  "description":"Some text here",
  "offices": {
    "_meta":{
      "href":"http://trainingprovider.com:8080/companies/1/offices"
      // expanded offices information here
    }
  }
}

コードレベルでは、これは(Hibernateを使用すると、他のプロバイダーとの関係がわかりませんが、それはほとんど同じだと思います)、コレクションをDepartmentフィールドとしてCompanyクラスに配置しないことを意味します。

  • 言ったように、私はそれをロードしてCompanyいないので、熱心にロードしたくありません
  • そして、それを熱心にロードしない場合は、削除することもできます。これは、Companyをロードした後に永続コンテキストが閉じ、後でそれをロードしようとしても意味がないためです(LazyInitializationException)。

その後、私が出してあげるInteger companyIdにはDepartment、私が会社にするにはカテゴリーを追加できるように、クラス。

また、すべての部門のIDを取得する必要があります。DBへの別のヒットですが、重いものではないので、大丈夫です。コードは次のようになります。

@Service
@Path("/companies")
public class CompanyResource {

    @Autowired
    private CompanyService companyService;

    @Autowired
    private CompanyParser companyParser;

    @Path("/{id}")
    @GET
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response findById(@PathParam("id") Integer id) {
        Optional<Company> company = companyService.findById(id);
        if (!company.isPresent()) {
            throw new CompanyNotFoundException();
        }
        CompanyResponse companyResponse = companyParser.parse(company.get());
        // Creates a DTO with a similar structure to Company, and recursivelly builds
        // sub-resource DTOs such as OfficeDTO
        Set<Integer> departmentIds = companyService.getDepartmentIds(id);
        // "SELECT id FROM departments WHERE companyId = id"
        // add list of links to the response
        return Response.ok(companyResponse).build();
    }
}
@Entity
@Table(name = "companies")
public class Company {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    private String name;

    private String industry;

    @OneToMany(fetch = EAGER, cascade = {ALL}, orphanRemoval = true)
    @JoinColumn(name = "companyId_fk", referencedColumnName = "id", nullable = false)
    private Set<Office> offices = new HashSet<>();

    // getters and setters
}
@Entity
@Table(name = "departments")
public class Department {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    private String name;

    private Integer companyId;

    @OneToMany(fetch = EAGER, cascade = {ALL}, orphanRemoval = true)
    @JoinColumn(name = "departmentId", referencedColumnName = "id", nullable = false)
    private Set<Employee> employees = new HashSet<>();

    // getters and setters
}

リソースの更新

更新操作では、PUTまたはでエンドポイントを公開できますPOST。私PUTはべき等であるため、部分的な更新を許可できません。ただし、会社の説明フィールドを変更する場合は、リソース全体を送信する必要があります。それは肥大化しすぎているようです。従業員のを更新する場合も同様PersonalInformationです。Skills+ Coursesをすべて一緒に送信する必要があるのは理にかなっているとは思いません。

質問2:PUTは、きめの細かいリソースに使用されるだけですか?

ログで、エンティティのマージ時にHibernateが一連のSELECTクエリを実行することを確認しました。何かが変更されたかどうかを確認し、必要な情報を更新するだけだと思います。階層内のエンティティが上になるほど、クエリはより重く、より複雑になります。しかし、いくつかの情報源は、粗いリソースを使用することを勧めています。繰り返しますが、多すぎるテーブルの数を確認し、リソースの粒度とDBクエリの複雑さの間の妥協点を見つける必要があります。

質問3:これは、エンジニアリングの決定を「どこで削減するかを知る」だけなのですか、それとも何か不足していますか?

質問4:これはそうですか、そうでない場合は、RESTサービスを設計し、リソースの粒度、クエリの複雑さ、ネットワークの雑談の間の妥協点を探す際の正しい「思考プロセス」は何ですか?


1
1.はい。REST呼び出しは負荷が高いため、細分性を正しく試すことは重要です。
ロバートハーヴェイ

1
2.いいえ。PUT動詞自体は、粒度とは関係ありません。
Robert Harvey

1
3.はい。いいえ、何も不足していません。
Robert Harvey

1
4.適切な考え方は、「スケーラビリティ、パフォーマンス、保守性、およびその他の問題に対する要件に最も適合するものを実行すること」です。これには、スイートスポットを見つけるためにいくつかの実験が必要になる場合があります。
Robert Harvey

4
長すぎる。読みませんでした。これは実際の4つの質問に分かれますか?
MetaFight

回答:


7

複雑すぎることから始めているので、複雑だと思います。

パスは次のようになります。

companies/1/departments/1/employees/1/courses/1
companies/1/offices/1/employees/1/courses/1

代わりに、次のような簡単なURLスキームを導入します。

GET companies/
    Returns a list of companies, for each company 
    return short essential info (ID, name, maybe industry)
GET companies/1
    Returns single company info like this:

    {
        "name":"Acme",
        "description":"Some text here"
        "industry":"Manufacturing"
        departments: {
            "href":"/companies/1/departments"
            "count": 5
        }
        offices: {
            "href":"/companies/1/offices"
            "count": 3
        }
    }

    We don't expand the data for internal sub-resources, 
    just return the count, so client knows that some data is present.
    In some cases count may be not needed too.
GET companies/1/departments
    Returns company departments, again short info for each department
GET departments/
    Here you need to decide if it makes sense to expose 
    a list of departments or not. 
    If not - leave only companies/X/departments method.

    Note, that you can also use query string to make this 
    method "searchable", like:
        /departments?company=1 - list of all departments for company 1
        /departments?type=support - all 'support' departments for all companies
GET departments/1
    Returns department 1 data

このようにして、ほとんどの質問に答えます-階層をすぐに「カット」し、URLスキームを内部データ構造にバインドしません。私たちは、従業員のIDを知っている場合たとえば、あなたは同じようにそれを問い合わせることを期待するemployees/:IDかのようなcompanies/:X/departments/:Y/employees/:ID

PUTvs POSTリクエストに関して、あなたの質問から、部分的な更新がデータに対してより効率的になると感じることは明らかです。したがって、私は単にPOSTsを使用します。

実際には、データの読み取り(GET要求)を実際にキャッシュする必要があり、データの更新にとってそれほど重要ではありません。また、実行するリクエストのタイプに関係なく、更新がキャッシュされないことがよくあります(サーバーが更新時間を自動的に設定する場合など、リクエストごとに異なります)。

更新:適切な「思考プロセス」について-HTTPに基づいているため、Webサイト構造を設計するときに通常の考え方を適用できます。この場合、上部には会社のリストがあり、会社の詳細とオフィス/部門へのリンクなどを表示する「会社の表示」ページへのリンクとともに、それぞれの簡単な説明を表示できます。


5

私見、あなたは要点を逃していると思います。

まず、REST APIとDBのパフォーマンスは無関係です。

REST APIは単なるインターフェースであり、内部でどのように処理するかをまったく定義していません。その背後にある任意のDB構造にマップできます。したがって:

  1. ユーザーが使いやすいようにAPIを設計する
  2. 合理的にスケーリングできるようにデータベースを設計します。
    • 適切なインデックスがあることを確認してください
    • オブジェクトを格納する場合は、オブジェクトが大きすぎないことを確認してください。

それでおしまい。

...そして最後に、これは時期尚早の最適化のようなにおいがします。シンプルに保ち、試してみて、必要に応じて調整してください。


2

質問1:私の考えは正しいですか。「階層をどこで切るか」は、私が行う必要がある典型的なエンジニアリング上の決定ですか

たぶん、あなたはそれを逆向きにしてしまうのではないかと心配しています。

だから、会社を返すとき、私は明らかに階層全体を返さない

それはまったく明白ではないと思います。サポートしているユースケースに適した会社の代表を返す必要があります。なんでだろう?APIが永続性コンポーネントに依存していることは本当に意味がありますか?実装でクライアントがその選択にさらされる必要がない点の一部ではありませんか?ある永続化コンポーネントを別の永続化コンポーネントに交換するときに、侵害されたAPIを保持しますか?

つまり、ユースケースが階層全体を必要としない場合、それを返す必要はありません。理想的な世界では、APIはクライアントの当面のニーズに完全に適合する会社の表現を生成します。

質問2:PUTは、きめの細かいリソースに使用されますか?

ほとんど-変更のべき等の性質をputとして実装することで伝えるのは良いことですが、HTTP仕様により、エージェントは実際に何が起こっているのかを推測することができます。

RFC 7231からのこのコメントに注意してください

ターゲットリソースに適用されたPUTリクエストは、他のリソースに悪影響を与える可能性があります。

つまり、プライマリリソース(エンティティ)で実行される副作用を説明するメッセージ(「きめの細かいリソース」)をPUTできます。実装がべき等であることを保証するために、いくつかの注意を払う必要があります。

質問3:これは、エンジニアリングの決定を「どこでカットするかを知る」だけなのですか、それとも何か不足していますか?

多分。エンティティのスコープが正しくないことを伝えようとしている可能性があります。

質問4:これはそうですか、そうでない場合は、RESTサービスを設計し、リソースの粒度、クエリの複雑さ、ネットワークの雑談の間の妥協点を探すときに適切な「思考プロセス」は何ですか?

リソーススキームをエンティティに密結合しようとしていて、永続性の選択がデザインを駆動するのではなく、その逆ではないように思える限り、これは私には適切ではありません。

HTTPは基本的にドキュメントアプリケーションです。ドメイン内のエンティティがドキュメントである場合、すばらしいですが、エンティティがドキュメントではない場合、考える必要があります。参照してくださいジム・ウェバーの話を:実践におけるREST、特に36m40sから始まります。

これが「きめの細かい」リソースアプローチです。


質問1への回答で、なぜ私は後退するかもしれないと言うのですか
user3748908

要件を永続化レイヤー制約に合わせるのではなく、逆にしようとしているように聞こえたからです。
VoiceOfUnreason

2

一般に、APIで公開される実装の詳細は必要ありません。mswとVoiceofUnreasonの回答はどちらもそれを伝えているため、理解することが重要です。

特にべき等について心配している場合は、驚きが最も少ないという原則に留意してください。投稿した記事(https://stormpath.com/blog/put-or-post/)のコメントをいくつか見てください。記事がべき等性をどのように提示するかについては、多くの意見の相違があります。私がこの記事から取り上げる大きなアイデアは、「同一のputリクエストは同じ結果をもたらすはずだ」ということです。つまり、会社名の更新をPUTした場合、その会社の名前は変更され、そのPUTの結果としてその会社の他の何も変更さません。5分後の同じリクエストでも同じ効果が得られます。

考えるべき興味深い質問(記事のgtrevgのコメントを確認してください):完全な更新を含むすべてのPUT要求は、クライアントがそれを指定していなくても、dateUpdatedを変更します。それはPUTリクエストをべき等性に違反させないでしょうか?

APIに戻ります。考える一般的なこと:

  • APIで公開される実装の詳細は避けてください
  • 実装が変更されても、APIは直感的で使いやすいはずです。
  • ドキュメントは重要です
  • パフォーマンスを向上させるためにAPIをワープしないようにしてください

1
マイナーはさておき:べき等は文脈的に拘束されます。例として、ロギングおよび監査プロセスはPUT内でトリガーでき、これらのアクションはべき等ではありません。ただし、これらは内部実装の詳細であり、サービスの抽象化を通じて公開される表現には影響しません。したがって、限りAPIが懸念され、PUTがあるべき等。
K.

0

エンジニアリング上の決定をどこで切り取るかについてのQ1について、エンティティの一意のIDを取得して、他の方法でバックエンドに必要な詳細を提供することはどうですか?たとえば、 "companies / 1 / department / 1"は独自の一意の識別子を持ち(または同じものを表すことができます)、階層を提供します。これを使用できます。

完全な肥大化した情報を含むQ3 on PUTの場合、更新されたフィールドにフラグを付け、その追加のメタデータ情報をサーバーに送信して、それらのフィールドのみをイントロスペクトおよび更新することができます。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.