How to create a proxy entity instance from a non @Id field?

I keep two identifiers for my entities -

  1. An internal id which is also the primary key of the corresponding table and the foreign key for referencing tables.
  2. An external id that is generally provided by clients and is guaranteed to be unique.
@Entity
class Student {
@Id
Integer id; // internal id
String email; // external id

String name;
@ManyToOne
School school  // Assume that school also has an internal and external id 
              // student is the owning side of this relationship,
             // In database terms, table student has school_id
}

This seems fine, however, say I get a request from a client to create a new Student record, this is generally of the form (foo_email, bar_name, school_external_id). Even even though the School entity’s instance is not changing, it will need to be fetched from the db to create its association with the newly created Student instance.

Student student = new Student(foo_email, bar_name);
School school = // code to fetch from db
student.setSchool(school);
// code to persist student instance

There is no option to create a proxy session.load(School.class, id) instance because that can only be done with the field marked as @Id i.e. the internal id.

From a design perspective it’s better to have separate internal and external ids, for example - the external id email might need to be updated in the future.
From a performance perspective, it’s better to not load a School instance if it’s not intended to be read from or written to.

Using Hibernate, I feel like I have to give up on either one of them, or maybe I am looking at the problem from the wrong perspective. Is there a better way?

Your problem has nothing to do with Hibernate ORM but is a general modeling problem. You chose to model your association based on the primary key of School which is usually fine. You could have just as well modeled your association with the natural key to point to this external_id with e.g.

@Entity
class Student {
  @Id
  Integer id; // internal id
  String email; // external id

  String name;
  @ManyToOne
  @JoinColumn(name = "school_external_id", referenceColumnName = "external_id")
  School school;
}

But even if you do this, you might still have to load the School entity, because Hibernate ORM has no support for proxies based on anything other than the @Id.
So if you want to avoid the load, you’d have to model the natural key as primary key.

You can somewhat ease the pain of loading an entity by enabling second level caching which has an option for caching the mapping from a natural id to the primary key.
By using the natural id API with e.g. Session.byNaturalId(School.class).using("externalId", ..).getReference(), Hibernate ORM will consult this natural id resolutions cache to figure out the primary key and create a proxy for that.