Chris Renner hates database abstraction layers
Sunday, May 9th, 2010Chris Renner hates database abstraction layers, but I think he doesn’t know how to use them. He writes:
Propel actually does manage to provide some support for joins. I do not like Propel’s reliance on the “*” wildcard in the linked example, but perhaps that is just for simplicity in the documentation. Good SQL explicity names the columns to retrieve, and except for in-development testing, you should rarely if ever use “*” …its lazy coding and wastes resources (see Rudy Limeback’s Simply SQL for a more detailed argument against “*”). I’d like to see Propel, or any other ORM/DAL class deal, however, with a query such as this:
SELECT SQL_CALC_FOUND_ROWS
projects.id
, faculty
, author
, author2
, author3
, date_created
, users.last_name as faculty_last
, users.first_name as faculty_first
, users2.last_name as author_last
, users2.first_name as author_first
, dept.dept_name
, project_title
, project_type
, staff_no
, project_status
, organization.org_name
, project_date
FROM projects
LEFT OUTER JOIN users as faculty ON projects.faculty = users.id
LEFT OUTER JOIN users as users2 ON projects.author = users2.id
LEFT OUTER JOIN dept ON projects.dept = dept.id
LEFT OUTER JOIN organization ON project.org = organization.id
LEFT OUTER JOIN staff ON project.staff_no = staff.id
WHERE (users.last_name LIKE ‘%smith%’ OR users.first_name LIKE ‘%smith%’)
AND project.title LIKE ‘%Material Composition of Unobtanium%’
ORDER BY Items.Entry_Date DESC
LIMIT 0, 15The above example is a sanitized (names anonymized to protect the innocent) version of a real query I use to return paginated results in one of our web apps. Some of the joins are included dynamically based on search criteria, others are always included. The WHERE clause is likewise generated dynamically, as well as the ORDER BY, which is used for column-based sorting.
Show me a DAL/ORM class that can deal with this example and I’ll be your best friend forever.
I do almost all my development work nowadays with Symfony, using Propel as the ORM, so I work with Propel a great deal. One trick for handling multiple joins is to use a database view. This is a trick that halfer mentioned to me on the Symfony forums.
Recently I’ve been working for a client who is taking an old system and porting it to Symfony. The old system had a lot of complicated JOINs. So, for instance, I take this:
SELECT
c.id, c.textid, c.title, c.title_link, c.content_date, c.issue_date,
c.protected AS protect, c.allow_comments, c.show_comments, c.content_hint,
c.author1, c.author2, c.author3, c.author4, c.author_str,
c.subtitle, c.abstract, c.direct_url,
c.imgpath1, c.imgpath2, c.imgcaption1, c.imgcaption2,
c.content_type, c.live_date,
c.reference_link_1, c.alt_info_1,
b.blog_name, b.blog_id,
cao_author_xref.author_id, cao_author_xref.state,
cao_author_xref.content_type AS content_t,
it.issue_text_date,
d.literal, d.alias, d.cdaobject, d.cdaaction, d.channel, d.adsite,
d.adzone, d.indexable
FROM cao_author_xref
LEFT OUTER JOIN cao_content c on (cao_author_xref.content_id = c.id
and cao_author_xref.state=c.state)
LEFT OUTER JOIN cao_issue it on c.issue_date = it.issue_date
LEFT OUTER JOIN cao_blogs b on c.reference_link_1 = b.blog_id
LEFT OUTER JOIN cao_content_type d on c.content_type=d.id
and I turn it into a view:
CREATE VIEW cao_paginated_content_by_author AS
SELECT
c.id, c.textid, c.title, c.title_link, c.content_date, c.issue_date,
c.protected AS protect, c.allow_comments, c.show_comments, c.content_hint,
c.author1, c.author2, c.author3, c.author4, c.author_str,
c.subtitle, c.abstract, c.direct_url,
c.imgpath1, c.imgpath2, c.imgcaption1, c.imgcaption2,
c.content_type, c.live_date,
c.reference_link_1, c.alt_info_1,
b.blog_name, b.blog_id,
cao_author_xref.author_id, cao_author_xref.state,
cao_author_xref.content_type AS content_t,
it.issue_text_date,
d.literal, d.alias, d.cdaobject, d.cdaaction, d.channel, d.adsite,
d.adzone, d.indexable
FROM cao_author_xref
LEFT OUTER JOIN cao_content c on (cao_author_xref.content_id = c.id
and cao_author_xref.state=c.state)
LEFT OUTER JOIN cao_issue it on c.issue_date = it.issue_date
LEFT OUTER JOIN cao_blogs b on c.reference_link_1 = b.blog_id
LEFT OUTER JOIN cao_content_type d on c.content_type=d.id
Now I add it to the config/schema.yml file as if it was a real database table:
cao_paginated_content_by_author:
id: ~
textid: { type: varchar(200) }
title: { type: varchar(200) }
title_link: { type: varchar(150) }
content_date: { type: timestamp }
issue_date: { type: date }
protect: { type: varchar(1) }
allow_comments: { type: varchar(1) }
show_comments: { type: varchar(1) }
content_hint: { type: varchar(1) }
author1: { type: integer }
author2: { type: integer }
author3: { type: integer }
author4: { type: integer }
author_str: { type: longvarchar }
subtitle: { type: longvarchar }
abstract:
peerName: cao_abstract
phpName: cao_abstract
type: LONGVARCHAR
required: true
direct_url: { type: varchar(250) }
imgpath1: { type: varchar(150) }
imgpath2: { type: varchar(150) }
imgcaption1: { type: longvarchar }
imgcaption2: { type: longvarchar }
content_type: { type: integer }
reference_link_1: { type: integer }
alt_info_1: { type: varchar(200) }
blog_name: { type: varchar(200) }
blog_id: { type: integer }
issue_text_date: { type: varchar(100) }
author_id: { type: integer }
state: { type: varchar(1) }
content_t: { type: integer }
live_date: { type: timestamp }
literal: { type: varchar(50) }
alias: { type: varchar(50) }
cdaobject: { type: varchar(50) }
cdaaction: { type: varchar(50) }
channel: { type: varchar(50) }
adsite: { type: varchar(50) }
adzone: { type: varchar(50) }
indexable: { type: varchar(1) }
Now I run the Symfony command that autogenerates my Propel classes based on the above schema.yml.
Now to run the above JOIN statement, using Propel, I only need a few lines of code. If I want all the rows from the JOIN where STATE equals “P” I write:
$c = new Criteria();
$c->add(CaoPaginatedContentByAuthorPeer::STATE, “P”);
$arrayOfItems = CaoPaginatedContentByAuthorPeer:doSelect($c);
That is 3 lines of code. And it keeps the SQL out of my code, which I think is a big plus.
