Apa praktik terbaik untuk sumber daya bersarang REST?


301

Sejauh yang saya tahu setiap sumber daya individu harus hanya memiliki satu jalur kanonik . Jadi, dalam contoh berikut, apa yang akan menjadi pola URL yang baik?

Ambil contoh, perwakilan dari Perusahaan. Dalam contoh hipotetis ini, setiap perusahaan memiliki 0 atau lebih departemen dan masing-masing departemen memiliki 0 atau lebih karyawan.

Departemen tidak dapat eksis tanpa perusahaan terkait.

Seorang karyawan tidak dapat ada tanpa departemen terkait.

Sekarang saya akan menemukan representasi alami dari pola sumber daya.

  • /companies Kumpulan perusahaan - Menerima penempatan untuk perusahaan baru. Dapatkan untuk seluruh koleksi.
  • /companies/{companyId}Perusahaan individual. Terima DAPATKAN, PUT dan HAPUS
  • /companies/{companyId}/departmentsMenerima POST untuk item baru. (Menciptakan departemen dalam perusahaan.)
  • /companies/{companyId}/departments/{departmentId}/
  • /companies/{companyId}/departments/{departmentId}/employees
  • /companies/{companyId}/departments/{departmentId}/employees/{empId}

Mengingat kendala, di setiap bagian, saya merasa ini masuk akal jika agak bersarang.

Namun, kesulitan saya datang jika saya ingin mendaftar ( GET) semua karyawan di semua perusahaan.

Pola sumber daya untuk yang paling dekat akan dipetakan ke /employees(Pengumpulan semua karyawan)

Apakah itu berarti saya seharusnya /employees/{empId}juga karena jika demikian maka ada dua URI untuk mendapatkan sumber daya yang sama?

Atau mungkin seluruh skema harus diratakan tetapi itu berarti bahwa karyawan adalah objek tingkat atas yang bersarang.

Pada tingkat dasar /employees/?company={companyId}&department={deptId}mengembalikan pandangan yang sama persis tentang karyawan sebagai pola yang paling bersarang.

Apa praktik terbaik untuk pola URL di mana sumber daya dimiliki oleh sumber daya lain tetapi harus dapat digunakan secara terpisah?


1
Ini hampir persis masalah oppsite yang dijelaskan dalam stackoverflow.com/questions/7104578/… meskipun jawabannya mungkin terkait. Kedua pertanyaan adalah tentang kepemilikan tetapi contoh itu menyiratkan bahwa objek tingkat atas bukan miliknya.
Wes

1
Persis apa yang aku pikirkan. Untuk kasus penggunaan yang diberikan solusi Anda tampaknya baik-baik saja, tetapi bagaimana jika hubungannya adalah agregasi daripada komposisi? Masih berjuang untuk mencari tahu apa praktik terbaik di sini ... Juga, apakah solusi ini menyiratkan hanya penciptaan hubungan, misalnya orang yang sudah ada dipekerjakan atau apakah itu membuat objek orang?
Jakob O.

Itu menciptakan seseorang dalam contoh fiktif saya. Alasan saya menggunakan istilah-istilah domain itu adalah contoh yang cukup dimengerti, meskipun meniru masalah saya yang sebenarnya. Sudahkah Anda melihat melalui pertanyaan terkait yang mungkin membuat Anda lebih terhambat untuk hubungan aggragasi.
Wes

Saya telah membagi pertanyaan saya menjadi jawaban dan pertanyaan.
Wes

Jawaban:


152

Apa yang Anda lakukan benar. Secara umum bisa ada banyak URI dengan sumber daya yang sama - tidak ada aturan yang mengatakan Anda tidak boleh melakukan itu.

Dan umumnya, Anda mungkin perlu mengakses item secara langsung atau sebagai bagian dari sesuatu yang lain - jadi struktur Anda masuk akal bagi saya.

Hanya karena karyawan dapat diakses di bawah departemen:

company/{companyid}/department/{departmentid}/employees

Bukan berarti mereka tidak dapat diakses di bawah perusahaan juga:

company/{companyid}/employees

Yang akan mengembalikan karyawan untuk perusahaan itu. Itu tergantung pada apa yang dibutuhkan oleh klien Anda - itulah yang harus Anda rancang.

Tetapi saya berharap bahwa semua penangan URL menggunakan kode dukungan yang sama untuk memenuhi permintaan sehingga Anda tidak menggandakan kode.


11
Ini menunjukkan semangat RESTful, tidak ada aturan yang mengatakan Anda harus atau tidak boleh melakukannya jika hanya Anda yang mempertimbangkan sumber daya yang berarti terlebih dahulu. Tetapi lebih lanjut, saya bertanya-tanya apa praktik terbaik untuk tidak menduplikasi kode dalam skenario seperti itu.
abookyun

13
@abookyun jika Anda membutuhkan kedua rute, maka kode pengontrol yang berulang di antara keduanya dapat diabstraksi menjadi objek layanan.
bgcode

Ini tidak ada hubungannya dengan REST. REST tidak peduli tentang bagaimana Anda menyusun bagian jalur URL Anda ... semua yang dipedulikannya valid, mudah-mudahan URI tahan lama ...
redben

Mengemudi pada jawaban ini, saya pikir api mana pun di mana segmen dinamis semua pengidentifikasi unik tidak perlu menangani beberapa segmen dinamis ( /company/3/department/2/employees/1). Jika api menyediakan cara untuk mendapatkan setiap sumber daya, maka membuat setiap permintaan tersebut dapat dilakukan di pustaka sisi klien atau sebagai titik akhir satu kali yang menggunakan kembali kode.
maks

1
Meskipun tidak ada larangan, saya anggap lebih elegan untuk hanya memiliki satu jalur ke sumber daya - membuat semua model mental lebih sederhana. Saya juga lebih suka bahwa URI tidak mengubah jenis sumber dayanya jika ada sarang. misalnya /company/*hanya mengembalikan sumber daya perusahaan dan tidak mengubah jenis sumber sama sekali. Tak satu pun dari ini ditentukan oleh REST - yang umumnya kurang ditentukan - hanya preferensi pribadi.
kashif

174

Saya sudah mencoba kedua strategi desain - titik akhir bersarang dan tidak bersarang. Saya telah menemukan bahwa:

  1. Jika sumber daya bersarang memiliki kunci utama dan Anda tidak memiliki kunci utama induknya, struktur bersarang mengharuskan Anda untuk mendapatkannya, meskipun sistem sebenarnya tidak memerlukannya.

  2. titik akhir bersarang biasanya membutuhkan titik akhir redundan. Dengan kata lain, Anda akan lebih sering membutuhkan titik akhir tambahan / karyawan sehingga Anda bisa mendapatkan daftar karyawan lintas departemen. Jika Anda memiliki / karyawan, apa sebenarnya yang dibeli / perusahaan / departemen / karyawan Anda?

  3. titik akhir bersarang tidak berevolusi dengan baik. Misalnya Anda mungkin tidak perlu mencari karyawan sekarang tetapi Anda mungkin nanti dan jika Anda memiliki struktur bersarang, Anda tidak punya pilihan selain menambahkan titik akhir lain. Dengan desain non-bersarang, Anda hanya menambahkan lebih banyak parameter, yang lebih sederhana.

  4. terkadang sumber daya bisa memiliki banyak tipe orang tua. Menghasilkan banyak titik akhir semua mengembalikan sumber daya yang sama.

  5. titik akhir yang berlebihan membuat dokumen lebih sulit untuk ditulis dan juga membuat api lebih sulit untuk dipelajari.

Singkatnya, desain non-bersarang tampaknya memungkinkan skema titik akhir yang lebih fleksibel dan sederhana.


24
Sangat menyegarkan untuk menemukan jawaban ini. Saya telah menggunakan titik akhir bersarang selama beberapa bulan sekarang setelah diajarkan bahwa itu adalah "jalan yang benar". Saya sampai pada semua kesimpulan yang sama yang Anda sebutkan di atas. Jauh lebih mudah dengan desain yang tidak bersarang.
user3344977

6
Anda tampaknya membuat daftar beberapa kerugian sebagai kerugian. "Hanya menjejalkan lebih banyak parameter ke titik akhir tunggal" membuat API lebih sulit untuk didokumentasikan dan dipelajari, bukan sebaliknya. ;-)
Drenmi

4
Bukan penggemar jawaban ini. Tidak perlu memperkenalkan titik akhir yang berlebihan hanya karena Anda telah menambahkan sumber daya bersarang. Ini juga bukan masalah untuk memiliki sumber daya yang sama dikembalikan oleh banyak orang tua, asalkan orang tua itu benar-benar memiliki sumber daya bersarang. Bukan masalah untuk mendapatkan sumber daya orang tua untuk belajar bagaimana berinteraksi dengan sumber daya bersarang. API REST yang dapat ditemukan yang baik harus melakukan ini.
Scottm

3
@Scottm - Salah satu kelemahan dari sumber daya bersarang yang saya temui adalah bahwa hal itu dapat menyebabkan pengembalian data yang salah jika id sumber daya induk salah / tidak sesuai. Dengan asumsi tidak ada masalah otorisasi, diserahkan kepada implementasi api untuk memverifikasi bahwa sumber daya bersarang memang adalah anak dari sumber daya induk yang disahkan. Jika pemeriksaan ini tidak dikodekan, respons api bisa salah mengarah ke korupsi. Apa yang kamu pikirkan?
Andy Dufresne

1
Anda tidak memerlukan nomor induk perantara jika semua sumber daya akhir memiliki nomor unik. Misalnya, untuk mendapatkan karyawan melalui id Anda memiliki GET / perusahaan / departemen / karyawan / {empId} atau untuk mendapatkan semua karyawan di perusahaan 123 Anda memiliki GET / perusahaan / 123 / departemen / karyawan / Menjaga jalurnya secara hierarkis membuatnya lebih jelas bagaimana Anda bisa mendapatkan sumber daya perantara untuk memfilter / membuat / memodifikasi dan membantu dengan kemampuan menemukan menurut saya.
PaulG

77

Saya telah memindahkan apa yang telah saya lakukan dari pertanyaan ke sebuah jawaban di mana lebih banyak orang akan melihatnya.

Apa yang telah saya lakukan adalah memiliki titik akhir pembuatan di titik akhir bersarang, Titik akhir kanonik untuk memodifikasi atau meminta item tidak berada di sumber daya bersarang .

Jadi dalam contoh ini (cukup cantumkan titik akhir yang mengubah sumber daya)

  • POST /companies/ membuat perusahaan baru mengembalikan tautan ke perusahaan yang dibuat.
  • POST /companies/{companyId}/departments ketika suatu departemen diletakkan menciptakan departemen baru mengembalikan tautan ke /departments/{departmentId}
  • PUT /departments/{departmentId} memodifikasi departemen
  • POST /departments/{deparmentId}/employees membuat karyawan baru mengembalikan tautan ke /employees/{employeeId}

Jadi ada sumber daya level root untuk setiap koleksi. Namun buat adalah di objek yang dimiliki .


4
Saya telah datang dengan jenis desain yang sama juga. Saya pikir itu intuitif untuk membuat hal-hal seperti ini "di mana mereka berada", tetapi kemudian masih dapat mendaftar secara global. Terlebih lagi ketika ada hubungan di mana sumber daya HARUS memiliki orang tua. Maka menciptakan sumber daya itu secara global tidak membuatnya jelas, tetapi melakukannya di sub-sumber daya seperti ini masuk akal.
Joakim

Saya kira Anda menggunakan POSTmakna PUT, dan sebaliknya.
Gerardo Lima

Sebenarnya tidak ada Catatan bahwa saya tidak menggunakan Id yang ditetapkan sebelumnya untuk pembuatan karena server dalam kasus ini bertanggung jawab untuk mengembalikan id (dalam tautan). Oleh karena itu menulis POST benar (tidak bisa mendapatkan implementasi yang sama). Namun put mengubah seluruh sumber daya tetapi masih tersedia di lokasi yang sama jadi saya PUT. PUT vs POST adalah masalah yang berbeda dan juga kontroversial. Misalnya stackoverflow.com/questions/630453/put-vs-post-in-rest
Wes

@ Kami Bahkan saya lebih suka memodifikasi metode kata kerja di bawah induknya. Tapi, apakah Anda melihat melewati parameter permintaan untuk sumber daya global diterima dengan baik? Mis: POST / departemen dengan parameter kueri company = company-id
Ayyappa

1
@Mohamad. Jika Anda berpikir bahwa cara lain lebih mudah dalam memahami dan menerapkan kendala maka jangan ragu untuk memberikan jawaban. Ini tentang membuat pemetaan eksplisit dalam kasus ini. Itu bisa bekerja dengan parameter tetapi sungguh itulah pertanyaannya. Apakah cara terbaiknya.
Wes

35

Saya sudah membaca semua jawaban di atas tetapi sepertinya mereka tidak memiliki strategi yang sama. Saya menemukan artikel yang bagus tentang praktik terbaik dalam API Desain dari Dokumen Microsoft . Saya pikir Anda harus merujuk.

Dalam sistem yang lebih kompleks, mungkin tergoda untuk menyediakan URI yang memungkinkan klien untuk menavigasi melalui beberapa tingkat hubungan, seperti /customers/1/orders/99/products.Namun, tingkat kompleksitas ini mungkin sulit dipertahankan dan tidak fleksibel jika hubungan antar sumber daya berubah di masa depan. Sebagai gantinya, cobalah untuk menjaga URI relatif sederhana . Setelah aplikasi memiliki referensi ke sumber daya, seharusnya dimungkinkan untuk menggunakan referensi ini untuk menemukan item yang terkait dengan sumber daya itu. Permintaan sebelumnya dapat diganti dengan URI /customers/1/ordersuntuk menemukan semua pesanan untuk pelanggan 1, dan kemudian /orders/99/productsuntuk menemukan produk dalam pesanan ini.

.

Tip

Hindari membutuhkan URI sumber daya yang lebih kompleks daripada collection/item/collection.


3
Referensi yang Anda berikan luar biasa bersama dengan poin yang Anda yakini tidak membuat URI kompleks.
vicco

Jadi ketika saya ingin membuat tim untuk pengguna, apakah itu POST / tim (userId dalam diri seseorang) atau POST / pengguna /: id / tim
coinhndp

@coinhndp Hai, Anda harus menggunakan POST / tim dan Anda bisa mendapatkan userId setelah mengesahkan token akses. Maksud saya ketika Anda membuat sesuatu yang Anda butuhkan kode otorisasi, kan? Saya tidak tahu kerangka apa yang Anda gunakan tetapi saya yakin Anda bisa mendapatkan userId di pengontrol API. Misalnya: Di ASP.NET API, panggil RequestContext.Principal dari dalam metode di ApiController. Di Spring Secirity, SecurityContextHolder.getContext (). GetAuthentication (). GetPrincipal () akan membantu Anda. Di AWS NodeJS Lambda, yaitu cognito: nama pengguna di objek tajuk.
Long Nguyen

Jadi apa yang salah dengan POST / pengguna /: id / tim. Saya pikir ini direkomendasikan dalam Dokumen Microsoft yang Anda posting di atas
coinhndp

@coinhndp Jika Anda membuat tim sebagai admin, itu bagus. Tapi, sebagai pengguna normal, saya tidak tahu mengapa Anda perlu userId di jalur? Saya kira kita memiliki user_A dan user_B, bagaimana menurut Anda jika user_A dapat membuat tim baru untuk pengguna_B jika pengguna_A memanggil POST / pengguna / user_B / tim. Jadi, tidak perlu melewati userId dalam hal ini, userId bisa mendapatkan setelah otorisasi. Tapi, tim /: id / proyek bagus untuk membuat hubungan antara tim & proyek misalnya.
Long Nguyen

10

Tampilan URL Anda tidak ada hubungannya dengan REST. Apapun itu. Ini sebenarnya adalah "detail implementasi". Jadi sama seperti bagaimana Anda memberi nama variabel Anda. Mereka harus unik dan tahan lama.

Jangan buang waktu terlalu banyak untuk hal ini, buat saja pilihan dan tetaplah konsisten / konsisten. Misalnya jika Anda menggunakan hierarki maka Anda melakukannya untuk semua sumber daya Anda. Jika Anda menggunakan parameter kueri ... dll seperti halnya penamaan konvensi dalam kode Anda.

Kenapa begitu ? Sejauh yang saya tahu, API "RESTful" harus dapat dijelajahi (Anda tahu ... "Hypermedia sebagai Engine of Application State"), oleh karena itu klien API tidak peduli tentang seperti apa URL Anda asalkan URL mereka valid (tidak ada SEO, tidak ada manusia yang perlu membaca "url ramah" itu, kecuali mungkin untuk debugging ...)

Seberapa bagus / mudah dimengerti URL dalam REST API hanya menarik bagi Anda sebagai pengembang API, bukan klien API, seperti halnya nama variabel dalam kode Anda.

Yang paling penting adalah klien API Anda tahu cara menginterpretasikan tipe media Anda. Misalnya tahu bahwa:

  • tipe media Anda memiliki properti tautan yang mencantumkan tautan yang tersedia / terkait.
  • Setiap tautan diidentifikasi oleh suatu hubungan (seperti halnya browser tahu bahwa tautan [rel = "stylesheet"] berarti lembar gaya atau rel = favico adalah tautan ke favicon ...)
  • dan diketahui apa arti hubungan itu ("perusahaan" berarti daftar perusahaan, "pencarian" berarti url templated untuk melakukan pencarian pada daftar sumber daya, "departemen" berarti departemen dari sumber daya saat ini)

Di bawah ini adalah contoh pertukaran HTTP (badan berada di yaml karena lebih mudah untuk menulis):

Permintaan

GET / HTTP/1.1
Host: api.acme.io
Accept: text/yaml, text/acme-mediatype+yaml

Respons: daftar tautan ke sumber daya utama (perusahaan, orang, apa pun ...)

HTTP/1.1 200 OK
Date: Tue, 05 Apr 2016 15:04:00 GMT
Last-Modified: Tue, 05 Apr 2016 00:00:00 GMT
Content-Type: text/acme-mediatype+yaml

# body: this is your API's entrypoint (like a homepage)  
links:
  # could be some random path https://api.acme.local/modskmklmkdsml
  # the only thing the API client cares about is the key (or rel) "companies"
  companies: https://api.acme.local/companies
  people: https://api.acme.local/people

Permintaan: tautan ke perusahaan (menggunakan body.links.companies respons sebelumnya)

GET /companies HTTP/1.1
Host: api.acme.local
Accept: text/yaml, text/acme-mediatype+yaml

Respons: sebagian daftar perusahaan (di bawah item), sumber daya tersebut berisi tautan terkait, seperti tautan untuk mendapatkan pasangan perusahaan berikutnya (body.links.next) tautan lain (templated) untuk pencarian (body.links.search)

HTTP/1.1 200 OK
Date: Tue, 05 Apr 2016 15:06:00 GMT
Last-Modified: Tue, 05 Apr 2016 00:00:00 GMT
Content-Type: text/acme-mediatype+yaml

# body: representation of a list of companies
links:
  # link to the next page
  next: https://api.acme.local/companies?page=2
  # templated link for search
  search: https://api.acme.local/companies?query={query} 
# you could provide available actions related to this resource
actions:
  add:
    href: https://api.acme.local/companies
    method: POST
items:
  - name: company1
    links:
      self: https://api.acme.local/companies/8er13eo
      # and here is the link to departments
      # again the client only cares about the key department
      department: https://api.acme.local/companies/8er13eo/departments
  - name: company2
    links:
      self: https://api.acme.local/companies/9r13d4l
      # or could be in some other location ! 
      department: https://api2.acme.local/departments?company=8er13eo

Jadi seperti yang Anda lihat jika Anda membuka tautan / hubungan dengan cara Anda menyusun bagian jalur URL Anda tidak memiliki nilai apa pun untuk klien API Anda. Dan jika Anda mengkomunikasikan struktur URL Anda kepada klien Anda sebagai dokumentasi, maka Anda tidak melakukan REST (atau setidaknya bukan Level 3 sesuai " model jatuh tempo Richardson ")


7
"Betapa bagus / mudah dimengerti sebuah URL dalam REST API hanya menarik bagi Anda sebagai pengembang API, bukan klien API, seperti halnya nama variabel dalam kode Anda." Mengapa ini TIDAK menarik? Ini sangat penting, jika ada orang selain Anda yang juga menggunakan API. Ini adalah bagian dari pengalaman pengguna, jadi saya akan mengatakan ini sangat penting bahwa ini mudah dimengerti untuk pengembang klien API. Membuat hal-hal menjadi lebih mudah dipahami dengan menghubungkan sumber daya dengan jelas tentu saja merupakan bonus (level 3 di url yang Anda berikan). Semuanya harus intuitif dan logis dengan hubungan yang jelas.
Joakim

1
@ Joakim Jika Anda membuat API level 3 sisanya (Hypertext As The Engine Of Application State), maka struktur jalur url sama sekali tidak menarik bagi klien (asalkan valid). Jika Anda tidak bertujuan untuk level 3, maka ya, itu penting dan harus dapat ditebak. Tapi REST yang sebenarnya adalah level 3. Artikel yang bagus: martinfowler.com/articles/richardsonMaturityModel.html
redben

4
Saya keberatan untuk pernah membuat API atau UI yang tidak ramah pengguna bagi manusia. Level 3 atau tidak, saya setuju menghubungkan sumber daya adalah ide bagus. Tetapi menyarankan melakukan hal itu "memungkinkan untuk mengubah skema URL" adalah tidak berhubungan dengan kenyataan, dan bagaimana orang menggunakan API. Jadi ini rekomendasi yang buruk. Tapi pasti yang terbaik dari semua dunia semua orang akan berada di Level 3 REST. Saya menggabungkan hyperlink DAN menggunakan skema URL yang dapat dipahami secara manusia. Level 3 tidak mengecualikan yang pertama, dan satu HARUS peduli menurut pendapat saya. Artikel bagus :)
Joakim

Seseorang tentu saja harus peduli demi pemeliharaan dan masalah lainnya, saya pikir Anda kehilangan titik jawaban saya: cara tampilan url tidak pantas banyak berpikir dan Anda harus "hanya membuat pilihan dan tetap berpegang pada itu / menjadi konsisten "seperti yang saya katakan dalam jawaban. Dan dalam kasus REST API, setidaknya pendapat saya, keramahan pengguna tidak ada di url, sebagian besar di (tipe media) Pokoknya saya harap Anda mengerti maksud saya :)
redben

9

Saya tidak setuju dengan jalan seperti ini

GET /companies/{companyId}/departments

Jika Anda ingin mendapatkan departemen, saya pikir lebih baik menggunakan sumber daya / departemen

GET /departments?companyId=123

Saya kira Anda memiliki companiestabel dan departmentstabel kemudian kelas untuk memetakannya dalam bahasa pemrograman yang Anda gunakan. Saya juga berasumsi bahwa departemen dapat dilampirkan ke entitas lain selain perusahaan, sehingga sumber daya / departemen mudah, lebih mudah untuk memiliki sumber daya yang dipetakan ke tabel dan juga Anda tidak memerlukan banyak titik akhir karena Anda dapat menggunakan kembali

GET /departments?companyId=123

untuk segala jenis pencarian, misalnya

GET /departments?name=xxx
GET /departments?companyId=123&name=xxx
etc.

Jika Anda ingin membuat departemen,

POST /departments

sumber daya harus digunakan dan badan permintaan harus berisi ID perusahaan (jika departemen hanya dapat ditautkan dengan satu perusahaan).


1
Bagi saya, ini adalah pendekatan yang dapat diterima hanya jika objek bersarang masuk akal sebagai objek atom. Jika tidak, itu tidak masuk akal untuk memisahkan mereka.
Simme

Inilah yang saya katakan, jika Anda juga ingin dapat mengambil departemen, artinya jika Anda akan menggunakan titik akhir / departemen.
Maxime Laval

2
Mungkin juga masuk akal untuk mengizinkan departemen untuk dimasukkan melalui pemuatan malas saat mengambil perusahaan, misalnya GET /companies/{companyId}?include=departments, karena hal ini memungkinkan perusahaan dan departemennya untuk diambil dalam satu permintaan HTTP. Fraktal melakukan ini dengan sangat baik.
Matthew Daly

1
Saat Anda menyiapkan ACL, Anda mungkin ingin membatasi /departmentstitik akhir hanya dapat diakses oleh admin, dan meminta setiap perusahaan mengakses departemen mereka sendiri hanya melalui `/ company / {companyId} / department`
Cuzox

@MatthewDaly OData juga melakukannya dengan baik dengan $ expand
Rob Grant

1

Rails memberikan solusi untuk ini: bersarang dangkal .

Saya pikir ini bagus karena ketika Anda berurusan langsung dengan sumber daya yang dikenal, tidak perlu menggunakan rute bersarang, seperti yang telah dibahas dalam jawaban lain di sini.

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.