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. Companiestem Departmentse Offices. Departmentstem Employees. Employeestem Skillse Courses, e certas Levelhabilidades 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 Officese Addressesnã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 Departmentcomo campo na minha Companyclasse, 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 companyIdna Departmentclasse, 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 PUTou POST. Como quero que eu PUTseja 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+ Coursesjunto 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 SELECTconsultas. 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?