struct point { float x; float y; }; QUINCE_MAP_CLASS(point, (x)(y)) struct movie { serial id; // primary key with automatically assigned values std::string title; }; QUINCE_MAP_CLASS(movie, (id)(title)) struct cinema { serial id; // primary key with automatically assigned values point location; }; QUINCE_MAP_CLASS(cinema, (id)(location)) struct screen { serial id; // primary key with automatically assigned values serial cinema_id; // foreign key to cinemas table serial current_movie_id; // foreign key to movies table }; QUINCE_MAP_CLASS(screen, (id)(cinema_id)(current_movie_id))
Each invocation of the macro QUINCE_MAP_CLASS
generates all the meta-information quince will need, regarding one of our
class types. (We've declared them with the keyword struct
,
but structs are classes as far as quince is concerned.) This meta-information
is packaged into a class, known as the mapper class
for our class. So the first invocation of QUINCE_MAP_CLASS
generates the definition of point
's
mapper class, aka class_mapper<point>
.
It's called a mapper class because it tells quince how to map
a point
to its representation
as columns (to accompany an SQL statement), and how to map columns back
to a point
(when retrieving
results from an SQL statement). In particular, it tells quince that, in
order to map a point
p
in any way, it must map the two floats p
.x
and
p
.y
.
As we shall see, there are variations in the way point
s
are represented in different contexts: the SQL column names can vary, the
encoding of float
s can vary.
So different situations will call for different mapper objects
for point
. But they will
all be instances of the mapper class that we have just defined.
We rarely mention mapper classes by name in application code, but there
is something about them that we need to understand. class_mapper<point>
has two public members also
called x
and y
. If cmp
is any class_mapper<point>
,
then, cmp
.x
and cmp
.y
refer
to float
mappers, which tell
quince how to map a point
's
x
and y
members respectively. So it's nearly true to say the members
of the mapper are the mappers of the members [3] .
Following the same pattern, if cmc
is a class_mapper<cinema>
,
then cmc
.id
and cmc
.location
refer to a serial
mapper
and a point
mapper respectively.
[4] .
Which brings us to serial
.
This is a C++ type, defined by quince, and it corresponds to whatever SQL
type a database uses for primary keys with automatically assigned values.
We use serial
for items
that are DBMS-generated (such as movie::id
),
and for items that will hold copies of those (such as screen::current_movie_id
).
[3]
Nearly true but not 100% true. In fact, the members of the mapper are
const references to the mappers of the members.
This is for the sake of polymorphism. At this stage, nobody knows exactly
what type the mappers of the members should be: there are different mapper
classes for float
on different
backends, and we haven't yet said which backend we're using. Also there
is the possibility of custom mappings. At run time, when quince constructs
cmp
, it will have that information, and it
will build the appropriate mapper objects for cmp
.x
and cmp
.y
to refer to. But at compile time,
all that is known is that they refer to some mapper of float
-- which is an abstract
type (abstract_mapper<float>
, in fact).
[4]
Readers of the previous footnote will be sensitive to a subtle difference
in this case. We do know, at compile-time, the exact
type of cmc
.location
: it's a class_mapper<point>
, and that type is already fully defined.
So quince does not need to use polymorphism in this case, and in fact
it chooses not to. This subtlety affects what you can write in application
code. E.g. if your code gets hold of a reference to cmc
(and we shall soon see how that can happen), then the expression cmc
.location.x
is well-formed, because the compiler can see the type of cmc
.location
.