Value Pricing - NOT

If you want to sell business software these days, then you better demonstrate a clear cut return on investment. And I think that most of those who supported the new Moveable Type license thought along these lines: I want the value of the software to exceed its price or fair is what a buyer and a seller agree to exchange based on the value. But I don’t think that the personal software buyer thinks that way.

Here’s one way we think different: we don’t like metered service. We worry more about the peaks than the average. Give us a choice between metered and unlimited service and we’ll go with unlimited. We get flat rate local phone service even though many of us would save with a per call plan. We love Netflix because we’re never overdue.

Once we’ve experienced unlimited service, the genie is out of the bottle. Try to take it back, and we’ll vote with our feet. Because someone else will offer it and they’ll be laughing all the way to the bank.

All idioms are learned, Good idioms are only learned once

While doing this work I’ve gone back to the conversations I’ve had with Alan Cooper back in the 1990s. I was in a small group of people who would meet with him and talk with him about the ideas he had that later turned into his book About Face.

Alan is a God. Yes, upper case “G.”

I was fortunate enough to take a UC extension course from Alan Cooper in the early 90’s. And the title quote, personas and my Cooper Software Design Kit (a pad of graph paper with a mechanical pencil) have stuck with me. Just a handful of Saturday mornings in Menlo Park - normal folk need years to make that big an impression.

Cascading actions from Parent to Child in Hibernate

It’s long past time for me to discuss the cascade attribute in Hibernate’s Collection support. Let’s use the BeanShell to explore the behavior of a simple collection for various attribute values. For reference, here is my abbreviated hibernate mapping file with the cascade element shown in bold:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<hibernate-mapping>
<class name="hb.KeySet"
table="keysets">


<id name="id" ... unsaved-value="-1">
<generator class="identity"/>
</id>

<property name="name" ... />
<property name="info" ... />

<set name="keys" ...
cascade="save-update" >

<key column="set_id"/>
<many-to-many class="hb.Keyword"
column="key_id"
/>

</set>

</class>
</hibernate-mapping>

The primary BeanShell scripted object was extended by adding the findAll method:

1
2
3
4
Collection findAll( String className )
{

return session.find( "from " + className );
}

And now we’re ready to examine different values of the cascade element. We’ll skip none, as we’ve already seen that Hibernate performs a shallow save when cascade=”none”.

save-update: cascades save and update actions from the parent to the child.

Note that the id value of the child Keyword changes from the unsaved-value of -1 to 0 when the parent KeySet is changed. And that the delete action does not cascade to the child.

1
bsh % import hb.*;
bsh % source("bsh/Hibernate.bsh");
bsh % hb = Hibernate();
bsh % tx = hb.transaction();
bsh % print(hb.findAll("Keyword"));
[]
bsh % print(hb.findAll("KeySet"));
[]
bsh % ireland = new Keyword("Ireland");
bsh % country = new KeySet("Country");
bsh % country.add(ireland);
bsh % print( country );
{-1:Country(null)|[-1:Ireland(null)]}
bsh % hb.save( country );
bsh % print( country );
{0:Country(null)|[0:Ireland(null)]}
bsh % tx.commit();
bsh % tx = hb.transaction();
bsh % country = hb.findByName("KeySet",null);
bsh % hb.delete( country );
bsh % tx.commit();
bsh % print( hb.findAll("Keyword") );
[0:Ireland(null)]
bsh % print( hb.findAll("KeySet") );
[]

delete: cascades the delete action from the parent to the child.

In this example, it is necessary to explicitly save both the parent and the child (observe the value of the child’s id field when the parent is saved first). The child Keyword is automatically deleted when the parent KeySet is saved.

1
bsh % import hb.*;
bsh % source("bsh/Hibernate.bsh");
bsh % hb = Hibernate();
bsh % tx = hb.transaction();
bsh % print( hb.findAll("Keyword"));
[]
bsh % print( hb.findAll("KeySet"));
[]
bsh % ireland = new Keyword("Ireland");
bsh % country = new KeySet("Country");
bsh % country.add(ireland);
bsh % hb.save(country);
bsh % print( country );
{0:Country(null)|[-1:Ireland(null)]}
bsh % hb.save(ireland);
bsh % print( country );
{0:Country(null)|[0:Ireland(null)]}
bsh % tx.commit();
bsh % tx = hb.transaction();
bsh % country = hb.findByName("KeySet",null);
bsh % hb.delete( country );
bsh % tx.commit();
bsh % print( hb.findAll("Keyword") );
[]
bsh % print( hb.findAll("KeySet") );
[]
```  
> 
> This example shows why you need to use delete with care -- hibernate will attempt to delete children that are shared with existing parents.  
> 
> ``` shell
bsh % tx = hb.transaction();
bsh % ireland = new Keyword("Ireland");
bsh % country = new KeySet("Country");
bsh % uk = new KeySet("United Kingdom");
bsh % country.add( ireland );
bsh % uk.add( ireland );
bsh % hb.save( ireland );
bsh % hb.save( country );
bsh % hb.save( uk );
bsh % tx.commit();
bsh % tx = hb.transaction();
bsh % country = hb.findByName("KeySet","Country");
bsh % hb.delete(country);
bsh % tx.commit();
// Error: // Uncaught Exception: Method Invocation tx.commit : at Line: 1
                               : in file: <unknown file> : tx .commit ( )  
> 
> Target exception: net.sf.hibernate.JDBCException: could not delete: [hb.Keyword#1]

all: all actions are cascaded from the parent to the child.

All is the union of save-update and delete. It has the benefits of both (save and delete actions are propagated).

1
bsh % import hb.*;
bsh % source ("bsh/Hibernate.bsh");
bsh % hb = Hibernate();
bsh % tx = hb.transaction();
bsh % print( hb.findAll("Keyword"));
[]
bsh % print( hb.findAll("KeySet"));
[]
bsh % ireland = new Keyword( "Ireland" );
bsh % country = new KeySet( "Country" );
bsh % country.add( ireland );
bsh % print( country );
{-1:Country(null)|[-1:Ireland(null)]}
bsh % hb.save( country );
bsh % print( country );
{0:Country(null)|[0:Ireland(null)]}
bsh % tx.commit();
bsh % tx = hb.transaction();
bsh % country = hb.findByName("KeySet",null );
bsh % print( country );
{0:Country(null)|[0:Ireland(null)]}
bsh % hb.delete( country );
bsh % tx.commit();
bsh % tx = hb.transaction();
bsh % print( hb.findAll("Keyword"));
[]
bsh % print( hb.findAll("KeySet"));
[]
bsh % hb.close();

And the downside of delete – hibernate will attempt to delete children that are shared with existing parents.

1
bsh % import hb.*;
bsh % source("bsh/Hibernate.bsh");
bsh % hb = Hibernate();
bsh % tx = hb.transaction();
bsh % print( hb.findAll("Keyword"));
[]
bsh % print( hb.findAll("KeySet"));
[]
bsh % ireland = new Keyword("Ireland");
bsh % country = new KeySet("Country");
bsh % uk = new KeySet("United Kingdom");
bsh % country.add( ireland );
bsh % uk.add( ireland );
bsh % hb.save( country );
bsh % hb.save( uk );
bsh % tx.commit();
bsh % tx = hb.transaction();
bsh % country = hb.findByName("KeySet","Country");
bsh % print( country );
{0:Country(null)|[0:Ireland(null)]}
bsh % hb.delete( country );
bsh % tx.commit();
// Error: // Uncaught Exception: Method Invocation tx.commit
                               : at Line: 1 : in file:  : tx .commit ( )
> 
> Target exception: net.sf.hibernate.HibernateException: Flush during cascade is
       dangerous - this might occur if an object was deleted and then re-saved
       by cascade (remove deleted object from associations)

Note that delete actions do not cascade to orphan children.

1
bsh % hb = Hibernate();
bsh % tx = hb.transaction();
bsh % print( hb.findAll("Keyword"));
[0:Ireland(null)]
bsh % print( hb.findAll("KeySet"));
[{0:Country(null)|[0:Ireland(null)]}]
bsh % ireland = hb.findByName("Keyword",null);
bsh % country = hb.findByName("KeySet",null);
bsh % country.remove( ireland );
bsh % hb.delete( country );
bsh % tx.commit();
bsh % print( hb.findAll("Keyword") );
[0:Ireland(null)]
bsh % print( hb.findAll("KeySet") );
[]

all-delete-orphan: all actions are cascaded from the parent to the child, orphan children are deleted.

Child is removed from the parent. When the parent is saved [updated], the orphan child is deleted.

1
bsh % import hb.*;
bsh % source( "bsh/Hibernate.bsh" );
bsh % hb = Hibernate();
bsh % tx = hb.transaction();
bsh % print ( hb.findAll("Keyword") );
[0:Ireland(null)]
bsh % print ( hb.findAll("KeySet"));
[{2:Country(null)|[0:Ireland(null)]}]
bsh % country = hb.findByName("KeySet",null);
bsh % ireland = hb.findByName("Keyword",null);
bsh % country.remove(ireland);
bsh % hb.save( country );
bsh % tx.commit();
bsh % print( hb.findAll("Keyword"));
[]
bsh % print( hb.findAll("KeySet"));
[{2:Country(null)|[]}]

Child is removed from the parent. When the parent is deleted, the orphan child is deleted.

1
bsh % print( hb.findAll("Keyword") );
[1:Ireland(null)]
bsh % print( hb.findAll("KeySet") );
[{2:Country(null)|[1:Ireland(null)]}]
bsh % ireland = hb.findByName("Keyword",null);
bsh % country = hb.findByName("KeySet",null);
bsh % country.remove( ireland );
bsh % hb.delete( country );
bsh % tx.commit();
bsh % print( hb.findAll("Keyword"));
[]
bsh % print( hb.findAll("KeySet"));
[]

For my money, save-update is the winner here. If save-update-orphan-deletion (cascade save and update, delete orphans when necessary) existed, then that might be a strong second choice. This is a many-to-many relationship example, your mileage will vary for other relationships.

If you’d like to try these examples on your own, the cascading action example source is available. For the record, here’s my current Hibernate configuration.

Disclaimer: I don’t claim to be an expert on hibernate. Please send comments and corrections.

Follow the Money

Matt Blumberg is worried about BLAM. I think that if he follows the money, then he’ll see that most blam is an attempt to garner PageRank. Remove the motivation and you’ll minimize the bad behavior.

*Knock on Wood* I haven’t experienced a blam problem here at Take the First Step. I attribute that to clean living and rank stripped trackback and comments. Like Matt, I have a lot of misgivings about blacklists. I’d prefer to have comments stripped of rank transference, moderation to enable transference if appropriate, and whitelists to minimize the moderation load. And blacklists only if all that fails.

So far, step one is good enough for me.

Link of a Link Whitelists

David Sifry thinks we should all turn off comments and let people who want to comment get their own blog. That just turns comment spam into trackback spam. What we really need are automatic link of a link whitelists via Technorati.

If you link to someone from the main text of your blog, then you are probably willing to allow them to comment or trackback to your blog. While that is easy enough to build on your own, it provides a pretty narrow slice of the world. If you could specify the degree of separation to build a whitelist of places that you link to plus places that they link and so on, then you’d really have something. Of course, Technorati already knows all that.

TypeKey: Dead or Alive?

TypeKey has entered the ICU. The popular support for TypeKey hinged upon the social capital of Ben and Mena Trott. Between the capital drawdown of the MT 3.0 crisis and reports that spammers are already TypeKey registered, it badly needs life support. I’m not shedding any tears. I think that TypeKey is overkill for the spam problem and that PageRank Stripping is a better solution.

I don’t understand why people think comment links should be rank full. But given that they do, it seems to me that the answer is link moderation. Comment moderation stifles the conversation by introducing lag. Link moderation would allow comments to appear immediately, but links would be rank stripped until explicitly approved. A rank full whitelist could be added to alleviate the moderation burden.

Focus on the Save

As Six Apart goes through its self-inflicted MT 3.0 crisis, they need to focus on the save. It’s easier to win back customers than find new ones and it’s easier to save than to win back. Here are some thoughts to consider:

We don’t like metered service. Whether it’s local phone service or online access, most people don’t like metered service and prefer to pay a premium for unlimited service. I’d think about selling unlimited author rights - a license for an individual to post as often as they want and as many places as they want. This might lead to a 3 way personal license model built around software instances, authors with unlimited posting privilege, and part time authors drawing from a collective post pool.

Look back before looking forward. I think a lot of outrage comes from After all I did for you, this is what I get? If you want to win back the self-proclaimed developers and evangelists, then you need to reward their past performance. The developer’s contest is about the future. How about a license discount for everyone who moves/verifies their existing plug in on MT 3.0? And what about referral discounts for the evangelists out there?

Of course, you can also decide that the people you’re losing aren’t worth having as customers. The clock is ticking.

Sideline View of the MT 3.0 Imbroglio

BTW, My award for most insightful post goes to by John Gruber:

  • Features Sell Upgrades
  • Pre-Announcements are Almost Always Regretted

And this telling comment on open source:

With open source software, users put their faith in the licenses behind the software. …

With commercial software, users put their faith in the company behind the product.