Codest
  • Om oss
  • Tjänster
    • Utveckling av programvara
      • Frontend-utveckling
      • Backend-utveckling
    • Staff Augmentation
      • Frontend-utvecklare
      • Backend-utvecklare
      • Dataingenjörer
      • Ingenjörer inom molntjänster
      • QA-ingenjörer
      • Övriga
    • Det rådgivande
      • Revision och rådgivning
  • Industrier
    • Fintech & bankverksamhet
    • E-commerce
    • Adtech
    • Hälsoteknik
    • Tillverkning
    • Logistik
    • Fordon
    • IOT
  • Värde för
    • VD OCH KONCERNCHEF
    • CTO
    • Leveranschef
  • Vårt team
  • Fallstudier
  • Vet hur
    • Blogg
    • Möten
    • Webbinarier
    • Resurser
Karriär Ta kontakt med oss
  • Om oss
  • Tjänster
    • Utveckling av programvara
      • Frontend-utveckling
      • Backend-utveckling
    • Staff Augmentation
      • Frontend-utvecklare
      • Backend-utvecklare
      • Dataingenjörer
      • Ingenjörer inom molntjänster
      • QA-ingenjörer
      • Övriga
    • Det rådgivande
      • Revision och rådgivning
  • Värde för
    • VD OCH KONCERNCHEF
    • CTO
    • Leveranschef
  • Vårt team
  • Fallstudier
  • Vet hur
    • Blogg
    • Möten
    • Webbinarier
    • Resurser
Karriär Ta kontakt med oss
Pil tillbaka GÅ TILLBAKA
2022-03-22
Utveckling av programvara

Inkludera underresurser i ett REST-liknande API

Codest

Krzysztof Buszewicz

Senior Software Engineer

Vi ska bygga en app för bokhyllor som listar böcker med (eller utan) författardata.

Vad ska vi göra?

Vi ska bygga en app för bokhyllor som listar böcker med (eller utan) författardata. Det kommer att finnas en enda #index action och några frön. Detta kommer att vara en exempelapp för att visa hur du kan ge en användare kontroll över inkluderade underresurser i ett REST-liknande API.

"Acceptanskriterier"

  • Användaren kan lista böckerna.
  • Användaren kan passera inkluderar frågeparameter för att ladda associerade resurser (författare).
  • inkluderar frågeparametern har formatet sträng: kommaseparerade ord som representerar nästlade resurser.
  • Vi borde ha några konstanter som definierar vilka resurser som kan inkluderas för vilken åtgärd.

Verktyg

Vi kommer att använda blåtryckare som serializer, eftersom den är formatagnostisk och ganska flexibel. Detta är en enda pärla som vi kommer att lägga till i Rails standardverktyg.

Appen

Låt oss skapa ett exempel på en app. Vi lägger inte till något testramverk eftersom det ligger utanför vårt område.

skenor ny bokhylla -T

Skapa nu Författaren modell:

rails g modell författarnamn:sträng
#=> invoke active_record
#=> skapa db/migrate/20211224084524_create_authors.rb
#=> skapa app/modeller/författare.rb

Och Bok:

rails g model book författare:referenser titel:sträng
# => invoke active_record
# => skapa db/migrate/20211224084614_create_books.rb
# => skapa app/modeller/bok.rb

Vi kommer att behöva några frön:

# db/seeds.rb

dumas = Författare.skapa(namn: 'Alexandre Dumas')
lewis = Författare.skapa(namn: 'C.S. Lewis')
martin = Författare.skapa(namn: 'Robert C. Martin')

Book.create(författare: dumas, titel: 'De tre musketörerna')
Book.create(författare: lewis, titel: 'Lejonet, häxan och garderoben')
Bok.skapa(författare: martin, titel: 'Clean Code')

Och nu är vi redo att köra migreringar och seed the db:

rails db:migrate && rails db:seed

Låt oss lägga till har_många för böcker i Författaren modell:

# app/modeller/author.rb

klass Författare < ApplicationRecord
  har_många :böcker
slut

Nu är det dags att skriva en controller som returnerar våra data. Vi kommer att använda API namnrymden, så låt oss först lägga till en akronym till böjningar:

# config/initializers/inflections.rb

ActiveSupport::Inflector.inflections(:en) do |inflect|
  inflektera.akronym 'API'
slut

Ok, låt oss lägga till vår serializer till Gemfil:

# Lägg till i Gemfile

gem 'blåskrivare'

Och naturligtvis installera den:

paketinstallation

Sedan kan vi bygga våra ritningar:

# app/blueprints/author_blueprint.rb

klass AuthorBlueprint < Blueprinter::Bas
  identifierare :id

  fält :namn
slut
# app/blueprints/book_blueprint.rb

klass BookBlueprint < Blåskrivare::Bas
  identifierare :id

  fält :title

  association :författare, blueprint: AuthorBlueprint
slut

Lägg till en baskontroll för API:

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

modul API
  modul V1
    klass BaseController < ActionController::API
    slut
  slut
slut

Och utkastet till version av vår BooksController:

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

modul API
  modul V1
    klass BooksController < BasController
      def index
        böcker = Bok.alla

        Rendera json: BookBlueprint.render(böcker)
      slut
    slut
  slut
slut

Vi måste naturligtvis också definiera routing:

# config/routes.rb

Rails.application.routes.draw do
  namnområde :api do
    namnområde :v1 do
      resurser :böcker, endast: :index
    slut
  slut
slut

Låt oss testa vad vi har gjort hittills:

räls s 
curl http://localhost:3000/api/v1/books

# => [{"id":1,"author":{"id":1,"name":"Alexandre Dumas"},"title":"De tre musketörerna"},{"id":2,"author":{"id":2,"name":"C.S. Lewis"},"title":"Lejonet, häxan och garderoben"},{"id":3,"author":{"id":3,"name":"Robert C. Martin"},"title":"Clean Kod"}]

Data verkar vara bra, men hur är det med loggar?

# förfrågningsloggar (n+1)

Startade GET "/api/v1/books" för 127.0.0.1 kl 2021-12-24 10:19:40 +0100
Bearbetas av API::V1::BooksController#index som */*
  Bokladdning (0,1 ms) SELECT "books".* FROM "books"
  ↳ app/controllers/api/v1/books_controller.rb:7:in `index'
  Laddning av författare (0,1 ms) SELECT "authors".* FROM "authors" WHERE "authors"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]] [["id", 1]]
  ↳ app/controllers/api/v1/books_controller.rb:7:in `index'
  Laddning av författare (0,1 ms) SELECT "authors".* FROM "authors" WHERE "authors"."id" = ? LIMIT ?  [["id", 2], ["LIMIT", 1]] [["id", 2], ["LIMIT", 1]]
  ↳ app/controllers/api/v1/books_controller.rb:7:in `index'
  Laddning av författare (0,1 ms) SELECT "authors".* FROM "authors" WHERE "authors"."id" = ? LIMIT ?  [["id", 3], ["LIMIT", 1]] [["id", 3], ["LIMIT", 1]]
  ↳ app/controllers/api/v1/books_controller.rb:7:in `index'
Avslutad 200 OK på 6 ms (Views: 0,1 ms | ActiveRecord: 0,4 ms | Allokeringar: 3134)

Genom att använda association i våra serializers införde vi n+1 problem. Vi vill eliminera det genom att ge användaren kontroll över vad han begär i denna endpoint. Så han bör kunna antingen ladda bara böcker, eller skicka parametern includes och få författare också, men helst utan n+1.

Låt oss definiera en konstant som kommer att hålla information om vilka associationer av böcker som användaren kan inkludera i böcker#index handling:

# lib/constants/books/includes.rb

modul Konstanter
  modul Böcker
    modul Inkluderar
      ALLOWED = {
        index: %i[
          författare
        ].freeze
      }.freeze
    slut
  slut
slut

Därefter definierar vi ett namnområde för tomma objektkonstanter:

# lib/constants/empty.rb

modul Konstanter
  modul Empty
    HASH = {}.freeze
  slut
slut

Och här är vår huvudtjänst för att tillåta inkluderingar. Jag tror att koden är ganska självförklarande, några bitar av magi är endast allokerade i #standard_resursnyckel och #förvaltat_syfte. Dessa metoder är definierade för att vi ska kunna anropa permit includes som bara skickar params i Rails controllers. Utdata kommer att vara den hash som lagrar sant för varje tillåten inkludering.

# app/services/permit_includes.rb

kräver 'constants/empty'
kräver 'constants/books/includes'

klassen PermitIncludes
  Empty = Konstanter::Empty

  COMMA = ','
  SLASH = '/'

  INCLUDES_FORMAT = /A[a-z]+(,[a-z]+)*z/.freeze
  TILLÅTNA_INKLUDERINGAR = {
    böcker::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 om inte includes_valid?(params)

    requested_includes = parse_includes(params)
    allowed_includes = filter_includes(requested_includes, resurser, syfte)

    tillåtna_inkluderingar.index_with(true)
  slut

  privat

  def default_resources_key(params)
    raise(ArgumentError, 'params :controller-nyckel måste vara en sträng') unless params[:controller].is_a?(String)

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

  def default_purpose(params)
    raise(ArgumentError, 'params :action-nyckeln måste vara en sträng') unless params[:action].is_a?(String)

    params[:åtgärd].to_sym
  slut

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

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

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

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

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

Nu måste vi använda nycklarna för att ladda inkluderingar och skicka själva inlcudes-hashningen till serialiseraren:

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

modul API
  modul V1
    klass BooksController < BasController
      def index
        includes = PermitIncludes.new.call(params)
        böcker = Bok.includes(includes.keys).all

        rendera json: BookBlueprint.render(böcker, inkluderar: inkluderar)
      slut
    slut
  slut
slut

Och det är så här vi måste justera vår serializer - vi laddar bara associationen om den ingår:

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

Låt oss testa det igen:

räls s
curl http://localhost:3000/api/v1/books
# => [{"id":1,"title":"De tre musketörerna"},{"id":2,"title":"Lejonet, häxan och garderoben"},{"id":3,"title":"Clean Code"}]
# förfrågningsloggar (vi laddar bara böcker)
Startade GET "/api/v1/books" för ::1 kl 2021-12-24 10:33:41 +0100
Bearbetas av API::V1::BooksController#index som */*
   (0.1ms) SELECT sqlite_version(*)
  ↳ app/controllers/api/v1/books_controller.rb:8:in `index'
  Bokladdning (0,1 ms) SELECT "books".* FROM "books"
  ↳ app/controllers/api/v1/books_controller.rb:8:in `index'
Avslutad 200 OK på 9ms (Views: 0.1ms | ActiveRecord: 0.9ms | Allocations: 4548)

Bra, vi har inte passerat inkluderingarna så vi fick bara böcker, utan författare. Låt oss nu begära dem:

curl 'http://localhost:3000/api/v1/books?includes=author'
# => [{"id":1,"author":{"id":1,"name":"Alexandre Dumas"},"title":"De tre musketörerna"},{"id":2,"author":{"id":2,"name":"C.S. Lewis"},"titel":"Lejonet, häxan och garderoben"},{"id":3,"författare":{"id":3,"namn":"Robert C. Martin"},"titel":"Clean Code"}]% 
# begäran loggar (eliminerad n+1)

Startade GET "/api/v1/books?includes=author" för ::1 kl 2021-12-24 10:38:23 +0100
Bearbetas av API::V1::BooksController#index som */*
  Parametrar: {"includes"=>"author {"includes"=>"author"}
  Bokladdning (0,1 ms) SELECT "books".* FROM "books"
  ↳ app/controllers/api/v1/books_controller.rb:8:in `index'
  Laddning av författare (0,2 ms) SELECT "authors".* FROM "authors" WHERE "authors"."id" IN (?, ?, ?) [["id", 1], ["id", 2], ["id", 3]]
  ↳ app/controllers/api/v1/books_controller.rb:8:in `index'
Avslutad 200 OK på 17 ms (Views: 0,1 ms | ActiveRecord: 0,7 ms | Allokeringar: 7373)

Coolt! Vi fick föreningen laddad och eliminerad n+1 problem. Tjänsten kan användas för vilken resurs som helst, allt vi vill göra är att lägga till tillåtna inlcudes-konstanter i rätt format och lägga till dem i PermitIncludes::ALLOWED_INCLUDES.

Vi måste komma ihåg att detta förmodligen bör användas med paginering (och försiktighet) eftersom inkludering av associationer kan "äta" mycket minne.

Relaterade artiklar

Fintech

5 exempel på hur Ruby används på bästa sätt

Har du någonsin undrat vad vi kan göra med Ruby? Tja, himlen är förmodligen gränsen, men vi är glada att prata om några mer eller mindre kända fall ...

Codest
Pawel Muszynski Software Engineer
Utveckling av programvara

Polymorfism i Ruby och GraphQL

I den här artikeln kommer jag att presentera användningen av polymorfism i GraphQL. Innan jag börjar är det dock värt att påminna om vad polymorfism och GraphQL är.

Lukasz Brzeszcz
E-commerce

Dilemman inom cybersäkerhet: Dataläckage

Julruschen är i full gång. I jakt på presenter till sina nära och kära är människor alltmer villiga att "storma" onlinebutiker

Codest
Jakub Jakubowicz CTO och medgrundare
Utveckling av programvara

En enkel Ruby-applikation från grunden med Active Record

MVC är ett designmönster som delar upp ansvaret för en applikation för att göra det lättare att flytta runt. Rails följer detta designmönster enligt konvention.

Codest
Damian Watroba Software Engineer

Prenumerera på vår kunskapsbas och håll dig uppdaterad om expertisen från IT-sektorn.

    Om oss

    The Codest - Internationellt mjukvaruutvecklingsföretag med teknikhubbar i Polen.

    Förenade kungariket - Huvudkontor

    • Kontor 303B, 182-184 High Street North E6 2JA
      London, England

    Polen - Lokala tekniknav

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

      Codest

    • Hem
    • Om oss
    • Tjänster
    • Fallstudier
    • Vet hur
    • Karriär
    • Ordbok

      Tjänster

    • Det rådgivande
    • Utveckling av programvara
    • Backend-utveckling
    • Frontend-utveckling
    • Staff Augmentation
    • Backend-utvecklare
    • Ingenjörer inom molntjänster
    • Dataingenjörer
    • Övriga
    • QA-ingenjörer

      Resurser

    • Fakta och myter om att samarbeta med en extern partner för mjukvaruutveckling
    • Från USA till Europa: Varför väljer amerikanska startup-företag att flytta till Europa?
    • Jämförelse av Tech Offshore Development Hubs: Tech Offshore Europa (Polen), ASEAN (Filippinerna), Eurasien (Turkiet)
    • Vilka är de största utmaningarna för CTO:er och CIO:er?
    • Codest
    • Codest
    • Codest
    • Privacy policy
    • Användarvillkor för webbplatsen

    Copyright © 2025 av The Codest. Alla rättigheter reserverade.

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