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?