Codest
  • Om os
  • Serviceydelser
    • Udvikling af software
      • Frontend-udvikling
      • Backend-udvikling
    • Staff Augmentation
      • Frontend-udviklere
      • Backend-udviklere
      • Dataingeniører
      • Cloud-ingeniører
      • QA-ingeniører
      • Andet
    • Det rådgivende
      • Revision og rådgivning
  • Industrier
    • Fintech og bankvirksomhed
    • E-commerce
    • Adtech
    • Sundhedsteknologi
    • Produktion
    • Logistik
    • Biler
    • IOT
  • Værdi for
    • ADMINISTRERENDE DIREKTØR
    • CTO
    • Leder af levering
  • Vores team
  • Casestudier
  • Ved hvordan
    • Blog
    • Møder
    • Webinarer
    • Ressourcer
Karriere Tag kontakt til os
  • Om os
  • Serviceydelser
    • Udvikling af software
      • Frontend-udvikling
      • Backend-udvikling
    • Staff Augmentation
      • Frontend-udviklere
      • Backend-udviklere
      • Dataingeniører
      • Cloud-ingeniører
      • QA-ingeniører
      • Andet
    • Det rådgivende
      • Revision og rådgivning
  • Værdi for
    • ADMINISTRERENDE DIREKTØR
    • CTO
    • Leder af levering
  • Vores team
  • Casestudier
  • Ved hvordan
    • Blog
    • Møder
    • Webinarer
    • Ressourcer
Karriere Tag kontakt til os
Pil tilbage GÅ TILBAGE
2022-03-22
Udvikling af software

Inkludering af underressourcer i en REST-agtig API

Codest

Krzysztof Buszewicz

Senior Software Engineer

Vi vil bygge en bogreol-app til at liste bøger med (eller uden) forfatterdata.

Hvad skal vi gøre?

Vi vil bygge en bogreol-app til at liste bøger med (eller uden) forfatterdata. Der vil være en enkelt #index action og nogle seeds. Dette vil være et eksempel på en app, der viser, hvordan du kan give en bruger kontrol over inkluderede underressourcer i en REST-agtig API.

"Acceptkriterier"

  • Brugeren kan liste bøgerne.
  • Brugeren kan bestå omfatter forespørgselsparameter for at indlæse tilknyttede ressourcer (forfatter).
  • omfatter Forespørgselsparameteren har formatet string: kommaseparerede ord, der repræsenterer indlejrede ressourcer.
  • Vi bør have nogle konstanter, der definerer, hvilke ressourcer der kan inkluderes i hvilke handlinger.

Værktøjer

Vi vil bruge blåprinter som serializer, fordi den er format-agnostisk og ret fleksibel. Dette er den eneste perle, vi vil tilføje til rails' standardværktøjssæt.

App'en

Lad os skabe et eksempel på en app. Vi tilføjer ikke et testframework, da det er uden for vores område.

skinner ny bogreol -T

Opret nu Forfatter model:

rails g model forfatternavn:string
#=> invoke active_record
#=> opret db/migrate/20211224084524_create_authors.rb
#=> opret app/models/author.rb

Og Bog:

rails g model book author:references title:string
# => påkald aktiv_rekord
# => opret db/migrate/20211224084614_create_books.rb
# => opret app/models/book.rb

Vi får brug for nogle frø:

# db/seeds.rb

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

Book.create(forfatter: dumas, titel: 'De tre musketerer')
Book.create(author: lewis, title: 'Løven, heksen og garderobeskabet')
Book.create(forfatter: martin, titel: 'Clean Code')

Og nu er vi klar til at køre migreringer og sætte db'en i gang:

rails db:migrate && rails db:seed

Lad os tilføje har_mange for bøger i Forfatter model:

# app/models/author.rb

class Author < ApplicationRecord
  has_many :bøger
end

Det er tid til at skrive en controller, der returnerer vores data. Vi vil bruge API namespace, så lad os først tilføje et akronym til bøjninger:

# config/initializers/inflections.rb

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

Okay, lad os tilføje vores serializer til Gemfile:

# Tilføj til Gemfile

gem 'blueprinter'

Og selvfølgelig installere den:

installation af bundt

Så kan vi bygge vores tegninger:

# app/blueprints/author_blueprint.rb

class AuthorBlueprint < Blueprinter::Base
  identifikator :id

  felter :navn
end
# app/blueprints/book_blueprint.rb

class BookBlueprint < Blueprinter::Base
  identifikator :id

  felter :title

  association :author, blueprint: ForfatterBlueprint
slutning

Tilføj en basecontroller til API:

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

modul API
  modul V1
    class BaseController < ActionController::API
    end
  slut
slut

Og udkastet til vores BooksController:

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

modul API
  modul V1
    class BooksController < BaseController
      def index
        bøger = Book.all

        render json: BookBlueprint.render(bøger)
      slut
    slut
  slut
slut

Vi skal selvfølgelig også definere routing:

# config/routes.rb

Rails.application.routes.draw do
  namespace :api do
    namespace :v1 do
      ressourcer :bøger, kun: :indeks
    end
  end
slut

Lad os teste, hvad vi har gjort indtil nu:

Skinner s 
krølle http://localhost:3000/api/v1/books

# => [{"id":1,"author":{"id":1,"name":"Alexandre Dumas"},"title":"De tre musketerer"},{"id":2,"author":{"id":2,"name":"C. S. Lewis"},"title":"Løven, heksen og garderobeskabet"},{"id":3,"author":{"id":3,"name":"The Lion, Witch and Wardrobe"}.S. Lewis"},"title":"Løven, heksen og garderobeskabet"},{"id":3,"author":{"id":3,"name":"Robert C. Martin"},"title":"Clean Kode"}]

Dataene ser ud til at være i orden, men hvad med logfilerne?

#-anmodningslogfiler (n+1)

Startede GET "/api/v1/books" for 127.0.0.1 kl. 2021-12-24 10:19:40 +0100
Behandlet af API::V1::BooksController#index som */*
  Indlæsning af bøger (0,1 ms) 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'
Afsluttet 200 OK på 6 ms (Views: 0,1 ms | ActiveRecord: 0,4 ms | Allokeringer: 3134)

Ved at bruge association i vores serializers introducerede vi n+1 problem. Vi ønsker at eliminere det ved at give brugeren kontrol over, hvad han anmoder om i dette endpoint. Så han skal enten kun kunne indlæse bøger eller sende includes-parameteren og også få forfattere, men helst uden n+1.

Lad os definere en konstant, der holder styr på, hvilke associationer af bøger brugeren kan inkludere i bøger#index handling:

# lib/constants/books/includes.rb

modul Konstanter
  modul Bøger
    modul Inkluderer
      ALLOWED = {
        index: %i[
          forfatter
        ].freeze
      }.freeze
    slut
  slut
slut

Dernæst definerer vi et navneområde til tomme objektkonstanter:

# lib/constants/empty.rb

modul Konstanter
  modul Tomt
    HASH = {}.freeze
  end
slut

Og her er vores hovedtjeneste til at tillade inkludering. Jeg tror, at koden er ret selvforklarende, nogle dele af magi er kun tildelt i #standard_ressourcer_nøgle og #standard_formål. Disse metoder er defineret for at give os mulighed for at kalde permit includes, der kun sender params i rails' controllere. Outputtet vil være den hash, der gemmer ægte for hver tilladt inklusion.

# 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
  TILLADTE_INKLUDERINGER = {
    bøger: 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)
  slut

  privat

  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
  slut

  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 medmindre 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

Nu skal vi bruge nøglerne til at indlæse includes og sende selve inlcudes-hash'en til serializeren:

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

modul API
  modul V1
    class BooksController < BaseController
      def index
        includes = PermitIncludes.new.call(params)
        bøger = Book.includes(includes.keys).all

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

Og det er sådan, vi skal tilpasse vores serializer - vi indlæser kun associationen, hvis den er inkluderet:

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

Lad os teste det igen:

Skinner s
krølle http://localhost:3000/api/v1/books
# => [{"id":1,"title":"The Three Musketeers"},{"id":2,"title":"The Lion, the Witch and the Wardrobe"},{"id":3,"title":"Clean Code"}].
# request logs (vi indlæser kun bøger)
Startede GET "/api/v1/books" for ::1 kl. 2021-12-24 10:33:41 +0100
Behandlet af API::V1::BooksController#index som */*
   (0.1ms) SELECT sqlite_version(*)
  ↳ app/controllers/api/v1/books_controller.rb:8:in `index'
  Indlæsning af bøger (0,1 ms) SELECT "books".* FROM "books"
  ↳ app/controllers/api/v1/books_controller.rb:8:in `index'
Afsluttet 200 OK på 9ms (Views: 0.1ms | ActiveRecord: 0.9ms | Allocations: 4548)

Godt, vi har ikke bestået inkluderingen, så vi fik kun bøger uden forfattere. Lad os nu anmode om dem:

curl 'http://localhost:3000/api/v1/books?includes=author'
# => [{"id":1,"author":{"id":1,"name":"Alexandre Dumas"},"title":"De tre musketerer"},{"id":2,"author":{"id":2,"name":"C.S. Lewis"},"title":"Løven, heksen og garderobeskabet"},{"id":3,"author":{"id":3,"name":"The Lion, Witch and Wardrobe"}. Lewis"},"title":"Løven, heksen og garderobeskabet"},{"id":3,"author":{"id":3,"name":"Robert C. Martin"},"title":"Clean Code"}]% 
#-anmodningslogfiler (elimineret n+1)

Startede GET "/api/v1/books?includes=author" for ::1 at 2021-12-24 10:38:23 +0100
Behandlet af API::V1::BooksController#index som */*
  Parametre: {"includes"=>"author"}
  Indlæsning af bøger (0,1 ms) 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'
Afsluttet 200 OK på 17 ms (Views: 0,1 ms | ActiveRecord: 0,7 ms | Allokeringer: 7373)

Sejt! Vi fik foreningen indlæst og elimineret n+1 problem. Tjenesten kan bruges til enhver ressource, og det eneste, vi vil gøre, er at tilføje tilladte konstanter i det rigtige format og tilføje dem til PermitIncludes::ALLOWED_INCLUDES.

Vi skal huske, at dette sandsynligvis skal bruges med paginering (og forsigtighed), fordi inkludering af associationer kan "spise" en masse hukommelse.

Relaterede artikler

Fintech

5 eksempler på den bedste brug af Ruby

Har du nogensinde undret dig over, hvad vi kan gøre med Ruby? Det er nok kun fantasien, der sætter grænser, men vi fortæller gerne om nogle mere eller mindre kendte tilfælde...

Codest
Pawel Muszynski Software Engineer
Udvikling af software

Polymorfisme i Ruby og GraphQL

I denne artikel vil jeg præsentere brugen af polymorfisme i GraphQL. Før jeg går i gang, er det dog værd at huske på, hvad polymorfisme og GraphQL er.

Lukasz Brzeszcz
E-commerce

Dilemmaer i forbindelse med cybersikkerhed: Læk af data

Førjulsræset er i fuld gang. I jagten på gaver til deres kære er folk i stigende grad villige til at "storme" onlinebutikker

Codest
Jakub Jakubowicz CTO og medstifter
Udvikling af software

En simpel Ruby-applikation fra bunden med Active Record

MVC er et designmønster, der opdeler ansvarsområderne i en applikation for at gøre den lettere at flytte rundt på. Rails følger dette designmønster pr. konvention.

Codest
Damian Watroba Software Engineer

Tilmeld dig vores vidensbase, og hold dig opdateret om ekspertisen fra it-sektoren.

    Om os

    The Codest - International softwareudviklingsvirksomhed med tech-hubs i Polen.

    Storbritannien - Hovedkvarter

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

    Polen - Lokale teknologiske knudepunkter

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

      Codest

    • Hjem
    • Om os
    • Serviceydelser
    • Casestudier
    • Ved hvordan
    • Karriere
    • Ordbog

      Serviceydelser

    • Det rådgivende
    • Udvikling af software
    • Backend-udvikling
    • Frontend-udvikling
    • Staff Augmentation
    • Backend-udviklere
    • Cloud-ingeniører
    • Dataingeniører
    • Andet
    • QA-ingeniører

      Ressourcer

    • Fakta og myter om at samarbejde med en ekstern softwareudviklingspartner
    • Fra USA til Europa: Hvorfor beslutter amerikanske startups sig for at flytte til Europa?
    • Sammenligning af Tech Offshore-udviklingsknudepunkter: Tech Offshore Europa (Polen), ASEAN (Filippinerne), Eurasien (Tyrkiet)
    • Hvad er de største udfordringer for CTO'er og CIO'er?
    • Codest
    • Codest
    • Codest
    • Privacy policy
    • Vilkår for brug af hjemmesiden

    Copyright © 2025 af The Codest. Alle rettigheder forbeholdes.

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