Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Fixed ManyToMany relations and outer joins in Squeryl-Record

Closes #869
Closes #870

ManyToMany relations can now be expressed with relation tables of POSO objects.
For relation tables, no record should be needed. It is now also possible to mix
records and POSOs for tables belonging to one project.

For outer joins, implicit conversions of Option[TypedField[T]] to
NumericalExpression[Option[T]]/NonNumericalExpression[T] were added.

Unit tests now include test cases for ManyToMany relations and outer joins.
  • Loading branch information...
commit 09038652c7ec322c85796daa4fd0ba7145796451 1 parent 04170b1
Michael Gottschalk authored
View
21 ...ift-persistence/lift-squeryl-record/src/main/scala/net/liftweb/squerylrecord/RecordMetaDataFactory.scala
@@ -60,6 +60,13 @@ class RecordMetaDataFactory extends FieldMetaDataFactory {
def build(parentMetaData: PosoMetaData[_], name: String,
property: (Option[Field], Option[Method], Option[Method], Set[Annotation]),
sampleInstance4OptionTypeDeduction: AnyRef, isOptimisticCounter: Boolean): FieldMetaData = {
+ if (!isRecord(parentMetaData.clasz)) {
+ // No Record class, treat it as a normal class in primitive type mode.
+ // This is needed for ManyToMany association classes, for example
+ return SquerylRecord.posoMetaDataFactory.build(parentMetaData, name, property,
+ sampleInstance4OptionTypeDeduction, isOptimisticCounter)
+ }
+
val metaField = findMetaField(parentMetaData.clasz, name)
val (field, getter, setter, annotations) = property
@@ -119,13 +126,25 @@ class RecordMetaDataFactory extends FieldMetaDataFactory {
}
}
+ /**
+ * Checks if the given class is a subclass of Record. A special handling is only
+ * needed for such subtypes. For other classes, use the standard squeryl methods.
+ */
+ private def isRecord(clasz: Class[_]) = {
+ classOf[Record[_]].isAssignableFrom(clasz)
+ }
+
/**
* For records, the constructor must not be used directly when
* constructing Objects. Instead, the createRecord method must be called.
*/
def createPosoFactory(posoMetaData: PosoMetaData[_]): () => AnyRef = {
-
+ if (!isRecord(posoMetaData.clasz)) {
+ // No record class - use standard poso meta data factory
+ return SquerylRecord.posoMetaDataFactory.createPosoFactory(posoMetaData);
+ }
+
// Extract the MetaRecord for the companion object. This
// is done only once for each class.
val metaRecord = Class.forName(posoMetaData.clasz.getName +
View
54 ...ework/lift-persistence/lift-squeryl-record/src/main/scala/net/liftweb/squerylrecord/RecordTypeMode.scala
@@ -44,20 +44,33 @@ trait RecordTypeMode extends PrimitiveTypeMode {
implicit def double2ScalarDouble(f: MandatoryTypedField[Double]) = convertNumericalMandatory(f, createOutMapperDoubleType)
/** Conversion of mandatory BigDecimal fields to Squeryl Expressions. */
- implicit def decimal2ScalarBoolean(f: MandatoryTypedField[BigDecimal]) = convertNumericalMandatory(f, createOutMapperBigDecimalType)
+ implicit def decimal2ScalarDecimal(f: MandatoryTypedField[BigDecimal]) = convertNumericalMandatory(f, createOutMapperBigDecimalType)
/** Conversion of optional Int fields to Squeryl Expressions. */
implicit def optionInt2ScalarInt(f: OptionalTypedField[Int]) = convertNumericalOptional(f, createOutMapperIntTypeOption)
+
+ /** Conversion needed for outer joins */
+ implicit def optionIntField2OptionInt(f: Option[TypedField[Int]]) = convertNumericalOption(f, createOutMapperIntTypeOption)
/** Conversion of optional Long fields to Squeryl Expressions. */
implicit def optionLong2ScalarLong(f: OptionalTypedField[Long]) = convertNumericalOptional(f, createOutMapperLongTypeOption)
+
+ /** Conversion needed for outer joins */
+ implicit def optionLongField2OptionLong(f: Option[TypedField[Long]]) = convertNumericalOption(f, createOutMapperLongTypeOption)
/** Conversion of optional Double fields to Squeryl Expressions. */
implicit def optionDouble2ScalarDouble(f: OptionalTypedField[Double]) = convertNumericalOptional(f, createOutMapperDoubleTypeOption)
+ /** Conversion needed for outer joins */
+ implicit def optionDoubleField2OptionDouble(f: Option[TypedField[Double]]) = convertNumericalOption(f, createOutMapperDoubleTypeOption)
+
/** Conversion of optional BigDecimal fields to Squeryl Expressions. */
implicit def optionDecimal2ScalarBoolean(f: OptionalTypedField[BigDecimal]) = convertNumericalOptional(f, createOutMapperBigDecimalTypeOption)
+ /** Conversion needed for outer joins */
+ implicit def optionDecimalField2OptionDecimal(f: Option[TypedField[BigDecimal]]) = convertNumericalOption(f, createOutMapperBigDecimalTypeOption)
+
+
/** Conversion of mandatory String fields to Squeryl Expressions. */
implicit def string2ScalarString(f: MandatoryTypedField[String]) = fieldReference match {
case Some(e) => new SelectElementReference[String](e)(createOutMapperStringType) with StringExpression[String] with SquerylRecordNonNumericalExpression[String]
@@ -69,6 +82,12 @@ trait RecordTypeMode extends PrimitiveTypeMode {
case Some(e) => new SelectElementReference[Option[String]](e)(createOutMapperStringTypeOption) with StringExpression[Option[String]] with SquerylRecordNonNumericalExpression[Option[String]]
case None => new ConstantExpressionNode[Option[String]](f.is) with StringExpression[Option[String]] with SquerylRecordNonNumericalExpression[Option[String]]
}
+
+ /** Needed for outer joins */
+ implicit def optionStringField2OptionString(f: Option[TypedField[String]]) = fieldReference match {
+ case Some(e) => new SelectElementReference[String](e)(createOutMapperStringType) with StringExpression[String] with SquerylRecordNonNumericalExpression[String]
+ case None => new ConstantExpressionNode[String](getValueOrNull(f)) with StringExpression[String] with SquerylRecordNonNumericalExpression[String]
+ }
/** Conversion of mandatory Boolean fields to Squeryl Expressions. */
implicit def bool2ScalarBoolean(f: MandatoryTypedField[Boolean]) = fieldReference match {
@@ -82,6 +101,12 @@ trait RecordTypeMode extends PrimitiveTypeMode {
case None => new ConstantExpressionNode[Option[Boolean]](f.is) with BooleanExpression[Option[Boolean]] with SquerylRecordNonNumericalExpression[Option[Boolean]]
}
+ /** Needed for outer joins. */
+ implicit def optionBooleanField2Boolean(f: Option[TypedField[Boolean]]) = fieldReference match {
+ case Some(e) => new SelectElementReference[Boolean](e)(createOutMapperBooleanType) with BooleanExpression[Boolean] with SquerylRecordNonNumericalExpression[Boolean]
+ case None => new ConstantExpressionNode[Boolean](getValue(f).getOrElse(false)) with BooleanExpression[Boolean] with SquerylRecordNonNumericalExpression[Boolean]
+ }
+
/** Conversion of mandatory Calendar fields to Squeryl Expressions. */
implicit def date2ScalarDate(f: MandatoryTypedField[Calendar]) = fieldReference match {
case Some(e) => new SelectElementReference[Timestamp](e)(createOutMapperTimestampType) with DateExpression[Timestamp] with SquerylRecordNonNumericalExpression[Timestamp]
@@ -99,6 +124,12 @@ trait RecordTypeMode extends PrimitiveTypeMode {
new ConstantExpressionNode[Option[Timestamp]](date) with BooleanExpression[Option[Timestamp]] with SquerylRecordNonNumericalExpression[Option[Timestamp]]
}
}
+
+ /** Needed for outer joins. */
+ implicit def optionDateField2OptionDate(f: Option[TypedField[Calendar]]) = fieldReference match {
+ case Some(e) => new SelectElementReference[Timestamp](e)(createOutMapperTimestampType) with DateExpression[Timestamp] with SquerylRecordNonNumericalExpression[Timestamp]
+ case None => new ConstantExpressionNode[Timestamp](getValue(f).map(field => new Timestamp(field.getTimeInMillis)).orNull) with BooleanExpression[Timestamp] with SquerylRecordNonNumericalExpression[Timestamp]
+ }
/** Conversion of mandatory Enum fields to Squeryl Expressions. */
implicit def enum2EnumExpr[EnumType <: Enumeration](f: MandatoryTypedField[EnumType#Value]) = fieldReference match {
@@ -111,6 +142,12 @@ trait RecordTypeMode extends PrimitiveTypeMode {
case Some(e) => new SelectElementReference[Option[Enumeration#Value]](e)(e.createEnumerationOptionMapper) with EnumExpression[Option[Enumeration#Value]] with SquerylRecordNonNumericalExpression[Option[Enumeration#Value]]
case None => new ConstantExpressionNode[Option[Enumeration#Value]](f.is) with EnumExpression[Option[Enumeration#Value]] with SquerylRecordNonNumericalExpression[Option[Enumeration#Value]]
}
+
+ /** Needed for outer joins. */
+ implicit def optionEnumField2OptionEnum[EnumType <: Enumeration](f: Option[TypedField[EnumType#Value]]) = fieldReference match {
+ case Some(e) => new SelectElementReference[Enumeration#Value](e)(e.createEnumerationMapper) with EnumExpression[Enumeration#Value] with SquerylRecordNonNumericalExpression[Enumeration#Value]
+ case None => new ConstantExpressionNode[Enumeration#Value](getValue(f).orNull) with EnumExpression[Enumeration#Value] with SquerylRecordNonNumericalExpression[Enumeration#Value]
+ }
/**
* Helper method for converting mandatory numerical fields to Squeryl Expressions.
@@ -127,6 +164,21 @@ trait RecordTypeMode extends PrimitiveTypeMode {
case Some(e: SelectElement) => new SelectElementReference[Option[T]](e)(outMapper) with NumericalExpression[Option[T]] with SquerylRecordNumericalExpression[Option[T]]
case None => new ConstantExpressionNode[Option[T]](f.is) with NumericalExpression[Option[T]] with SquerylRecordNumericalExpression[Option[T]]
}
+
+ private def convertNumericalOption[T](f: Option[TypedField[T]], outMapper: OutMapper[Option[T]]) = fieldReference match {
+ case Some(e) => new SelectElementReference[Option[T]](e)(outMapper) with NumericalExpression[Option[T]] with SquerylRecordNumericalExpression[Option[T]]
+ case None => new ConstantExpressionNode[Option[T]](getValue(f)) with NumericalExpression[Option[T]] with SquerylRecordNumericalExpression[Option[T]]
+ }
+
+ private def getValue[T](f: Option[TypedField[T]]): Option[T] = f match {
+ case Some(field) => field.valueBox
+ case None => None
+ }
+
+ private def getValueOrNull[T <: AnyRef](f: Option[TypedField[T]]): T = f match {
+ case Some(field) => field.valueBox.openOr(null.asInstanceOf[T])
+ case None => null.asInstanceOf[T]
+ }
/**
* Returns the field that was last referenced by Squeryl. Can also be None.
View
7 framework/lift-persistence/lift-squeryl-record/src/main/scala/net/liftweb/squerylrecord/SquerylRecord.scala
@@ -27,6 +27,13 @@ object SquerylRecord extends Loggable {
private object currentSession extends DynoVar[Session]
/**
+ * We have to remember the default Squeryl metadata factory before
+ * we override it with our own implementation, so that we can use
+ * the original factory for non-record classes.
+ */
+ private[squerylrecord] var posoMetaDataFactory = FieldMetaData.factory
+
+ /**
* Initialize the Squeryl/Record integration. This must be called somewhere during your Boot, and before you use any
* Records with Squeryl. Use this function instead of init if you want to use the squeryl session factory
* instead of mapper.DB as the transaction manager with squeryl-record.
View
59 framework/lift-persistence/lift-squeryl-record/src/test/scala/net/liftweb/squerylrecord/Fixtures.scala
@@ -28,6 +28,8 @@ import org.squeryl.adapters.H2Adapter
import org.squeryl.annotations.Column
import org.squeryl.internals.AutoIncremented
import org.squeryl.internals.PrimaryKey
+import org.squeryl.dsl.CompositeKey2
+import org.squeryl.KeyedEntity
import java.math.MathContext
import java.sql.DriverManager
@@ -48,9 +50,14 @@ object DBHelper {
*/
def createSchema() {
inTransaction {
- //MySchema.printDdl
- MySchema.dropAndCreate
- MySchema.createTestData
+ try {
+ //MySchema.printDdl
+ MySchema.dropAndCreate
+ MySchema.createTestData
+ } catch {
+ case e => e.printStackTrace()
+ throw e;
+ }
}
}
}
@@ -129,20 +136,52 @@ class Employee private () extends Record[Employee] with KeyedRecord[Long] {
val role = new EnumNameField(this, EmployeeRole)
lazy val company = MySchema.companyToEmployees.right(this)
+ lazy val rooms = MySchema.roomAssignments.left(this)
}
object Employee extends Employee with MetaRecord[Employee]
/**
+ * Test record: One or more employees can have a room (one-to-many-relation).
+ */
+class Room private() extends Record[Room] with KeyedRecord[Long] {
+ override def meta = Room
+
+ override val idField = new LongField(this)
+
+ val name = new StringField(this, 50)
+
+ lazy val employees = MySchema.roomAssignments.right(this)
+}
+
+object Room extends Room with MetaRecord[Room]
+
+/**
+ * Relation table for assignments of rooms to employees.
+ * This must not be a Record. However, it's ok if it is not
+ * a record, because we won't use a relation table for
+ * a web form or similar.
+ */
+class RoomAssignment(val employeeId: Long, val roomId: Long) extends KeyedEntity[CompositeKey2[Long,Long]] {
+ def id = compositeKey(employeeId, roomId)
+}
+
+
+/**
* Schema for the test database.
*/
object MySchema extends Schema {
val companies = table[Company]
val employees = table[Employee]
+ val rooms = table[Room]
val companyToEmployees =
oneToManyRelation(companies, employees).via((c, e) => c.id === e.companyId)
+ val roomAssignments = manyToManyRelation(employees, rooms).
+ via[RoomAssignment]((employee, room, roomAssignment) =>
+ (roomAssignment.employeeId === employee.idField, roomAssignment.roomId === room.idField))
+
on(employees)(e =>
declare(e.companyId defineAs (indexed("idx_employee_companyId")),
e.email defineAs indexed("idx_employee_email")))
@@ -165,6 +204,10 @@ object MySchema extends Schema {
allCompanies.foreach(companies.insert(_))
allEmployees.foreach(employees.insert(_))
+ allRooms.foreach(rooms.insert(_))
+
+ e1.rooms.associate(r1)
+ e1.rooms.associate(r2)
}
object TestData {
@@ -174,7 +217,7 @@ object MySchema extends Schema {
val c2 = Company.createRecord.name("Second Company USA").
created(Calendar.getInstance()).
country(Countries.USA).postCode("54321")
- val c3 = Company.createRecord.name("First Company Canada").
+ val c3 = Company.createRecord.name("Company or Employee").
created(Calendar.getInstance()).
country(Countries.Canada).postCode("1234")
@@ -189,7 +232,7 @@ object MySchema extends Schema {
photo(Array[Byte](0, 1, 2, 3, 4))
lazy val e2 = Employee.createRecord.companyId(c2.idField.is).
- name("Test Name").
+ name("Company or Employee").
email("test@example.com").salary(BigDecimal("123.123")).
locale(java.util.Locale.US.toString()).
timeZone("America/Los_Angeles").password("test").
@@ -197,6 +240,12 @@ object MySchema extends Schema {
photo(Array[Byte](1))
lazy val allEmployees = List(e1, e2)
+
+ val r1 = Room.createRecord.name("Room 1")
+ val r2 = Room.createRecord.name("Room 2")
+ val r3 = Room.createRecord.name("Room 3")
+
+ val allRooms = List(r1, r2, r3)
}
}
View
74 ...k/lift-persistence/lift-squeryl-record/src/test/scala/net/liftweb/squerylrecord/SquerylRecordSpecs.scala
@@ -16,7 +16,7 @@ package squerylrecord
import RecordTypeMode._
import MySchema.{ TestData => td }
-import MySchema.{ companies, employees }
+import MySchema.{ companies, employees, rooms, roomAssignments }
import record.{ BaseField, Record }
@@ -68,7 +68,7 @@ object SquerylRecordSpecs extends Specification {
}
}
- "support joins" >> {
+ "support normal joins" >> {
transaction {
val companiesWithEmployees = from(companies, employees)((c, e) =>
where(c.id === e.id)
@@ -80,6 +80,60 @@ object SquerylRecordSpecs extends Specification {
(td.c2.id, td.e2.id)))
}
}
+
+ "support left outer joins" >> {
+ transaction {
+ val companiesWithEmployees = join(companies, employees.leftOuter)((c, e) =>
+ select(c, e)
+ on(c.id === e.map(_.companyId))
+ )
+
+ companiesWithEmployees must haveSize(3)
+ // One company doesn't have an employee, two have
+ companiesWithEmployees.filter(ce => ce._2.isEmpty) must haveSize(1)
+
+ val companiesAndEmployeesWithSameName = join(companies, employees.leftOuter)((c, e) =>
+ groupBy(c.id)
+ compute(countDistinct(e.map(_.id)))
+ on(c.name === e.map(_.name))
+ )
+
+ // There are three companies
+ companiesAndEmployeesWithSameName must haveSize(3)
+ // One company has the same name as an employee, two don't
+ companiesAndEmployeesWithSameName.filter(ce => ce.measures == 0) must haveSize(2)
+
+ val employeesWithSameAdminSetting = join(employees, employees.leftOuter)((e1, e2) =>
+ select(e1, e2)
+ on(e1.admin === e2.map(_.admin))
+ )
+
+ // two employees, both have distinct admin settings
+ employeesWithSameAdminSetting must haveSize(2)
+ employeesWithSameAdminSetting.foreach { ee =>
+ ee._2 must not (beEmpty)
+ ee._1.id must_== ee._2.get.id
+ }
+
+ val companiesWithSameCreationDate = join(companies, companies.leftOuter)((c1, c2) =>
+ select(c1, c2)
+ on(c1.created === c2.map(_.created))
+ )
+ companiesWithSameCreationDate must not (beEmpty)
+
+ val employeesWithSameDepartmentNumber = join(employees, employees.leftOuter)((e1, e2) =>
+ select(e1, e2)
+ on(e1.departmentNumber === e2.map(_.departmentNumber))
+ )
+ employeesWithSameDepartmentNumber must not (beEmpty)
+
+ val employeesWithSameRoles = join(employees, employees.leftOuter)((e1, e2) =>
+ select(e1, e2)
+ on(e1.role === e2.map(_.role))
+ )
+ employeesWithSameRoles must not (beEmpty)
+ }
+ }
"support one to many relations" >> {
transaction {
@@ -90,6 +144,20 @@ object SquerylRecordSpecs extends Specification {
checkEmployeesEqual(td.e1, employees.head)
}
}
+
+ "support many to many relations" >> {
+ transactionWithRollback {
+ td.e1.rooms must haveSize(2)
+
+ td.e2.rooms must beEmpty
+
+ td.r1.employees must haveSize(1)
+ td.r3.employees must beEmpty
+
+ td.r3.employees.associate(td.e2)
+ td.e2.rooms must haveSize(1)
+ }
+ }
"support updates" >> {
val id = td.c1.id
@@ -135,6 +203,8 @@ object SquerylRecordSpecs extends Specification {
loadedCompanies.size must beGreaterThanOrEqualTo(1)
}
}
+
+
}
class TransactionRollbackException extends RuntimeException
Please sign in to comment.
Something went wrong with that request. Please try again.