window.pipedriveLeadboosterConfig = { base: 'leadbooster-chat.pipedrive.com', companyId: 11580370, playbookUuid: '22236db1-6d50-40c4-b48f-8b11262155be', version: 2, } ;(function () { var w = window if (w.LeadBooster) { console.warn('LeadBooster już istnieje') } else { w.LeadBooster = { q: [], on: function (n, h) { this.q.push({ t: 'o', n: n, h: h }) }, trigger: function (n) { this.q.push({ t: 't', n: n }) }, } } })() Uwzględnianie pod-zasobów w API REST-ish - The Codest
The Codest
  • O nas
  • Nasze Usługi
    • Software Development
      • Frontend Development
      • Backend Development
    • Zespoły IT
      • Programiści frontendowi
      • Backend Dev
      • Inżynierowie danych
      • Inżynierowie rozwiązań chmurowych
      • Inżynierowie QA
      • Inne
    • Konsultacje IT
      • Audyt i doradztwo
  • Branże
    • Fintech i bankowość
    • E-commerce
    • Adtech
    • Healthtech
    • Produkcja
    • Logistyka
    • Motoryzacja
    • IOT
  • Wartość dla
    • CEO
    • CTO
    • Delivery Managera
  • Nasz zespół
  • Case Studies
  • Nasze Know How
    • Blog
    • Meetups
    • Webinary
    • Raporty
Kariera Skontaktuj się z nami
  • O nas
  • Nasze Usługi
    • Software Development
      • Frontend Development
      • Backend Development
    • Zespoły IT
      • Programiści frontendowi
      • Backend Dev
      • Inżynierowie danych
      • Inżynierowie rozwiązań chmurowych
      • Inżynierowie QA
      • Inne
    • Konsultacje IT
      • Audyt i doradztwo
  • Wartość dla
    • CEO
    • CTO
    • Delivery Managera
  • Nasz zespół
  • Case Studies
  • Nasze Know How
    • Blog
    • Meetups
    • Webinary
    • Raporty
Kariera Skontaktuj się z nami
Strzałka w tył WSTECZ
2022-03-22
Software Development

Włączanie pod-zasobów do interfejsu API REST-ish

The Codest

Krzysztof Buszewicz

Senior Software Engineer

Zbudujemy aplikację półki z książkami, aby wyświetlić listę książek z (lub bez) danych autorów.

Co zrobimy?

Zbudujemy aplikację półki z książkami, aby wyświetlić listę książek z (lub bez) danych autorów. Dostępna będzie jedna aplikacja #index i kilka nasion. Będzie to przykładowa aplikacja, która pokaże, w jaki sposób można dać użytkownikowi kontrolę nad dołączonymi plikami. pod-zasoby w interfejsie API typu REST.

"Kryteria akceptacji"

  • Użytkownik może wymienić książki.
  • Użytkownik może przekazać zawiera parametr zapytania w celu załadowania powiązanych zasobów (autor).
  • zawiera ma format ciągu znaków: słowa oddzielone przecinkami, reprezentujące zagnieżdżone zasoby.
  • Powinniśmy mieć pewne stałe, które definiują, które zasoby mogą być uwzględnione w danej akcji.

Narzędzia

Będziemy używać blueprinter jako serializatora, ponieważ jest on niezależny od formatu i dość elastyczny. Jest to jedyny klejnot, który dodamy do standardowego zestawu narzędzi Rails.

Aplikacja

Stwórzmy przykładową aplikację. Nie będziemy dodawać frameworka testowego, ponieważ jest on poza naszym zakresem.

szyny nowy regał -T

Teraz utwórz Autor model:

rails g model author name:string
#=> wywołaj active_record
#=> create db/migrate/20211224084524_create_authors.rb
#=> create app/models/author.rb

I Książka:

rails g model book author:references title:string
# => wywołaj active_record
# => create db/migrate/20211224084614_create_books.rb
# => create app/models/book.rb

Będziemy potrzebować nasion:

# db/seeds.rb

dumas = Author.create(name: 'Alexandre Dumas')
lewis = Author.create(name: 'C.S. Lewis')
martin = Author.create(name: 'Robert C. Martin')

Book.create(author: dumas, title: 'Trzej muszkieterowie')
Book.create(autor: lewis, tytuł: 'Lew, czarownica i stara szafa')
Book.create(author: martin, title: 'Czysty kod')

Teraz jesteśmy gotowi do uruchomienia migracji i zasiania bazy danych:

rails db:migrate && rails db:seed

Dodajmy has_many dla książek w Autor model:

# app/models/author.rb

class Author < ApplicationRecord
  has_many :books
end

Nadszedł czas, aby napisać kontroler, który zwróci nasze dane. Użyjemy API więc najpierw dodajmy akronim do fleksji:

# config/initializers/inflections.rb

ActiveSupport::Inflector.inflections(:en) do |inflect|
  inflect.acronym 'API'
end

Ok, dodajmy nasz serializator do Gemfile:

# Dodaj do Gemfile

gem 'blueprinter'

I oczywiście zainstalować:

instalacja pakietu

Następnie możemy zbudować nasze plany:

# app/blueprints/author_blueprint.rb

class AuthorBlueprint < Blueprinter::Base
  identyfikator :id

  fields :name
end
# app/blueprints/book_blueprint.rb

class BookBlueprint < Blueprinter::Base
  identyfikator :id

  fields :title

  association :author, blueprint: AuthorBlueprint
end

Dodaj kontroler bazowy dla API:

# app/controllers/api/v1/base_controller.rb

moduł API
  moduł V1
    class BaseController < ActionController::API
    end
  end
end

A wersja robocza naszego BooksController:

# app/controllers/api/v1/books_controller.rb

moduł API
  moduł V1
    class BooksController < BaseController
      def index
        books = Book.all

        render json: BookBlueprint.render(books)
      end
    end
  end
end

Musimy też oczywiście zdefiniować routing:

# config/routes.rb

Rails.application.routes.draw do
  namespace :api do
    namespace :v1 do
      resources :books, only: :index
    end
  end
end

Przetestujmy to, co zrobiliśmy do tej pory:

szyny s 
curl http://localhost:3000/api/v1/books

# => [{"id":1, "author":{"id":1, "name": "Alexandre Dumas"}, "title": "Trzej muszkieterowie"},{"id":2, "author":{"id":2, "name": "C.S. Lewis"}, "title": "Lew, czarownica i stara szafa"},{"id":3, "author":{"id":3, "name": "Robert C. Martin"}, "title": "Clean Kod"}]

Dane wydają się być w porządku, a co z logami?

Rejestry żądań # (n+1)

Rozpoczęto GET "/api/v1/books" dla 127.0.0.1 o 2021-12-24 10:19:40 +0100
Przetwarzanie przez API::V1::BooksController#index jako */*
  Ładowanie książki (0.1ms) SELECT "books".* FROM "books"
  ↳ app/controllers/api/v1/books_controller.rb:7:in `index'
  Author Load (0.1ms) SELECT "authors".* FROM "authors" WHERE "authors". "id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  ↳ app/controllers/api/v1/books_controller.rb:7:in `index'
  Author Load (0.1ms) SELECT "authors".* FROM "authors" WHERE "authors". "id" = ? LIMIT ?  [["id", 2], ["LIMIT", 1]]
  ↳ app/controllers/api/v1/books_controller.rb:7:in `index'
  Author Load (0.1ms) SELECT "authors".* FROM "authors" WHERE "authors". "id" = ? LIMIT ?  [["id", 3], ["LIMIT", 1]]
  ↳ app/controllers/api/v1/books_controller.rb:7:in `index'
Ukończono 200 OK w 6 ms (Views: 0,1 ms | ActiveRecord: 0,4 ms | Allocations: 3134)

Używając asocjacji w naszych serializatorach wprowadziliśmy n+1 problem. Chcemy go wyeliminować, dodając użytkownikowi kontrolę nad tym, czego żąda w tym punkcie końcowym. Powinien więc być w stanie albo załadować tylko książki, albo przekazać parametr includes i pobrać również autorów, ale najlepiej bez parametru n+1.

Zdefiniujmy stałą, która będzie przechowywać informacje o tym, jakie asocjacje książek użytkownik może umieścić w books#index działanie:

# lib/constants/books/includes.rb

moduł Constants
  moduł Books
    moduł Includes
      ALLOWED = {
        index: %i[
          autor
        freeze
      }.freeze
    koniec
  end
end

Następnie definiujemy przestrzeń nazw dla pustych stałych obiektu:

# lib/constants/empty.rb

moduł Constants
  moduł Empty
    HASH = {}.freeze
  end
end

A oto nasza główna usługa zezwalająca na dołączanie. Myślę, że kod jest dość oczywisty, niektóre elementy magia są przydzielane tylko w #default_resources_key i #default_purpose. Metody te są zdefiniowane, aby umożliwić nam wywoływanie zezwoleń obejmujących przekazywanie tylko parametrów w kontrolerach rails. Wynikiem będzie hash, który przechowuje prawda dla każdego dozwolonego włączenia.

# app/services/permit_includes.rb

require 'constants/empty'
require 'constants/books/includes'

class PermitIncludes
  Empty = Constants::Empty

  COMMA = ','
  SLASH = '/'

  INCLUDES_FORMAT = /A[a-z]+(,[a-z]+)*z/.freeze
  ALLOWED_INCLUDES = {
    books: Constants::Books::Includes::ALLOWED
  }.freeze

  def call(params, resources: default_resources_key(params), purpose: default_purpose(params))
    return Empty::HASH unless includes_sent?(params)
    return Empty::HASH unless includes_valid?(params)

    requested_includes = parse_includes(params)
    allowed_includes = filter_includes(requested_includes, resources, purpose)

    allowed_includes.index_with(true)
  end

  private

  def default_resources_key(params)
    raise(ArgumentError, 'params :controller key must be a string') unless params[:controller].is_a?(String)

    params[:controller].split(SLASH).last&.to_sym
  end

  def default_purpose(params)
    raise(ArgumentError, 'params :action key must be a string') unless params[:action].is_a?(String)

    params[:action].to_sym
  end

  def includes_sent?(params)
    params.key?(:includes)
  end

  def includes_valid?(params)
    return false unless params[:includes].is_a?(String)

    params[:includes].match?(INCLUDES_FORMAT)
  end

  def parse_includes(params)
    params[:includes].split(COMMA).map(&:to_sym)
  end

  def filter_includes(requested_includes, resources_key, purpose)
    requested_includes & ALLOWED_INCLUDES[resources_key][purpose]
  end
end

Teraz musimy użyć kluczy do załadowania include'ów i przekazać sam hash do serializera:

# app/controllers/api/v1/books_controller.rb

moduł API
  moduł V1
    class BooksController < BaseController
      def index
        includes = PermitIncludes.new.call(params)
        books = Book.includes(includes.keys).all

        render json: BookBlueprint.render(books, includes: includes)
      end
    end
  end
end

I właśnie w ten sposób musimy dostosować nasz serializator - ładujemy asocjację tylko wtedy, gdy jest dołączona:

# app/blueprints/book_blueprint.rb
class BookBlueprint (_field_name, _book, options) {
                         options[:includes] && options[:includes][:author]
                       }
end

Przetestujmy to jeszcze raz:

szyny s
curl http://localhost:3000/api/v1/books
# => [{"id":1, "title": "Trzej muszkieterowie"},{"id":2, "title": "Lew, czarownica i stara szafa"},{"id":3, "title": "Czysty kod"}].
Dzienniki żądań # (ładujemy tylko książki)
Uruchomiono GET "/api/v1/books" dla ::1 o 2021-12-24 10:33:41 +0100
Przetwarzanie przez API::V1::BooksController#index jako */*
   (0.1ms) SELECT sqlite_version(*)
  ↳ app/controllers/api/v1/books_controller.rb:8:in `index'
  Ładowanie książki (0.1ms) SELECT "books".* FROM "books"
  ↳ app/controllers/api/v1/books_controller.rb:8:in `index'
Ukończono 200 OK w 9ms (Views: 0.1ms | ActiveRecord: 0.9ms | Allocations: 4548)

Dobrze, nie przeszliśmy przez include, więc mamy tylko książki, bez autorów. Zażądajmy ich teraz:

curl 'http://localhost:3000/api/v1/books?includes=author'
# => [{"id":1, "author":{"id":1, "name": "Alexandre Dumas"}, "title": "Trzej muszkieterowie"},{"id":2, "author":{"id":2, "name": "C.S. Lewis"}, "title": "Lew, czarownica i stara szafa"},{"id":3, "author":{"id":3, "name": "Robert C. Martin"}, "title": "Czysty kod"}]% 
Dzienniki żądań # (wyeliminowane n+1)

Uruchomiono GET "/api/v1/books?includes=author" dla ::1 o 2021-12-24 10:38:23 +0100
Przetwarzanie przez API::V1::BooksController#index jako */*
  Parametry: {"includes"=>"author"}
  Ładowanie książki (0.1ms) SELECT "books".* FROM "books"
  ↳ app/controllers/api/v1/books_controller.rb:8:in `index'
  Author Load (0.2ms) SELECT "authors".* FROM "authors" WHERE "authors". "id" IN (?, ?, ?) [["id", 1], ["id", 2], ["id", 3]]
  ↳ app/controllers/api/v1/books_controller.rb:8:in `index'
Ukończono 200 OK w 17 ms (Views: 0,1 ms | ActiveRecord: 0,7 ms | Allocations: 7373)

Super! Stowarzyszenie zostało załadowane i wyeliminowane n+1 problem. Usługa może być używana dla dowolnego zasobu, wszystko co chcemy zrobić, to dodać dozwolone stałe w odpowiednim formacie i dodać je do PermitIncludes::ALLOWED_INCLUDES.

Musimy pamiętać, że powinno to być prawdopodobnie używane z paginacją (i ostrożnością), ponieważ włączenie skojarzeń może "zjeść" dużo pamięci.

Powiązane artykuły

Fintech

5 przykładów najlepszego wykorzystania Rubiego

Czy kiedykolwiek zastanawiałeś się, co możemy zrobić z Ruby? Cóż, niebo jest prawdopodobnie granicą, ale chętnie porozmawiamy o kilku mniej lub bardziej znanych przypadkach...

The Codest
Paweł Muszyński Software Engineer
Software Development

Polimorfizm w Ruby i GraphQL

W tym artykule przedstawię zastosowanie polimorfizmu w GraphQL. Zanim jednak zacznę, warto przypomnieć czym są polimorfizm i GraphQL.

Łukasz Brzeszcz
E-commerce

Dylematy cyberbezpieczeństwa: Wycieki danych

Przedświąteczna gorączka trwa w najlepsze. W poszukiwaniu prezentów dla najbliższych ludzie coraz chętniej "szturmują" sklepy internetowe

The Codest
Jakub Jakubowicz CTO & Współzałożyciel
Software Development

Prosta aplikacja Ruby od podstaw z Active Record

MVC to wzorzec projektowy, który dzieli obowiązki aplikacji, aby ułatwić poruszanie się po niej. Railsy podążają za tym wzorcem projektowym zgodnie z konwencją.

The Codest
Damian Watroba Software Engineer

Subskrybuj naszą bazę wiedzy i bądź na bieżąco!

    O nas

    The Codest - Międzynarodowa firma programistyczna z centrami technologicznymi w Polsce.

    Wielka Brytania - siedziba główna

    • Office 303B, 182-184 High Street North E6 2JA
      Londyn, Anglia

    Polska - lokalne centra technologiczne

    • Fabryczna Office Park, Aleja
      Pokoju 18, 31-564 Kraków
    • Brain Embassy, Konstruktorska
      11, 02-673 Warszawa, Polska

      The Codest

    • Strona główna
    • O nas
    • Nasze Usługi
    • Case Studies
    • Nasze Know How
    • Kariera
    • Słownik

      Nasze Usługi

    • Konsultacje IT
    • Software Development
    • Backend Development
    • Frontend Development
    • Zespoły IT
    • Backend Dev
    • Inżynierowie rozwiązań chmurowych
    • Inżynierowie danych
    • Inne
    • Inżynierowie QA

      Raporty

    • Fakty i mity na temat współpracy z zewnętrznym partnerem programistycznym
    • Z USA do Europy: Dlaczego amerykańskie startupy decydują się na relokację do Europy?
    • Porównanie centrów rozwoju Tech Offshore: Tech Offshore Europa (Polska), ASEAN (Filipiny), Eurazja (Turcja)
    • Jakie są największe wyzwania CTO i CIO?
    • The Codest
    • The Codest
    • The Codest
    • Privacy policy
    • Warunki korzystania z witryny

    Copyright © 2025 by The Codest. Wszelkie prawa zastrzeżone.

    pl_PLPolish
    en_USEnglish de_DEGerman sv_SESwedish da_DKDanish nb_NONorwegian fiFinnish fr_FRFrench arArabic it_ITItalian jaJapanese ko_KRKorean es_ESSpanish nl_NLDutch etEstonian elGreek pl_PLPolish