What SQL speaking objects keep silence about

CommentsSubscribe (atom)Report an issueBack

SQL-speaking objects concept was introduced by Yegor Bugayenko in his blog. He proposed it as more maintainable alternative for working with relational databases, compared to ORMs. From that time, it became one of the most argued concept in Elegant Objects. Some of the sceptics blamed SQL speaking objects for verbosity and boilerplate, bringing back SQL and suboptimal examples from the post, others constantly keep drawing analogies between SQL speaking objects and Active Record pattern.

A speech on SQL speaking objects was the first time I heard about Elegant Objects. Back in the days, I already felt regret for the most ORMs I faced in Java ecosystem, but the speech about such cardinal ORM replacement haven’t convinced me at all. It took me a year to look at SQL speaking objects from a different angle, and from my experience I can tell that the topic is not revealed enough by the original blogpost and speech.

So, lets dig deeper.

What is the key difference between SQL speaking objects and Active Record?

To finally answer this question, lets bring the definition of Active Records:

An object that wraps a row in a database table or view, encapsulates the database access, and adds domain logic on that data.

www.martinfowler.com

Take a close look, how much the root “data” is met there. The pattern is deeply data-centric. There can be different opinions on that matter, but personally, I don’t yet claim it wrong. As I stated in my previous posts, there should be a strict line between data and objects — what is virtue for code, is flaw for data, and vice versa. Active Record, as a part of the data world, is designed to expose the data:

PostAR post = new PostAR();
post.title = "What SQL speaking objects keep silence about";
post.date = new Date();
post.save();

Assuming that there is a table posts with columns name and date, the code above will just create a new record in it. Simple as it sounds. Staying standalone, Active Record won’t cause you any troubles. But the troubles will come later, when you couple other components to it:

public class RSSFeed {
    private final Path path;
    private final List<PostAR> posts;

    public RSSFeed(Path path, List<PostAR> posts) {
        this.path = path;
        this.posts = posts;
    }

    public final void generate() {
        // Document below is generated by means of dom4j.
        Document document = new DOMDocument();
        DOMElement root = new DOMElement("rdf");
        DOMElement channel = new DOMElement("channel");
        for(Post post : posts) {
            DOMElement item = new DOMElement("item");
            DOMElement title = new DOMElement("title");
            title.setText(post.title);
            DOMElement date = new DOMElement("date");
            date.setText(post.date.toString());
            item.add(title);
            item.add(date);
            channel.add(item);
        }
        root.add(channel);
        document.add(root);
        try(Writer writer = new FileWriter(path.toFile())) {
            document.write(writer);
        } catch(IOException ex) {
            throw new RuntimeException(ex);
        }
    }
}

RSSFeed is tightly coupled to the PostAR Active Record class. At the same time, all it needs is title and date. It doesn’t need the whole PostAR with method save() — the coupling is obviously exhaustive here. Moreover, Post, as a member of data world, will inevitably be changing together with posts table, causing RSSFeed to be broken. Coupling on data model directly is always a risk.

What’s the role of SQL speaking objects here?

Speaking back to SQL speaking objects. If we look to example of such objects from the post, it may seem like they are no different from Active Records, DTOs, JPA entities or other data-centric artifacts:

final class PgPost implements Post {
  private final Source dbase;
  private final int number;
  public PgPost(DataSource data, int id) {
    this.dbase = data;
    this.number = id;
  }
  public Date date() {
    return new JdbcSession(this.dbase)
      .sql("SELECT date FROM post WHERE id = ?")
      .set(this.number)
      .select(new SingleOutcome<Utc>(Utc.class));
  }
  public String title() {
    return new JdbcSession(this.dbase)
      .sql("SELECT title FROM post WHERE id = ?")
      .set(this.number)
      .select(new SingleOutcome<String>(String.class));
  }
}

By looking at the public API of PgPost, you will probably implicitly imagine some table named posts, with date and title columns there. And that’s what the original blogpost doesn’t reveal, that’s where the main mislead comes from: actually, the table and it’s contents are secondary thing here. The primary thing is the Post interface:

interface Post {
  Date date();
  String title();
}

Post is abstraction here. As each abstraction, Post is (supposed to be) declared with client and business purpose in mind. Original text doesn’t reveal the purpose of the Post existence, and it’s a problem. Because the purpose is crucial here!

Lets make up some hypothetical purpose behind the Post existence. RSSFeed, mentioned in the previos section of this post, could be a nice candidate for such purpose.

class RSSFeed {
    private final List<Post> posts;

    public RSSFeed(List<Post> posts) {
        this.post = post;
    }

    public final void generate() {
        // Document below is generated by means of dom4j.
        Document document = new DOMDocument();
        DOMElement root = new DOMElement("rdf");
        DOMElement channel = new DOMElement("channel");
        for(Post post : posts) {
            DOMElement item = new DOMElement("item");
            DOMElement title = new DOMElement("title");
            title.setText(post.title());
            DOMElement date = new DOMElement("date");
            date.setText(post.date().toString());
            item.add(title);
            item.add(date);
            channel.add(item);
        }
        root.add(channel);
        document.add(root);
        try(Writer writer = new FileWriter(path.toFile())) {
            document.write(writer);
        } catch(IOException ex) {
            throw new RuntimeException(ex);
        }
    }
}

The code above is almost the same as the option with Active Record, but now, RSSFeed is bound to only the stuff it requires. The post, with the title and date.

Notice: RSSFeed doesn’t know anything about the database now. It doesn’t matter for RSSFeed from where those posts were coming from: the Post instances might be fixed, from file, from database or from some side microservice, unlike PostAR, which is bound to row from table by definition. PgPost in such context is just one of the infinite possible implementations for Post. As much there are implementations for the Post interface, as many reuse options there are for RSSFeed class. For RSSFeed coupled to PostAR, this trick just won’t work.

That makes difference.

Why do we even need that name — “SQL speaking objects”?

To think more of it, there is actually nothing special about SQL speaking objects. They are just objects, few from many. They have the same nature as any other objects: they implement some abstraction, which is supposed to be stable, and they have some dependencies, which are supposed to be loose. Period. Does the fact that implementation of these objects executes SQL queries makes them somewhat different? I don’t think so.

“SQL speaking objects” name is just a name of a pattern, but there is one common problem with any sort of patterns: the more you think of them instead of the business domain, the more deep and severe mistakes you are risking to make in your design. So, it’s meaningless to draw the analogies between SQL speaking objects, Active Records, JPA entities or other ways of working with data. It is much less interesting then the quality of abstractions SQL speaking objects serve.