dart-bridge / trestle

Database Gateway and ORM for Dart
MIT License
26 stars 12 forks source link

ORM #2

Open emilniklas opened 9 years ago

emilniklas commented 9 years ago

The ORM is being rewritten. The following is the specification of the target public API.

// A data structure will always be serialized to its direct representation as a map
class DataStructure { // table name 'data_structures' is infered
  String property; // serialized as 'property'
  String camelCase; // serialized as 'camel_case'
}

// A model can be more explicitly extended and configured
class MyModel extends Model { // infered table name 'my_models' is overriden below
  static const table = 'overriden_table';

  @field String field; // serialized as 'field'
  @field String camelCase; // serialized as 'camel_case'
  @Field('override') String name; // serialized as 'override'

  // These relationships are conventional; the keys used are 'id' and 'my_model_id'
  @hasOne Child child; // a one-to relationship where this is parent
  @belongsTo Parent parent; // a to-one relationship where this is child
  @hasMany List<Child> children; // a many-to relationship where this is parent
  @belongsToMany List<Parent> parents; // a to-many relationship where this is child

  // Overridden conventions with default values
  @HasOne(mine: 'child_id', theirs: 'id', table: 'childs')
  Child child;
  @BelongsTo(mine: 'id', theirs: 'my_model_id', table: 'parents')
  Parent parent;

  // HasMany and BelongsToMany annotations are potentially one side 
  // of a one-to-many relationship. One-to-many is the default, but many to many with
  // be infered from a corresponding annotation on the other model.
  @HasMany(mine: 'id', theirs: 'my_model_id', table: 'childs')
  List<Child> children;
  @HasMany(mine: 'id', theirs: 'my_model_id', table: 'overriden_table_childs')
  List<Child> children;
  @BelongsToMany(mine: 'id', theirs: 'my_model_id', table: 'parents')
  List<Parent> parents;
  @BelongsToMany(mine: 'id', theirs: 'my_model_id', table: 'parents_overriden_table')
  List<Parent> parents;

  // Relationships are loaded in five different fashions:
  @hasOne Child child; // will be eager loaded
  @hasOne Future<Child> child; // will be lazy loaded
  @hasMany List<Child> children; // will be eager loaded
  @hasMany Stream<Child> children; // will be lazy loaded
  @hasMany RepositoryQuery<Child> children; // will only store the query
}

// Each model is saved in itself, however if the relationship is eager loaded, it will dictate the
// hidden foreign key.
final parent = await parents.first(); // get a persisted parent
final child = new Child(); // create a new child
parent.child = child; // (re)assign the child property with the new child
await parents.save(parent); // the parent row will be saved with the child_id of the parent.child.id
await children.save(child); // the child row is inserted

// If the relationship isn't eager loaded, the foreign key must be provided
// as a property on the model to be changed.
final parent = await parents.first(); // get a persisted parent
var child = await parent.child; // the parent's child is fetched from the database
child = await children.first(); // another child is retrieved
parent.child_id = child.id; // (re)assign the child
await parents.save(parent); // save the change
child = await parent.child; // returns the new child
luisvt commented 9 years ago

Maybe I'm too java fan, but I would prefer JPA annotations

emilniklas commented 9 years ago

@luisvt,

I assume that you're talking about the difference between explicit migrations and specifying schema in models? I have considered this and here's my take on it:

Argument 1

I think the reason for mapping rows to objects at all is to decouple the application from the database. Therefore, it should be a priority to reduce the amount of database detail that the classes contain. Personally, I like the idea of having simple data structures that doesn't even depend on the ORM library, and still map database rows to them. However, most of the time we need to configure our models a bit to fit our database needs. To remain non-dependant on the actual database code (with the ability to reuse the models in front end code) annotations can be used.

I still feel like the annotations should be kept at a minimum, and storing the entire schema in the models force them to contain a lot of configuration. And I don't think it's the models responsibility to know that a field should be indexed in the database, for example.

Argument 2

If we store the schema of our rows in the objects that represent them, we lose control over the changes to the schema. Let's say I have a project that is already in production, with a User class and a users table with thousands of rows.

If I want to add a column to the users schema, and I add it to the model (with annotations) then the ORM has to not only realize that something has changed in the schema, but then apply that change without truncating the table.

However, if we have explicit migrations, we can create a new migration that does the schema change, and then just migrate the database on the live server. The migrator will then see that the only migration that is not applied is the one that adds the column, so it knows (and we know) exactly what it should do.

luisvt commented 9 years ago

sorry, maybe I didn't explain too well. When I say that I prefer JPA annotations. I mean this:

insteat:

@hasOne
@hasMany
@belongsTo
@BelongsToMany
@BelongsToOne

I prefer:

@OneToOne
@OneToMany
@ManyToMany
@ManyToOne

In Java JPA annotations are not used for migration of the database. However frameworks like Hibernate use them to create a database schema. Most of the time database migrations are done using Liquidbase wich uses XML, YAML, or SQL to do migrations, and also Flyway which is pure SQL.

In JPA they don't use @Field they use @Column to refer that the attribute is going to be mapped as column in the table. I don't like using @Column since is highly tied to SQL dbs. In that case I would prefer @Field. However this annotation is optional, so an attribute could be mapped to a column without having the annotation.

emilniklas commented 9 years ago

I see! If a field is annotated with @OneToMany there is no hierarchy, right?

class Article {
  @OneToMany List<Comment> comments;
}

class Comment {
  @ManyToOne Article article;
}
luisvt commented 9 years ago

what do you mean with hierarchy?

emilniklas commented 9 years ago

Logically, in a one-to-many relationship with articles and comments, each comment belongs to an article, and each article has many comments. There is a logical hierarchy there that I kind of like.

For example, each comment belongs to an article (article is parent, comment is child). A category can be shared between articles, but can be both the parent and the child (each category has articles, or each article has a category). By using has and belongs we establish a sense of hierarchy between the entities.

I don't know, the idea is quite elusive, but I think the syntax is very expressive. Do you disagree? (Don't get me wrong, I'm all for changing the terminology if you convince me that that's better :smile:)

emilniklas commented 9 years ago

Take articles and tags for example. I would say articles has tags, and tags belong to articles:

class Tag {
  @belongsToMany List<Article> articles;
}

class Article {
  @hasMany List<Tag> tags;
}

But with the JPA style that hierarchy is lost:

class Tag {
  @manyToMany List<Article> articles;
}

class Article {
  @manyToMany List<Tag> tags;
}
luisvt commented 9 years ago

To begin with I'm not sure if it is better or not.

In general the annotation @OneToMany and @ManyToMany use a parameter called mappedBy. This parameter is in charge of creating the hierarchy. It would be like this:

class Article {
  @OneToMany(mappedBy="owner")
  List<Comment> comments;
}

class Comment {
  @ManyToOne Article article;
}

or for the case of ManyToMany

class Tag {
  @ManyToMany
  @JoinTable(
      name="TAG_ARTICLE",
      joinColumns={@JoinColumn(name="TAG_ID", referencedColumnName="ID")},
      inverseJoinColumns={@JoinColumn(name="ARTICLE_ID", referencedColumnName="ID")})
  List<Article> articles;
}

class Article {
  @ManyToMany(mappedBy="articles")
  List<Tag> tags;
}

I think one good point of using JPA annotations is that we would get more traction for JPA developers since it is going to be easier for them to migrate from java to dart.

I think that we should also realease a library called DPA, which contains all the JPA annotations, enumerations and interfaces, but not sure about that.

emilniklas commented 9 years ago

From looking at this i think we can do a lot better in terms of readability. I also think that attracting Java devs is a good idea but maybe not for a Dart only project. Feel free to fork the ORM and create a JPA style ORM using the underlying Trestle gateway, if you want/can!

I'll keep this in mind and think some more about it!