Eu tenho seguido alguns tutoriais sobre como criar APIs REST, mas ainda tenho alguns grandes pontos de interrogação. Todos esses tutoriais mostram recursos com hierarquias relativamente simples, e eu gostaria de saber como os princípios usados naqueles se aplicam a um mais complexo. Além disso, eles permanecem em um nível arquitetural muito alto. Eles quase não mostram nenhum código relevante, muito menos a camada de persistência. Estou especialmente preocupado com a carga / desempenho do banco de dados, como Gavin King disse :
você economizará esforço se prestar atenção ao banco de dados em todas as etapas do desenvolvimento
Digamos que meu aplicativo forneça treinamento para Companies
. Companies
tem Departments
e Offices
. Departments
tem Employees
. Employees
tem Skills
e Courses
, e certas Level
habilidades são necessárias para poder assinar alguns cursos. A hierarquia é a seguinte, mas com:
-Companies
-Departments
-Employees
-PersonalInformation
-Address
-Skills (quasi-static data)
-Levels (quasi-static data)
-Courses
-Address
-Offices
-Address
Caminhos seria algo como:
companies/1/departments/1/employees/1/courses/1
companies/1/offices/1/employees/1/courses/1
Buscando um Recurso
Então, ok, ao retornar de uma empresa, eu, obviamente, não retornam toda a hierarquia companies/1/departments/1/employees/1/courses/1
+ companies/1/offices/../
. Posso devolver uma lista de links para os departamentos ou departamentos expandidos e ter que tomar a mesma decisão neste nível: devolvo uma lista de links para os funcionários do departamento ou para os funcionários expandidos? Isso dependerá do número de departamentos, funcionários etc.
Pergunta 1 : Meu pensamento está correto: "onde cortar a hierarquia" é uma decisão típica de engenharia que preciso tomar?
Agora, digamos que, quando solicitado GET companies/id
, decido retornar uma lista de links para a coleção de departamentos e as informações expandidas do escritório. Minhas empresas não têm muitos escritórios, portanto, juntar-me às mesas Offices
e Addresses
não deve ser um grande problema. Exemplo de resposta:
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
}
}
}
No nível do código, isso implica que (usando o Hibernate, não tenho certeza de como é com outros provedores, mas acho que é praticamente o mesmo) não colocarei uma coleção Department
como campo na minha Company
classe, porque:
- Como já disse, não estou carregando
Company
, então não quero carregá-lo avidamente - E se eu não carregá-lo com entusiasmo, é melhor removê-lo, porque o contexto de persistência será fechado depois que eu carrego uma empresa e não faz sentido tentar carregá-lo depois (
LazyInitializationException
).
Depois, colocarei um Integer companyId
na Department
classe, para poder adicionar um departamento a uma empresa.
Além disso, preciso obter os IDs de todos os departamentos. Outro sucesso no DB, mas não pesado, então tudo bem. O código pode se parecer com:
@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
}
Atualizando um Recurso
Para a operação de atualização, posso expor um terminal com PUT
ou POST
. Como quero que eu PUT
seja idempotente, não posso permitir atualizações parciais . Mas, se quiser modificar o campo de descrição da empresa, preciso enviar toda a representação do recurso. Isso parece muito inchado. O mesmo ao atualizar um funcionário PersonalInformation
. Eu não acho que faz sentido ter que enviar todo o Skills
+ Courses
junto com isso.
Pergunta 2 : O PUT é usado apenas para recursos refinados?
Eu vi nos logs que, ao mesclar uma entidade, o Hibernate executa várias SELECT
consultas. Eu acho que é apenas para verificar se alguma coisa mudou e atualizar as informações necessárias. Quanto mais alta a entidade na hierarquia, mais pesadas e complexas as consultas. Mas algumas fontes aconselham o uso de recursos granulares grosseiros . Então, novamente, precisarei verificar quantas tabelas são demais e encontrar um compromisso entre a granularidade de recursos e a complexidade da consulta ao banco de dados.
Pergunta 3 : Esse é apenas mais um "saber onde cortar" a decisão de engenharia ou estou perdendo alguma coisa?
Pergunta 4 : Esse é, ou não, qual é o "processo de reflexão" correto ao projetar um serviço REST e procurar um compromisso entre granularidade de recurso, complexidade de consulta e propriedade da rede?
Respostas:
Eu acho que você tem complexidade, porque você está começando com excesso de complicações:
Em vez disso, introduziria um esquema de URL mais simples como este:
Dessa forma, ele responde à maioria das suas perguntas - você "corta" a hierarquia imediatamente e não vincula seu esquema de URL à estrutura de dados interna. Por exemplo, se soubermos o ID do funcionário, você esperaria consultá-lo, gostando
employees/:ID
ou nãocompanies/:X/departments/:Y/employees/:ID
?Em relação às solicitações
PUT
vsPOST
, na sua pergunta, é claro que você acha que as atualizações parciais serão mais eficientes para seus dados. Então, eu apenas usariaPOST
s.Na prática, você realmente deseja armazenar em cache leituras de dados (
GET
solicitações) e é menos importante para atualizações de dados. E a atualização geralmente não pode ser armazenada em cache, independentemente do tipo de solicitação que você faz (como se o servidor definir automaticamente o tempo de atualização - será diferente para cada solicitação).Atualização: em relação ao "processo de raciocínio" correto - uma vez que é baseado em HTTP, podemos aplicar o modo de pensar regular ao projetar a estrutura do site. Nesse caso, na parte superior, podemos ter uma lista de empresas e mostrar uma breve descrição de cada uma com um link para a página "visualizar empresa", onde mostramos detalhes da empresa e links para escritórios / departamentos e assim por diante.
fonte
IMHO, acho que você está perdendo o ponto.
Primeiro, o desempenho da API REST e do banco de dados não está relacionado .
A API REST é apenas uma interface , ela não define de maneira alguma como você faz coisas ocultas. Você pode mapeá-lo para qualquer estrutura de banco de dados que desejar por trás dele. Portanto:
É isso aí.
... e, por último, isso cheira a otimização prematura. Mantenha-o simples, experimente e adapte-o, se necessário.
fonte
Talvez - eu estaria preocupado que você estivesse fazendo isso ao contrário, no entanto.
Eu não acho isso óbvio. Você deve devolver representações da empresa apropriadas para os casos de uso que você está apoiando. Por que você não? Realmente faz sentido que a API dependa do componente de persistência? Não faz parte do ponto que os clientes não precisam ser expostos a essa escolha na implementação? Você preservará uma API comprometida quando trocar um componente de persistência por outro?
Dito isto, se seus casos de uso não precisarem de toda a hierarquia, não será necessário retorná-la. Em um mundo ideal, a API produziria representações da empresa perfeitamente adequadas às necessidades imediatas do cliente.
Praticamente - é bom comunicar a natureza idempotente de uma mudança implementando como put, mas a especificação HTTP permite que os agentes façam suposições sobre o que realmente está acontecendo.
Observe este comentário do RFC 7231
Em outras palavras, você pode COLOCAR uma mensagem (um "recurso refinado") que descreve um efeito colateral a ser executado no seu recurso primário (entidade). Você precisa tomar alguns cuidados para garantir que sua implementação seja idempotente.
Talvez. Pode estar tentando dizer que suas entidades não estão no escopo correto.
Isso não parece certo para mim, na medida em que parece que você está tentando acoplar seu esquema de recursos a suas entidades e está deixando sua escolha de persistência direcionar seu design, e não o contrário.
O HTTP é fundamentalmente um aplicativo de documento; se as entidades em seu domínio são documentos, então é ótimo - mas as entidades não são documentos, você precisa pensar. Veja a palestra de Jim Webber : REST in Practice, particularmente a partir dos 36m40s.
Essa é a sua abordagem de recursos "refinada".
fonte
Em geral, você não deseja nenhum detalhe de implementação exposto na API. As respostas msw e VoiceofUnreason estão comunicando isso, por isso é importante entender.
Lembre-se do princípio de menos espanto , principalmente porque você está preocupado com a idempotência. Dê uma olhada em alguns dos comentários no artigo que você postou ( https://stormpath.com/blog/put-or-post/ ); existe muita discordância sobre como o artigo apresenta a idempotência. A grande idéia que eu retiraria do artigo é que "solicitações de venda idênticas devem causar resultados idênticos". Ou seja, se você colocar uma atualização no nome de uma empresa, o nome da empresa muda e nada mais muda para essa empresa como resultado dessa PUT. A mesma solicitação 5 minutos depois deve ter o mesmo efeito.
Uma pergunta interessante para se pensar (confira o comentário do gtrevg no artigo): qualquer solicitação de PUT, incluindo uma atualização completa, modificará a dateUpdated mesmo que um cliente não a especifique. Isso não faria nenhuma solicitação de PUT violar a idempotência?
Então, de volta à API. Aspectos gerais a serem considerados:
fonte
Para o seu Q1 sobre onde cortar as decisões de engenharia, que tal escolher o ID exclusivo de uma entidade que, de outra forma, forneceria os detalhes necessários no back-end? Por exemplo, "empresas / 1 / departamento / 1" terá um identificador exclusivo por si só (ou podemos ter um para representar o mesmo) para fornecer a hierarquia, você pode usá-lo.
Para o seu Q3 em PUT com informações completas, você pode sinalizar os campos que foram atualizados e enviar essas informações adicionais de metadados ao servidor para que você possa examinar e atualizar esses campos sozinho.
fonte