web-dev-qa-db-ja.com

複雑なREST DBのパフォーマンスを考慮したAPIを設計する方法は?

REST APIの設計方法に関するいくつかのチュートリアルを行ってきましたが、まだ大きな疑問符があります。これらのチュートリアルはすべて、比較的単純な階層のリソースを示しています。それらで使用されている原則は、より複雑なものに適用されます。さらに、それらは非常に高い/アーキテクチャレベルに留まります。永続化レイヤーはもちろん、関連するコードはほとんど表示されません。GavinKingのように、データベースの負荷/パフォーマンスについては特に心配しています 言った

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

アプリケーションがCompaniesのトレーニングを提供するとします。 CompaniesにはDepartmentsOfficesがあります。 DepartmentsEmployeesを持っています。 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、部署のコレクションへのリンクのリストと展開されたオフィス情報を返すことにしました。私の会社には多くのオフィスがないので、テーブルOfficesAddressesを結合することは大したことではありません。応答の例:

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

次に、Integer companyIdDepartmentクラスに追加して、部署を会社に追加できるようにします。

また、すべての部門の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サービスを設計するときの適切な「思考プロセス」は何ですか?リソースの粒度、クエリの複雑さ、ネットワークの雑談の間の妥協点を探していますか?

8
user3748908

あなたは複雑すぎて始めているので、あなたは複雑だと思います:

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

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

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

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

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

7
Boris Serebrov

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

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

REST APIはインターフェースにすぎず、内部での操作方法をまったく定義していません。それを好きなDB構造にマッピングできます。

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

それでおしまい。

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

5
dagnelies

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

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

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

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

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

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

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

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

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

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

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

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

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

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

HTTPは基本的にドキュメントアプリケーションです。ドメイン内のエンティティがドキュメントである場合はすばらしいですが、エンティティがドキュメントでない場合は、考える必要があります。 Jim Webber's トークを参照してください:REST実際には、特に36分40秒から始まります。

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

2
VoiceOfUnreason

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

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

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

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

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

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

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

0
itsraghz