shared base class for models

36 views
Skip to first unread message

steve.yen

unread,
Jan 2, 2008, 6:28:14 PM1/2/08
to liftweb
Hi lifters,

I'm wondering how to get a shared base class (or trait) for my lift
models.

The situation is my db tables have a bunch of common columns -- eg,
creatorId, updaterId, createdAt, updatedAt, version, title, etc, and
I'd like to get rid of the copy&paste code.

In my various attempts at this (stabbing in the dark, really), I get
compiler errors like...

[WARNING] C:\temp\funwithlift\src\main\scala\ff\model
\tryModelBaseClasses.scala:9: error: inferred type arguments
[ff.model.Posting] do not conform to class MappedLongIndex's type
parameter bounds [T <: net.liftweb.mapper.Mapper[T]]
[WARNING] object id extends MappedLongIndex(this)

Any ideas on avoiding copy&pasting the common field definitions all
over the place?

Cheers,
Steve

-----------------

trait Posting { // Defines some common fields for posted user content
def primaryKeyField = id

object id extends MappedLongIndex(this)

object creator extends MappedLongForeignKey(this, User)
object createdAt extends MappedLong(this) {
override def defaultValue = System.currentTimeMillis
}
}

// A Review is a Posting

object Review extends Review with KeyedMetaMapper[long, Review] {
override def dbTableName = "review"
}

class Review extends KeyedMapper[long, Review] with Posting {
def getSingleton = Review

// Review specific fields go here

object review extends MappedTextarea(this, 8192) {
override def textareaRows = 10
override def textareaCols = 50
}
}

// A Comment is also a Posting

object Comment extends Comment with KeyedMetaMapper[long, Comment] {
override def dbTableName = "comment"
}

class Comment extends KeyedMapper[long, Comment] with Posting {
def getSingleton = Comment

// Comment specific fields go here

object review extends MappedLongForeignKey(this, Review)

object comment extends MappedTextarea(this, 2048) {
override def textareaRows = 10
override def textareaCols = 50
}
}


David Pollak

unread,
Jan 2, 2008, 6:34:42 PM1/2/08
to lif...@googlegroups.com
You're looking for some Scala magic:

trait Posting[MyType <: Mapper[MyType]] { // Defines some common fields for posted user content
self: MyType =>
def primaryKeyField = id

object id extends MappedLongIndex(this)

object creator extends MappedLongForeignKey(this, User)
object createdAt extends MappedLong(this) {
override def defaultValue = System.currentTimeMillis
}
}

class FooPosting extends KeyedMapper[FooPosting] with Posting[MyType] {
....

steve.yen

unread,
Jan 2, 2008, 7:12:51 PM1/2/08
to liftweb
Sheesh, answering my own question...

I guess if I whack at the keyboard long enough, I'll get either some
shakespeare or compiling code. Right now code's good for me. Below
is a compiling version of the what I kind of wanted in having a shared
Model base trait.

In copy-&-paste terms, the primaryKeyField and id still needs to be
defined in every model subclass. I can live with that, though.

And, a new issue: now I'm wondering how can I get "typed" foreign
keys, where the foreign key is a composite of two columns, like
parent_type and parent_id, like in Rails. For example, I would like
to have Comment records point to either Reviews or to ForumTopics.

Cheers,
Steve

-------------------------------

trait Posting[KeyType, OwnerType<:Posting[KeyType, OwnerType]]
extends
KeyedMapper[KeyType, OwnerType] { self: OwnerType =>
object key extends MappedUniqueId(this, 20)

object creator extends MappedLongForeignKey(this, User)

object createdAt extends MappedLong(this) {
override def defaultValue = System.currentTimeMillis
}

object body extends MappedTextarea(this, 8192) {
override def textareaRows = 10
override def textareaCols = 50
}
}

// -------------------------------------------------

object Review extends Review with KeyedMetaMapper[long, Review] {
override def dbTableName = "review"
}

class Review extends Posting[long, Review] {
def getSingleton = Review

def primaryKeyField = id
object id extends MappedLongIndex(this)

object title extends MappedString(this, 80) {
override def dbIndexed_? = true // indexed in the DB
}
}

// -------------------------------------------------

object ForumTopic extends ForumTopic with KeyedMetaMapper[long,
ForumTopic] {
override def dbTableName = "forum_topic"
}

class ForumTopic extends Posting[long, ForumTopic] {
def getSingleton = ForumTopic

def primaryKeyField = id
object id extends MappedLongIndex(this)

object title extends MappedString(this, 80)
}

// -------------------------------------------------

object Comment extends Comment with KeyedMetaMapper[long, Comment] {
override def dbTableName = "comment"
}

class Comment extends Posting[long, Comment] {
def getSingleton = Comment

def primaryKeyField = id
object id extends MappedLongIndex(this)

// ISSUE: Can only have Comments on Reviews right now, not on
ForumTopics.
// Need to have a MappedLongTypedForeignKey or something??

object target extends MappedLongForeignKey(this, Review)

steve.yen

unread,
Jan 2, 2008, 7:22:20 PM1/2/08
to liftweb
egad, I should have just walked out for a coffee and waited for David
to answer instead!
Thanks David!

David Pollak

unread,
Jan 2, 2008, 8:20:45 PM1/2/08
to lif...@googlegroups.com
Steve,

On Jan 2, 2008, at 4:12 PM, steve.yen wrote:

>
> Sheesh, answering my own question...
>
> I guess if I whack at the keyboard long enough, I'll get either some
> shakespeare or compiling code. Right now code's good for me. Below
> is a compiling version of the what I kind of wanted in having a shared
> Model base trait.
>
> In copy-&-paste terms, the primaryKeyField and id still needs to be
> defined in every model subclass. I can live with that, though.
>
> And, a new issue: now I'm wondering how can I get "typed" foreign
> keys, where the foreign key is a composite of two columns, like
> parent_type and parent_id, like in Rails. For example, I would like
> to have Comment records point to either Reviews or to ForumTopics.

Can't do that directly... mainly because of type safety issues.

Each foreign key has to point to a specific type. You could build a
utility method on top to return one of the other depending on the value.

For example:

object reviewParent extends MappedLongForeignKey(this, Review)
object forumParent extends MappedLongForeignKey(this, ForumTopic)
object parentType extends MappedEnum(this, Parent)

def realParent: /* insert a shared base type here */ = parentType.is
match {
case ReviewParent => revientParent.obj
case ForumParent => forumParent.obj
case _ => Empty
}

Not overly clean, but if there's call for making that into some form
of mixin, it's possible.

Thanks,

David

--
David Pollak
http://blog.lostlake.org

steve.yen

unread,
Jan 3, 2008, 3:00:57 AM1/3/08
to liftweb
On Jan 2, 5:20 pm, David Pollak <d...@athena.com> wrote:
> Steve,
>
> On Jan 2, 2008, at 4:12 PM, steve.yen wrote:
>
> > And, a new issue: now I'm wondering how can I get "typed" foreign
> > keys, where the foreign key is a composite of two columns, like
> > parent_type and parent_id, like in Rails. For example, I would like
> > to have Comment records point to either Reviews or to ForumTopics.
>
> Can't do that directly... mainly because of type safety issues.
>
> Each foreign key has to point to a specific type. You could build a
> utility method on top to return one of the other depending on the value.
>
> For example:
>
> object reviewParent extends MappedLongForeignKey(this, Review)
> object forumParent extends MappedLongForeignKey(this, ForumTopic)
> object parentType extends MappedEnum(this, Parent)
>
> def realParent: /* insert a shared base type here */ = parentType.is
> match {
> case ReviewParent => revientParent.obj
> case ForumParent => forumParent.obj
> case _ => Empty
> }
>
> Not overly clean, but if there's call for making that into some form
> of mixin, it's possible.
>
> Thanks,
>
> David

If I understand right, it seems I'll need a distinct foreign key
column per type?

If that's yes, I agree it's not overly clean. At the DB level, too, I
think it'd be much nicer to have a simple Rails-esque table with
columns such as: target_type, target_id.

Another motivating example for this is trying to implement tagging /
folksonomy / tag clouds (I know, viva la 2005). Let's say you want to
have users be able to tag other users, blog posts, reviews, photos,
whatever, then with the above limitation you could either...

- have a distinct table per tag target:
create table user_tags ...
create table blog_tags ...
create table review_tags ...
create table photo_tags ...
etc

- or, have a single tags table, with lots of columns, such as:
target_type, user_id, blog_id, review_id, photo_id, etc
but, for any particular record, most of the XXX_id column values
except one would be NULL.

Lots of issues going with those 2 approaches.

If making "some form of mixin" has some potential clean solution to
get around type safety issues, love to hear your thoughts!

Thanks,
Steve

David Pollak

unread,
Jan 3, 2008, 11:38:21 AM1/3/08
to lif...@googlegroups.com
Steve,

If you want enforced foreign key constraints, then you should use MappedLongForeignKey.

If you don't care about that, use MappedLong and MappedEnum and write a method that "returns the right thing."  That'll give you what you want.  The biggest challenge is dealing with the "type" of the thing returned.  I think we can do cleaver things with the typing, but I've gotta noodle on it.

The nice thing is that once you get the pattern down right, you can use it over and over and maybe even contribute the code to the lift code base. :-)

Thanks,

David

http://liftweb.net
Collaborative Task Management http://much4.us

steve.yen

unread,
Jan 3, 2008, 2:35:21 PM1/3/08
to liftweb
Interesting. Keep on noodling!

Here's my own scratch noodling on the tagging/folksonomy example,
trying to avoid dynamic casting. Some fake code...

object TagTypes extends Enumeration { // All the different kinds of
models that I can tag...
val User = Value(1, "User")
val Blog = Value(2, "Blog")
val Review = Value(3, "Review")
val Photo = Value(4, "Photo")
}

// The "tag" table has name, target_type and target_id columns.
class Tag extends KeyedMapper[Long, Tag] {
object name extends MappedString(this, 80)

// For the target_type column, using the MappedEnum suggestion
object targetType extends MappedEnum(this, TagTypes)

// The following fields all map to the same target_id column.
object targetUser extends MappedTypedLongForeignKey(this, User,
"target",
TagTypes.User)

object targetBlog extends MappedTypedLongForeignKey(this, Blog,
"target",
TagTypes.Blog)

object targetReview extends MappedTypedLongForeignKey(this,
Review, "target",
TagTypes.Review)

object targetPhoto extends MappedTypedLongForeignKey(this, Photo,
"target",
TagTypes.Photo)
}

Perhaps there's a better way, but the idea is to have targetUser/
targetBlog/etc fields return Empty if the target_type value doesn't
match. So, the above fake code would kind of mean the same as this
fake code...

class Tag extends KeyedMapper[Long, Tag] {
object name extends MappedString(this, 80)

object targetType extends MappedEnum(this, TagTypes)

// All the following fields are using the same dbColumnName of
"target_id"

object targetUser extends MappedLongForeignKey(this, User) {
override def dbColumnName = "target_id"
override def real_i_set_!(value : Long) = {
fieldOwner.targetType = TagTypes.User;
super.real_i_set_!(value)
}
override def obj: Can[O] = if (fieldOwner.targetType ==
TagTypes.User) super.obj else Empty
}

object targetBlog extends MappedLongForeignKey(this, Blog) {
override def dbColumnName = "target_id"
override def real_i_set_!(value : Long) = {
fieldOwner.targetType = TagTypes.Blog;
super.real_i_set_!(value)
}
override def obj: Can[O] = if (fieldOwner.targetType ==
TagTypes.Blog) super.obj else Empty
}

object targetReview extends MappedLongForeignKey(this, Review) {
override def dbColumnName = "target_id"
override def real_i_set_!(value : Long) = {
fieldOwner.targetType = TagTypes.Review;
super.real_i_set_!(value)
}
override def obj: Can[O] = if (fieldOwner.targetType ==
TagTypes.Photo) super.obj else Empty
}

object targetPhoto extends MappedLongForeignKey(this, Photo) {
override def dbColumnName = "target_id"
override def real_i_set_!(value : Long) = {
fieldOwner.targetType = TagTypes.Photo;
super.real_i_set_!(value)
}
override def obj: Can[O] = if (fieldOwner.targetType ==
TagTypes.Photo) super.obj else Empty
}
}

Anyways, trying to make it work I ran into Schemafier, buildMapper,
and many other challenges. MappedLongForeignKey defines obj as a lazy
val instead of a method so obj is not overridable, too. (btw, using a
lazy val for MappedLongForeignKey.obj seems wrong if the caller keeps
on changing the foreign key value). Having a lot of fun reading your
code.

Cheers,
Steve

On Jan 3, 8:38 am, "David Pollak" <feeder.of.the.be...@gmail.com>
wrote:
Reply all
Reply to author
Forward
0 new messages