Still smells like in such a case the developer avoids the complications of abstraction or OOP by making the user deal with it. That's bad API design due to putting ideology before practicality or ergonomics.
IEnumerable<User> getExpiredUsers(DbSet<User> users)
=> users.Where(u => u.ExpiresAt < DateTime.UtcNow);
Such simple logical expressions (called expression trees) get converted to SQL queriesI'm building a new project, Typegres, on this same philosophy for the modern web stack (TypeScript/PostgreSQL).
We can take your example a step further and blur the lines between database columns and computed business logic, building the "functional core" right in the model:
// This method compiles directly to a SQL expression
class User extends db.User {
isExpired() {
return this.expiresAt.lt(now());
}
}
const expired = await User.where((u) => u.isExpired());
Here's the playground if that looks interesting: https://typegres.com/play/The query shape would be:
active_users = Query(User).filter(active=True)
That gives you an expression object which only encodes an intent. Then you have the option to make basic templates you can build from: def active_users_except(exclude):
return active_users.filter(User.id.not_in(exclude)
...where `exclude` is any set-valued expression.Then at execution time, the objects representing query expressions are rendered into queries and sent to the database:
exclude_criterion = rude_users() # A subquery expression
polite_active_users = load_records(
active_users_except(exclude_criterion)
)
With SQLAlchemy, I'll usually make simple dataclasses for the query shapes because "get_something" or "select_something" names are confusing when they're not really for actions. @dataclass
class ActiveUsers(QueryTemplate):
active_if: Expression = User.active == true()
@classmethod
excluding(cls, bad_set):
return cls(
and_(
User.active == true(),
User.id.not_in(bad_set)
)
)
@property
def query(self):
return Query(User).filter(self.active_if)
load_records(
ActiveUsers.excluding(select_alice | select_bob).query
)